icssoa 4 жил өмнө
parent
commit
5fc6bc1224

+ 1 - 1
package.json

@@ -12,7 +12,7 @@
 		"cl-admin": "^1.3.1",
 		"cl-admin-crud": "^1.4.0",
 		"cl-admin-export": "^1.0.5",
-		"cl-admin-theme": "^0.0.2",
+		"cl-admin-theme": "^0.0.3",
 		"clipboard": "^2.0.7",
 		"codemirror": "^5.59.4",
 		"core-js": "^3.6.5",

+ 8 - 0
public/theme/black.css

@@ -13124,6 +13124,13 @@
 	margin: 0;
 }
 
+.app-slider .cl-slider-menu .el-menu .el-submenu__title:hover,
+.app-slider .cl-slider-menu .el-menu .el-submenu__title.is-active,
+.app-slider .cl-slider-menu .el-menu .el-menu-item:hover,
+.app-slider .cl-slider-menu .el-menu .el-menu-item.is-active {
+	background-color: #1d1a1a !important;
+}
+
 .page-layout__right {
 	background-color: rgba(47, 52, 71, 0.9);
 }
@@ -13159,3 +13166,4 @@
 .page-layout__topbar .cl-menu-topbar .el-menu .el-menu-item.is-active {
 	background-color: rgba(47, 52, 71, 0.8) !important;
 }
+/*# sourceMappingURL=index.css.map */

+ 11 - 1
src/assets/css/common.scss

@@ -1,5 +1,15 @@
-$color-primary: #4165d7;
+$primary: #4165d7;
+
+$color-primary: var(--color-primary, $primary);
 $color-success: #67c23a;
 $color-danger: #f56c6c;
 $color-info: #909399;
 $color-warning: #e6a23c;
+
+:export {
+	colorPrimary: $primary;
+	colorSuccess: $color-success;
+	colorDanger: $color-danger;
+	colorInfo: $color-info;
+	colorWarning: $color-warning;
+}

+ 1 - 1
src/assets/css/element-variables.scss

@@ -1,4 +1,4 @@
-$--color-primary: $color-primary;
+$--color-primary: $primary;
 $--color-success: $color-success;
 $--color-danger: $color-danger;
 $--color-warning: $color-warning;

+ 3 - 2
src/config/env.js

@@ -30,7 +30,7 @@ export const baseUrl = (function() {
 export const iconfontUrl = ``;
 
 // 程序配置参数
-export const app = {
+export const app = store.get("__app__") || {
 	name: "COOL-ADMIN",
 
 	conf: {
@@ -41,7 +41,8 @@ export const app = {
 	},
 
 	theme: {
-		url: "" // 主题样式地址
+		color: "", // 主题色
+		url: "http://192.168.199.148:5000/black/index.css" // 主题样式地址
 	}
 };
 

+ 9 - 2
src/cool/modules/base/common/index.js

@@ -1,10 +1,17 @@
 import { iconfontUrl, app } from "@/config/env";
 import { createLink } from "../utils";
+import { colorPrimary } from "@/assets/css/common.scss";
 
 if (app.theme) {
-	if (app.theme.url) {
-		createLink(app.theme.url);
+	const { url, color } = app.theme;
+
+	if (url) {
+		createLink(url, "theme-style");
 	}
+
+	document
+		.getElementsByTagName("body")[0]
+		.style.setProperty("--color-primary", color || colorPrimary);
 }
 
 if (iconfontUrl) {

+ 2 - 2
src/cool/modules/base/components/menu/slider/index.js

@@ -11,14 +11,14 @@ export default {
 	},
 
 	computed: {
-		...mapGetters(["menuList", "menuCollapse", "browser", "conf"])
+		...mapGetters(["menuList", "menuCollapse", "browser", "app"])
 	},
 
 	watch: {
 		menuList() {
 			this.refresh();
 		},
-		"conf.showAMenu"() {
+		"app.conf.showAMenu"() {
 			this.$store.commit("SET_MENU_LIST");
 		}
 	},

+ 11 - 23
src/cool/modules/base/store/app.js

@@ -1,33 +1,27 @@
 import { app } from "@/config/env";
 import { deepMerge, getBrowser } from "cl-admin/utils";
+import store from "store";
+import common from "@/assets/css/common.scss";
+
+console.log(common);
 
 export default {
 	state: {
 		info: {
-			name: app.name
-		},
-		conf: {
-			...app.conf
+			...app
 		},
 		browser: {
 			isMobile: false
 		},
-		collapse: false,
-		upload: {
-			mode: "local"
-		}
+		collapse: false
 	},
 	getters: {
-		// 应用信息
-		appInfo: state => state.info,
 		// 应用配置
-		conf: state => state.conf,
+		app: state => state.info,
 		// 浏览器信息
 		browser: state => state.browser,
 		// 左侧菜单是否收起
-		menuCollapse: state => state.collapse,
-		// 上传配置
-		upload: state => state.upload
+		menuCollapse: state => state.collapse
 	},
 	actions: {
 		appLoad({ getters, dispatch }) {
@@ -36,14 +30,7 @@ export default {
 				dispatch("permMenu");
 				// 获取用户信息
 				dispatch("userInfo");
-				// 设置上传配置
-				dispatch("setUpload");
 			}
-		},
-		setUpload({ state }) {
-			this.$service.common.uploadMode().then(res => {
-				state.upload = res;
-			});
 		}
 	},
 	mutations: {
@@ -58,8 +45,9 @@ export default {
 		},
 
 		// 更新应用配置
-		UPDATE_CONF(state, val) {
-			deepMerge(state.conf, val);
+		UPDATE_APP(state, val) {
+			deepMerge(state.info, val);
+			store.set("__app__", state.info);
 		}
 	}
 };

+ 2 - 2
src/cool/modules/base/store/menu.js

@@ -82,7 +82,7 @@ export default {
 				};
 
 				// 监测自定义菜单
-				if (!getters.conf.customMenu) {
+				if (!getters.app.conf.customMenu) {
 					this.$service.common
 						.permMenu()
 						.then(res => {
@@ -118,7 +118,7 @@ export default {
 
 		// 设置左侧菜单
 		SET_MENU_LIST(state, index) {
-			const { showAMenu } = this.getters.conf;
+			const { showAMenu } = this.getters.app.conf;
 
 			if (isEmpty(index)) {
 				index = state.index;

+ 459 - 0
src/cool/modules/task/components/cron/base.js

@@ -0,0 +1,459 @@
+/**
+ * 表单项基础类, 所有输入组件都继承Base
+ * @module $ui/components/my-form/src/Base
+ */
+
+import { FormItem } from "element-ui";
+import { setStyle } from "element-ui/lib/utils/dom";
+import { addResizeListener, removeResizeListener } from "element-ui/lib/utils/resize-event";
+
+const _get = require("lodash/get");
+const _set = require("lodash/set");
+const _isEqual = require("lodash/isEqual");
+const _cloneDeep = require("lodash/cloneDeep");
+
+/**
+ * 深拷贝
+ * @param {*} value  要深拷贝的值
+ * @return {*} 返回拷贝后的值
+ */
+export function cloneDeep(value) {
+	return _cloneDeep(value);
+}
+
+/**
+ * 判断两个对象是否相等
+ * @param {*} object 对象1
+ * @param {*} other 对象2
+ * @return {boolean}
+ */
+export function isEqual(object, other) {
+	return _isEqual(object, other);
+}
+/**
+ * 插槽
+ * @member slots
+ * @property {string} before 输入组件前面的内容,仅当父组件是MyForm有效
+ * @property {string} after 输入组件后面的内容,仅当父组件是MyForm有效
+ * @property {string} label 定义字段的label内容,仅当父组件是MyForm有效
+ * @property {string} error 作用域插槽,定义验证错误提示内容,仅当父组件是MyForm有效
+ */
+
+export default {
+	inject: {
+		myForm: {
+			default: null
+		}
+	},
+	components: {
+		FormItem
+	},
+	/**
+	 * 属性参数
+	 * @member props
+	 * @property {string} [name] 表单域 model 字段名称, 等价于 el-form-item 的 prop 参数
+	 * @property {string} [width] 宽度,css属性,支持像素,百分比和表达式,也可以在MyForm中统一设置itemWidth
+	 * @property {object} [props] 输入组件参数对象,即 element 组件的参数
+	 * @property {Array} [options] 选项数据,数据优先顺序,options > loader > form.dictMap > form.loader
+	 * @property {Object} [keyMap] 选项数据对象属性名称映射, 默认:{id, parentId, label, value}
+	 * @property {boolean} [collapsible] 可收起
+	 * @property {boolean} [stopEnterEvent] 阻止回车事件冒泡
+	 * @property {string} [depend] 依赖字段名称
+	 * @property {*} [dependValue] 依赖字段的值,即依赖字段的值等于该值才会显示
+	 * @property {string} [cascade] 级联的上级字段名称,需要与loader配合加载数据
+	 * @property {Function} [loader] 加载数据函数,必须返回Promise
+	 * @property {string} [dict] 字典名称,只是标识,需要与loader配合 或 表单的dictMap加载数据
+	 * @property {boolean} [disabled] 禁用
+	 * @property {boolean} [readonly] 只读
+	 * @property {string} [placeholder] 占位文本
+	 *
+	 */
+	props: {
+		// 表单域 model 字段名称
+		name: String,
+		// 宽度,支持像素,百分比和表达式
+		width: String,
+		// 输入组件参数对象
+		props: Object,
+		// 选项数据,数据优先顺序,options > loader > form.dictMap > form.loader
+		options: Array,
+		// 选项数据对象属性名称映射
+		keyMap: {
+			type: Object,
+			default() {
+				return {
+					id: "id",
+					label: "label",
+					value: "value",
+					disabled: "disabled",
+					parentId: "parentId"
+				};
+			}
+		},
+		// 可折叠
+		collapsible: Boolean,
+
+		// 阻止回车事件冒泡
+		stopEnterEvent: Boolean,
+
+		// 依赖字段名称
+		depend: String,
+
+		// 依赖字段的值,即依赖字段的值等于该值才会显示
+		dependValue: [String, Number, Boolean, Object, Array, Function],
+
+		// 级联的上级字段名称,需要与loader配合加载数据
+		cascade: String,
+
+		// 加载数据函数,必须返回Promise
+		loader: Function,
+
+		// 字典名称,只是标识,需要与loader配合 或 表单的dictMap加载数据
+		dict: String,
+
+		// 禁用
+		disabled: Boolean,
+		// 只读
+		readonly: Boolean,
+
+		// 占位文本
+		placeholder: String,
+
+		// 尺寸
+		size: String
+	},
+	data() {
+		return {
+			// 级联的值缓存
+			cascadeValue: null,
+			// 当前选项数据
+			currentOptions: [],
+
+			// 正在调用loader
+			loading: false
+		};
+	},
+	computed: {
+		// 如果有name参数,并且是MyForm的子组件,即与MyForm的currentModel作双向绑定
+		// 否则与组件自身的value作双向绑定
+		fieldValue: {
+			get() {
+				if (this.name && this.myForm) {
+					const { currentModel } = this.myForm;
+					return _get(currentModel, this.name, this.getDefaultValue());
+				} else {
+					return this.value || this.getDefaultValue();
+				}
+			},
+			set(val) {
+				if (this.name && this.myForm) {
+					const { currentModel } = this.myForm;
+					const model = cloneDeep(currentModel);
+					_set(model, this.name, val);
+					if (!isEqual(currentModel, model)) {
+						this.myForm.currentModel[this.name] = model[this.name];
+						this.myForm.currentModel = model;
+					}
+				} else {
+					this.$emit("input", val);
+				}
+			}
+		},
+		// 字段域的宽度
+		itemWidth() {
+			// 优先取自身设置的宽度,没有就取父组件设置的公共设置宽度
+			return (
+				this.width || (this.myForm && this.myForm.itemWidth ? this.myForm.itemWidth : null)
+			);
+		},
+		// 字段域样式
+		itemStyle() {
+			return {
+				width: this.itemWidth
+			};
+		},
+		// 输入框组件参数
+		innerProps() {
+			return {
+				disabled: this.disabled,
+				readonly: this.readonly,
+				placeholder: this.placeholder,
+				size: this.size,
+				...this.props
+			};
+		}
+	},
+	watch: {
+		itemWidth: {
+			immediate: true,
+			handler() {
+				this.$nextTick(() => {
+					this.setContentWidth();
+				});
+			}
+		},
+		"myForm.currentCollapsed"(val) {
+			const { resetCollapsed, model } = this.myForm;
+			// 收起时重置表单项值
+			if (val && resetCollapsed && model && this.collapsible) {
+				this.$nextTick(() => {
+					// this.fieldValue = this.myForm.model[this.name]
+					this.fieldValue = _get(this.myForm.model, this.name, this.getDefaultValue());
+				});
+			}
+			// 开启了折叠功能
+			if (this.collapsible) {
+				// 折叠时先要清除事件句柄,因为原先的dom即将发生改变
+				if (val) {
+					removeResizeListener(this.$el, this.setContentWidth);
+				} else {
+					// 如果没有加载过选项数据,触发加载函数
+					if (!this.currentOptions || this.currentOptions.length === 0) {
+						this.loadOptions(this.myForm.currentModel, this);
+					}
+					// 展开时,待DOM生成后,重新注册事件句柄
+					this.$nextTick(() => {
+						addResizeListener(this.$el, this.setContentWidth);
+						this.setContentWidth();
+					});
+				}
+			}
+		},
+		// options 为了提高性能,不设置deep
+		options: {
+			immediate: true,
+			handler(val) {
+				this.currentOptions = cloneDeep(val) || [];
+				// options改变后,会触发表单验证,这里需要清楚验证错误信息
+				this.$nextTick(() => {
+					this.clearValidate();
+				});
+			}
+		}
+	},
+	methods: {
+		// 获取表单项的默认值,不同组件有不同的默认值,可在具体的组件重写这个函数
+		getDefaultValue() {
+			return "";
+		},
+		// 重置字段
+		resetField() {
+			this.$refs.elItem && this.$refs.elItem.resetField();
+		},
+		// 清除验证错误信息
+		clearValidate() {
+			this.$refs.elItem && this.$refs.elItem.clearValidate();
+		},
+		isCollapsed() {
+			if (!this.myForm) return false;
+
+			const { collapsible, currentCollapsed } = this.myForm;
+			// 是否已收起
+			return collapsible && currentCollapsed && this.collapsible;
+		},
+		isMatchDepend() {
+			// 没有设置依赖,即忽略,当已匹配处理
+			if (!this.depend || !this.myForm) return true;
+			const model = this.myForm.currentModel;
+			// 依赖不支持 按路径查找
+			const value = model[this.depend];
+			let isMatch = true;
+			// 如果 dependValue 是函数,执行回调函数返回布尔值
+			if (typeof this.dependValue === "function") {
+				isMatch = this.dependValue(value, model, this);
+			} else {
+				// 以上都不符合,即检验 dependValue 与 currentModel中的依赖属性是否一致
+				isMatch = isEqual(this.dependValue, value);
+			}
+
+			// 清除依赖不符合字段的值
+			if (!isMatch && this.name && model[this.name]) {
+				this.fieldValue = this.getDefaultValue();
+				delete model[this.name];
+			}
+			return isMatch;
+		},
+		// 传递给输入组件的插槽
+		createSlots(slots = []) {
+			return slots.map(name => {
+				return <template slot={name}>{this.$slots[name]}</template>;
+			});
+		},
+		// 渲染输入组件
+		renderComponent(vnode) {
+			// 如果组件不是MyForm的子组件,不需要包裹Item组件
+			if (!this.myForm) {
+				return vnode;
+			}
+			// el-form-item 作用域插槽
+			const scopedSlots = this.$scopedSlots.error
+				? {
+						error: props => (
+							<div class="el-form-item__error my-from__custom-error">
+								{this.$scopedSlots.error(props)}
+							</div>
+						)
+				  }
+				: null;
+
+			// 是否已收起
+			const collapsed = this.isCollapsed();
+			// 是否符合依赖项
+			const isMatched = this.isMatchDepend();
+
+			return (
+				<transition name={this.myForm.collapseEffect}>
+					{!collapsed && isMatched ? (
+						<FormItem
+							ref="elItem"
+							class="my-form-item"
+							{...{
+								props: this.$attrs,
+								scopedSlots: scopedSlots,
+								style: this.itemStyle
+							}}
+							// 停止回车键事件冒泡
+							nativeOnKeyup={this.stopEvent}
+							// el-form-item 的prop用name代替
+							prop={this.name}>
+							{// label 插槽
+							this.$slots.label ? (
+								<template slot="label">{this.$slots.label}</template>
+							) : null}
+							{this.$slots.before}
+							{vnode}
+							{this.$slots.after}
+						</FormItem>
+					) : (
+						// Vue组件必须要有一个根DOM,创建一个隐藏占位元素
+						<div style={{ display: "none" }}>{this.name}</div>
+					)}
+				</transition>
+			);
+		},
+		// 继承输入组件暴露的方法
+		extendMethods(ref, names = []) {
+			if (!ref) return;
+
+			names.forEach(name => {
+				// 子组件的方法加到实例
+				this[name] = (...args) => {
+					ref[name].apply(ref, args);
+				};
+			});
+		},
+		// 设置el-form-item内部的内容区宽度
+		setContentWidth() {
+			// 字段域没有设置宽度,默认自适应,不需要处理
+			if (!this.itemWidth || !this.$el) return;
+			const content = this.$el.querySelector(".el-form-item__content");
+			const label = this.$el.querySelector(".el-form-item__label");
+			if (content) {
+				const rect = label ? label.getBoundingClientRect() : { width: 0 };
+				const itemWidth = this.$el.getBoundingClientRect().width;
+				const contentWidth = itemWidth - rect.width;
+				setStyle(content, { width: `${contentWidth}px` });
+			}
+		},
+		// 阻止回车事件冒泡
+		stopEvent(e) {
+			if (this.stopEnterEvent) {
+				e.stopPropagation();
+			}
+		},
+		// 加载选项数据
+		loadOptions(model) {
+			// 已收起的,不需要处理
+			if (this.isCollapsed()) return;
+
+			// 如果不符合依赖,不处理
+			if (!this.isMatchDepend()) return;
+
+			// 数据优先顺序,options > loader > form.dictMap > form.loader
+			if (this.options) {
+				this.currentOptions = cloneDeep(this.options);
+				return;
+			}
+
+			if (this.loader) {
+				this.loading = true;
+				this.loader(model, this)
+					.then(res => {
+						this.currentOptions = cloneDeep(res);
+					})
+					.finally(() => {
+						this.loading = false;
+					});
+				return;
+			}
+
+			// 无form容器,终止
+			if (!this.myForm) return;
+
+			if (this.dict) {
+				const { dictMap } = this.myForm;
+				const options = (dictMap || {})[this.dict];
+				// 建立与表单的字典数据引用
+				if (options) {
+					this.currentOptions = options;
+					return;
+				}
+			}
+
+			if (this.myForm.loader) {
+				this.loading = true;
+				this.myForm
+					.loader(model, this)
+					.then(res => {
+						this.currentOptions = cloneDeep(res);
+					})
+					.finally(() => {
+						this.loading = false;
+					});
+			}
+		},
+		// 响应currentModel改变处理级联加载数据
+		handleWatch(model) {
+			// 级联上级的值
+			const val = model[this.cascade];
+			// 与上次的值不一致即重新获取数据
+			if (!isEqual(this.cascadeValue, val)) {
+				this.fieldValue = this.getDefaultValue();
+				this.cascadeValue = val;
+				this.loadOptions(model);
+			}
+		},
+		// 绑定级联
+		bindCascade() {
+			if (this.cascade && this.myForm) {
+				const model = this.myForm.currentModel;
+				this.cascadeValue = model[this.cascade];
+				this.unwatch = this.$watch("myForm.currentModel", this.handleWatch, { deep: true });
+			}
+		},
+		// 销毁级联事件句柄
+		unbindCascade() {
+			this.unwatch && this.unwatch();
+		}
+	},
+	mounted() {
+		addResizeListener(this.$el, this.setContentWidth);
+	},
+	created() {
+		let model = null;
+		if (this.myForm) {
+			this.myForm.addItem(this);
+			model = this.myForm.currentModel;
+		}
+
+		this.loadOptions(model, this);
+		this.bindCascade();
+	},
+	beforeDestroy() {
+		removeResizeListener(this.$el, this.setContentWidth);
+		this.unbindCascade();
+		if (this.myForm) {
+			this.myForm.removeItem(this);
+		}
+	}
+};

+ 54 - 0
src/cool/modules/task/components/cron/cn.js

@@ -0,0 +1,54 @@
+export default {
+	Seconds: {
+		name: "秒",
+		every: "每一秒钟",
+		interval: ["每隔", "秒执行 从", "秒开始"],
+		specific: "具体秒数(可多选)",
+		cycle: ["周期从", "到", "秒"]
+	},
+	Minutes: {
+		name: "分",
+		every: "每一分钟",
+		interval: ["每隔", "分执行 从", "分开始"],
+		specific: "具体分钟数(可多选)",
+		cycle: ["周期从", "到", "分"]
+	},
+	Hours: {
+		name: "时",
+		every: "每一小时",
+		interval: ["每隔", "小时执行 从", "小时开始"],
+		specific: "具体小时数(可多选)",
+		cycle: ["周期从", "到", "小时"]
+	},
+	Day: {
+		name: "天",
+		every: "每一天",
+		intervalWeek: ["每隔", "周执行 从", "开始"],
+		intervalDay: ["每隔", "天执行 从", "天开始"],
+		specificWeek: "具体星期几(可多选)",
+		specificDay: "具体天数(可多选)",
+		lastDay: "在这个月的最后一天",
+		lastWeekday: "在这个月的最后一个工作日",
+		lastWeek: ["在这个月的最后一个"],
+		beforeEndMonth: ["在本月底前", "天"],
+		nearestWeekday: ["最近的工作日(周一至周五)至本月", "日"],
+		someWeekday: ["在这个月的第", "个"]
+	},
+	Week: ["天", "一", "二", "三", "四", "五", "六"].map(val => "星期" + val),
+	Month: {
+		name: "月",
+		every: "每一月",
+		interval: ["每隔", "月执行 从", "月开始"],
+		specific: "具体月数(可多选)",
+		cycle: ["从", "到", "月之间的每个月"]
+	},
+	Year: {
+		name: "年",
+		every: "每一年",
+		interval: ["每隔", "年执行 从", "年开始"],
+		specific: "具体年份(可多选)",
+		cycle: ["从", "到", "年之间的每一年"]
+	},
+	Save: "保存",
+	Close: "关闭"
+};

+ 774 - 0
src/cool/modules/task/components/cron/cron.vue

@@ -0,0 +1,774 @@
+<template>
+	<div class="vue-cron">
+		<el-tabs type="border-card">
+			<el-tab-pane>
+				<span slot="label"><i class="el-icon-date"></i> {{ text.Seconds.name }}</span>
+				<div class="tabBody">
+					<el-row>
+						<el-radio v-model="second.cronEvery" label="1">{{
+							text.Seconds.every
+						}}</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="second.cronEvery" label="2"
+							>{{ text.Seconds.interval[0] }}
+							<el-input-number
+								size="small"
+								v-model="second.incrementIncrement"
+								:min="1"
+								:max="60"
+							></el-input-number>
+							{{ text.Seconds.interval[1] || "" }}
+							<el-input-number
+								size="small"
+								v-model="second.incrementStart"
+								:min="0"
+								:max="59"
+							></el-input-number>
+							{{ text.Seconds.interval[2] || "" }}
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio class="long" v-model="second.cronEvery" label="3"
+							>{{ text.Seconds.specific }}
+							<el-select size="small" multiple v-model="second.specificSpecific">
+								<el-option v-for="val in 60" :key="val" :value="val - 1">{{
+									val - 1
+								}}</el-option>
+							</el-select>
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="second.cronEvery" label="4"
+							>{{ text.Seconds.cycle[0] }}
+							<el-input-number
+								size="small"
+								v-model="second.rangeStart"
+								:min="1"
+								:max="60"
+							></el-input-number>
+							{{ text.Seconds.cycle[1] || "" }}
+							<el-input-number
+								size="small"
+								v-model="second.rangeEnd"
+								:min="0"
+								:max="59"
+							></el-input-number>
+							{{ text.Seconds.cycle[2] || "" }}
+						</el-radio>
+					</el-row>
+				</div>
+			</el-tab-pane>
+			<el-tab-pane>
+				<span slot="label"><i class="el-icon-date"></i> {{ text.Minutes.name }}</span>
+				<div class="tabBody">
+					<el-row>
+						<el-radio v-model="minute.cronEvery" label="1">{{
+							text.Minutes.every
+						}}</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="minute.cronEvery" label="2"
+							>{{ text.Minutes.interval[0] }}
+							<el-input-number
+								size="small"
+								v-model="minute.incrementIncrement"
+								:min="1"
+								:max="60"
+							></el-input-number>
+							{{ text.Minutes.interval[1] }}
+							<el-input-number
+								size="small"
+								v-model="minute.incrementStart"
+								:min="0"
+								:max="59"
+							></el-input-number>
+							{{ text.Minutes.interval[2] || "" }}
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio class="long" v-model="minute.cronEvery" label="3"
+							>{{ text.Minutes.specific }}
+							<el-select size="small" multiple v-model="minute.specificSpecific">
+								<el-option v-for="val in 60" :key="val" :value="val - 1">{{
+									val - 1
+								}}</el-option>
+							</el-select>
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="minute.cronEvery" label="4"
+							>{{ text.Minutes.cycle[0] }}
+							<el-input-number
+								size="small"
+								v-model="minute.rangeStart"
+								:min="1"
+								:max="60"
+							></el-input-number>
+							{{ text.Minutes.cycle[1] }}
+							<el-input-number
+								size="small"
+								v-model="minute.rangeEnd"
+								:min="0"
+								:max="59"
+							></el-input-number>
+							{{ text.Minutes.cycle[2] }}
+						</el-radio>
+					</el-row>
+				</div>
+			</el-tab-pane>
+			<el-tab-pane>
+				<span slot="label"><i class="el-icon-date"></i> {{ text.Hours.name }}</span>
+				<div class="tabBody">
+					<el-row>
+						<el-radio v-model="hour.cronEvery" label="1">{{
+							text.Hours.every
+						}}</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="hour.cronEvery" label="2"
+							>{{ text.Hours.interval[0] }}
+							<el-input-number
+								size="small"
+								v-model="hour.incrementIncrement"
+								:min="0"
+								:max="23"
+							></el-input-number>
+							{{ text.Hours.interval[1] }}
+							<el-input-number
+								size="small"
+								v-model="hour.incrementStart"
+								:min="0"
+								:max="23"
+							></el-input-number>
+							{{ text.Hours.interval[2] }}
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio class="long" v-model="hour.cronEvery" label="3"
+							>{{ text.Hours.specific }}
+							<el-select size="small" multiple v-model="hour.specificSpecific">
+								<el-option v-for="val in 24" :key="val" :value="val - 1">{{
+									val - 1
+								}}</el-option>
+							</el-select>
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="hour.cronEvery" label="4"
+							>{{ text.Hours.cycle[0] }}
+							<el-input-number
+								size="small"
+								v-model="hour.rangeStart"
+								:min="0"
+								:max="23"
+							></el-input-number>
+							{{ text.Hours.cycle[1] }}
+							<el-input-number
+								size="small"
+								v-model="hour.rangeEnd"
+								:min="0"
+								:max="23"
+							></el-input-number>
+							{{ text.Hours.cycle[2] }}
+						</el-radio>
+					</el-row>
+				</div>
+			</el-tab-pane>
+			<el-tab-pane>
+				<span slot="label"><i class="el-icon-date"></i> {{ text.Day.name }}</span>
+				<div class="tabBody">
+					<el-row>
+						<el-radio v-model="day.cronEvery" label="1">{{ text.Day.every }}</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="day.cronEvery" label="2"
+							>{{ text.Day.intervalWeek[0] }}
+							<el-input-number
+								size="small"
+								v-model="week.incrementIncrement"
+								:min="1"
+								:max="7"
+							></el-input-number>
+							{{ text.Day.intervalWeek[1] }}
+							<el-select size="small" v-model="week.incrementStart">
+								<el-option
+									v-for="val in 7"
+									:key="val"
+									:label="text.Week[val - 1]"
+									:value="val"
+								></el-option>
+							</el-select>
+							{{ text.Day.intervalWeek[2] }}
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="day.cronEvery" label="3"
+							>{{ text.Day.intervalDay[0] }}
+							<el-input-number
+								size="small"
+								v-model="day.incrementIncrement"
+								:min="1"
+								:max="31"
+							></el-input-number>
+							{{ text.Day.intervalDay[1] }}
+							<el-input-number
+								size="small"
+								v-model="day.incrementStart"
+								:min="1"
+								:max="31"
+							></el-input-number>
+							{{ text.Day.intervalDay[2] }}
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio class="long" v-model="day.cronEvery" label="4"
+							>{{ text.Day.specificWeek }}
+							<el-select size="small" multiple v-model="week.specificSpecific">
+								<el-option
+									v-for="val in 7"
+									:key="val"
+									:label="text.Week[val - 1]"
+									:value="
+										['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'][val - 1]
+									"
+								></el-option>
+							</el-select>
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio class="long" v-model="day.cronEvery" label="5"
+							>{{ text.Day.specificDay }}
+							<el-select size="small" multiple v-model="day.specificSpecific">
+								<el-option v-for="val in 31" :key="val" :value="val">{{
+									val
+								}}</el-option>
+							</el-select>
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="day.cronEvery" label="6">{{
+							text.Day.lastDay
+						}}</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="day.cronEvery" label="7">{{
+							text.Day.lastWeekday
+						}}</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="day.cronEvery" label="8"
+							>{{ text.Day.lastWeek[0] }}
+							<el-select size="small" v-model="day.cronLastSpecificDomDay">
+								<el-option
+									v-for="val in 7"
+									:key="val"
+									:label="text.Week[val - 1]"
+									:value="val"
+								></el-option>
+							</el-select>
+							{{ text.Day.lastWeek[1] || "" }}
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="day.cronEvery" label="9">
+							<el-input-number
+								size="small"
+								v-model="day.cronDaysBeforeEomMinus"
+								:min="1"
+								:max="31"
+							></el-input-number>
+							{{ text.Day.beforeEndMonth[0] }}
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="day.cronEvery" label="10"
+							>{{ text.Day.nearestWeekday[0] }}
+							<el-input-number
+								size="small"
+								v-model="day.cronDaysNearestWeekday"
+								:min="1"
+								:max="31"
+							></el-input-number>
+							{{ text.Day.nearestWeekday[1] }}
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="day.cronEvery" label="11"
+							>{{ text.Day.someWeekday[0] }}
+							<el-input-number
+								size="small"
+								v-model="week.cronNthDayNth"
+								:min="1"
+								:max="5"
+							></el-input-number>
+							<el-select
+								size="small"
+								v-model="week.cronNthDayDay"
+								style="margin-left: 5px"
+							>
+								<el-option
+									v-for="val in 7"
+									:key="val"
+									:label="text.Week[val - 1]"
+									:value="val"
+								></el-option>
+							</el-select>
+							{{ text.Day.someWeekday[1] }}
+						</el-radio>
+					</el-row>
+				</div>
+			</el-tab-pane>
+			<el-tab-pane>
+				<span slot="label"><i class="el-icon-date"></i> {{ text.Month.name }}</span>
+				<div class="tabBody">
+					<el-row>
+						<el-radio v-model="month.cronEvery" label="1">{{
+							text.Month.every
+						}}</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="month.cronEvery" label="2"
+							>{{ text.Month.interval[0] }}
+							<el-input-number
+								size="small"
+								v-model="month.incrementIncrement"
+								:min="0"
+								:max="12"
+							></el-input-number>
+							{{ text.Month.interval[1] }}
+							<el-input-number
+								size="small"
+								v-model="month.incrementStart"
+								:min="0"
+								:max="12"
+							></el-input-number>
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio class="long" v-model="month.cronEvery" label="3"
+							>{{ text.Month.specific }}
+							<el-select size="small" multiple v-model="month.specificSpecific">
+								<el-option
+									v-for="val in 12"
+									:key="val"
+									:label="val"
+									:value="val"
+								></el-option>
+							</el-select>
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="month.cronEvery" label="4"
+							>{{ text.Month.cycle[0] }}
+							<el-input-number
+								size="small"
+								v-model="month.rangeStart"
+								:min="1"
+								:max="12"
+							></el-input-number>
+							{{ text.Month.cycle[1] }}
+							<el-input-number
+								size="small"
+								v-model="month.rangeEnd"
+								:min="1"
+								:max="12"
+							></el-input-number>
+						</el-radio>
+					</el-row>
+				</div>
+			</el-tab-pane>
+			<el-tab-pane v-if="showYear || false">
+				<span slot="label"><i class="el-icon-date"></i> {{ text.Year.name }}</span>
+				<div class="tabBody">
+					<el-row>
+						<el-radio v-model="year.cronEvery" label="1">{{
+							text.Year.every
+						}}</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="year.cronEvery" label="2"
+							>{{ text.Year.interval[0] }}
+							<el-input-number
+								size="small"
+								v-model="year.incrementIncrement"
+								:min="1"
+								:max="99"
+							></el-input-number>
+							{{ text.Year.interval[1] }}
+							<el-input-number
+								size="small"
+								v-model="year.incrementStart"
+								:min="2018"
+								:max="2118"
+							></el-input-number>
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio class="long" v-model="year.cronEvery" label="3"
+							>{{ text.Year.specific }}
+							<el-select
+								size="small"
+								filterable
+								multiple
+								v-model="year.specificSpecific"
+							>
+								<el-option
+									v-for="val in 100"
+									:key="val"
+									:label="2017 + val"
+									:value="2017 + val"
+								></el-option>
+							</el-select>
+						</el-radio>
+					</el-row>
+					<el-row>
+						<el-radio v-model="year.cronEvery" label="4"
+							>{{ text.Year.cycle[0] }}
+							<el-input-number
+								size="small"
+								v-model="year.rangeStart"
+								:min="2018"
+								:max="2118"
+							></el-input-number>
+							{{ text.Year.cycle[1] }}
+							<el-input-number
+								size="small"
+								v-model="year.rangeEnd"
+								:min="2018"
+								:max="2118"
+							></el-input-number>
+						</el-radio>
+					</el-row>
+				</div>
+			</el-tab-pane>
+		</el-tabs>
+		<div class="bottom">
+			<!-- <span class="value">{{this.cron}}</span> -->
+			<el-button type="primary" @click="change">{{ text.Save }}</el-button>
+			<el-button type="primary" @click="close">{{ text.Close }}</el-button>
+		</div>
+	</div>
+</template>
+
+<script>
+import Language from "./cn";
+
+export default {
+	name: "vue-cron",
+
+	props: ["data", "i18n", "showYear"],
+
+	data() {
+		return {
+			second: {
+				cronEvery: "",
+				incrementStart: "3",
+				incrementIncrement: "5",
+				rangeStart: "",
+				rangeEnd: "",
+				specificSpecific: []
+			},
+			minute: {
+				cronEvery: "",
+				incrementStart: "3",
+				incrementIncrement: "5",
+				rangeStart: "",
+				rangeEnd: "",
+				specificSpecific: []
+			},
+			hour: {
+				cronEvery: "",
+				incrementStart: "3",
+				incrementIncrement: "5",
+				rangeStart: "",
+				rangeEnd: "",
+				specificSpecific: []
+			},
+			day: {
+				cronEvery: "",
+				incrementStart: "1",
+				incrementIncrement: "1",
+				rangeStart: "",
+				rangeEnd: "",
+				specificSpecific: [],
+				cronLastSpecificDomDay: 1,
+				cronDaysBeforeEomMinus: "",
+				cronDaysNearestWeekday: ""
+			},
+			week: {
+				cronEvery: "",
+				incrementStart: "1",
+				incrementIncrement: "1",
+				specificSpecific: [],
+				cronNthDayDay: 1,
+				cronNthDayNth: "1"
+			},
+			month: {
+				cronEvery: "",
+				incrementStart: "3",
+				incrementIncrement: "5",
+				rangeStart: "",
+				rangeEnd: "",
+				specificSpecific: []
+			},
+			year: {
+				cronEvery: "",
+				incrementStart: "2017",
+				incrementIncrement: "1",
+				rangeStart: "",
+				rangeEnd: "",
+				specificSpecific: []
+			},
+			output: {
+				second: "",
+				minute: "",
+				hour: "",
+				day: "",
+				month: "",
+				Week: "",
+				year: ""
+			}
+		};
+	},
+
+	watch: {
+		data() {
+			this.rest(this.$data);
+		}
+	},
+
+	computed: {
+		text() {
+			return Language;
+		},
+		secondsText() {
+			let seconds = "";
+			const cronEvery = this.second.cronEvery;
+			switch (cronEvery.toString()) {
+				case "1":
+					seconds = "*";
+					break;
+				case "2":
+					seconds = this.second.incrementStart + "/" + this.second.incrementIncrement;
+					break;
+				case "3":
+					this.second.specificSpecific.forEach(val => {
+						seconds += val + ",";
+					});
+					seconds = seconds.slice(0, -1);
+					break;
+				case "4":
+					seconds = this.second.rangeStart + "-" + this.second.rangeEnd;
+					break;
+			}
+			return seconds;
+		},
+		minutesText() {
+			let minutes = "";
+			const cronEvery = this.minute.cronEvery;
+			switch (cronEvery.toString()) {
+				case "1":
+					minutes = "*";
+					break;
+				case "2":
+					minutes = this.minute.incrementStart + "/" + this.minute.incrementIncrement;
+					break;
+				case "3":
+					this.minute.specificSpecific.forEach(val => {
+						minutes += val + ",";
+					});
+					minutes = minutes.slice(0, -1);
+					break;
+				case "4":
+					minutes = this.minute.rangeStart + "-" + this.minute.rangeEnd;
+					break;
+			}
+			return minutes;
+		},
+		hoursText() {
+			let hours = "";
+			const cronEvery = this.hour.cronEvery;
+			switch (cronEvery.toString()) {
+				case "1":
+					hours = "*";
+					break;
+				case "2":
+					hours = this.hour.incrementStart + "/" + this.hour.incrementIncrement;
+					break;
+				case "3":
+					this.hour.specificSpecific.forEach(val => {
+						hours += val + ",";
+					});
+					hours = hours.slice(0, -1);
+					break;
+				case "4":
+					hours = this.hour.rangeStart + "-" + this.hour.rangeEnd;
+					break;
+			}
+			return hours;
+		},
+		daysText() {
+			let days = "";
+			const cronEvery = this.day.cronEvery;
+			switch (cronEvery.toString()) {
+				case "1":
+					break;
+				case "2":
+				case "4":
+				case "11":
+					days = "?";
+					break;
+				case "3":
+					days = this.day.incrementStart + "/" + this.day.incrementIncrement;
+					break;
+				case "5":
+					this.day.specificSpecific.forEach(val => {
+						days += val + ",";
+					});
+					days = days.slice(0, -1);
+					break;
+				case "6":
+					days = "L";
+					break;
+				case "7":
+					days = "LW";
+					break;
+				case "8":
+					days = this.day.cronLastSpecificDomDay + "L";
+					break;
+				case "9":
+					days = "L-" + this.day.cronDaysBeforeEomMinus;
+					break;
+				case "10":
+					days = this.day.cronDaysNearestWeekday + "W";
+					break;
+			}
+			return days;
+		},
+		weeksText() {
+			let weeks = "";
+			const cronEvery = this.day.cronEvery;
+			switch (cronEvery.toString()) {
+				case "1":
+				case "3":
+				case "5":
+					weeks = "?";
+					break;
+				case "2":
+					weeks = this.week.incrementStart + "/" + this.week.incrementIncrement;
+					break;
+				case "4":
+					this.week.specificSpecific.forEach(val => {
+						weeks += val + ",";
+					});
+					weeks = weeks.slice(0, -1);
+					break;
+				case "6":
+				case "7":
+				case "8":
+				case "9":
+				case "10":
+					weeks = "?";
+					break;
+				case "11":
+					weeks = this.week.cronNthDayDay + "#" + this.week.cronNthDayNth;
+					break;
+			}
+			return weeks;
+		},
+		monthsText() {
+			let months = "";
+			const cronEvery = this.month.cronEvery;
+			switch (cronEvery.toString()) {
+				case "1":
+					months = "*";
+					break;
+				case "2":
+					months = this.month.incrementStart + "/" + this.month.incrementIncrement;
+					break;
+				case "3":
+					this.month.specificSpecific.forEach(val => {
+						months += val + ",";
+					});
+					months = months.slice(0, -1);
+					break;
+				case "4":
+					months = this.month.rangeStart + "-" + this.month.rangeEnd;
+					break;
+			}
+			return months;
+		},
+		yearsText() {
+			let years = "";
+			const cronEvery = this.year.cronEvery;
+			switch (cronEvery.toString()) {
+				case "1":
+					years = "*";
+					break;
+				case "2":
+					years = this.year.incrementStart + "/" + this.year.incrementIncrement;
+					break;
+				case "3":
+					this.year.specificSpecific.forEach(val => {
+						years += val + ",";
+					});
+					years = years.slice(0, -1);
+					break;
+				case "4":
+					years = this.year.rangeStart + "-" + this.year.rangeEnd;
+					break;
+			}
+			return years;
+		},
+		cron() {
+			return `${this.secondsText || "*"} ${this.minutesText || "*"} ${this.hoursText ||
+				"*"} ${this.daysText || "*"} ${this.monthsText || "*"} ${this.weeksText || "?"} ${
+				this.showYear ? this.yearsText || "*" : ""
+			}`;
+		}
+	},
+
+	methods: {
+		getValue() {
+			return this.cron;
+		},
+		change() {
+			this.$emit("change", this.cron);
+			this.close();
+		},
+		close() {
+			this.$emit("close");
+		},
+		rest(data) {
+			for (const i in data) {
+				if (data[i] instanceof Object) {
+					this.rest(data[i]);
+				} else {
+					switch (typeof data[i]) {
+						case "object":
+							data[i] = [];
+							break;
+						case "string":
+							data[i] = "";
+							break;
+					}
+				}
+			}
+		}
+	}
+};
+</script>
+<style lang="scss" scoped>
+.bottom {
+	text-align: center;
+	margin-top: 20px;
+}
+.tabBody /deep/ .el-row {
+	margin: 10px 0;
+}
+
+.vue-cron /deep/ .el-tabs {
+	box-shadow: none;
+}
+</style>

+ 80 - 0
src/cool/modules/task/components/cron/index.js

@@ -0,0 +1,80 @@
+import Cron from "./cron";
+import Base from "./base";
+
+export default {
+	name: "cl-cron",
+
+	mixins: [Base],
+
+	components: {
+		Cron
+	},
+
+	props: {
+		value: String,
+		placeholder: {
+			type: String,
+			default: "请输入定时策略"
+		}
+	},
+
+	data() {
+		return {
+			cronPopover: false,
+			cron: ""
+		};
+	},
+
+	watch: {
+		cron: {
+			handler(val) {
+				this.fieldValue = val;
+				this.$emit("change", val);
+			}
+		},
+		value: {
+			immediate: true,
+			handler(val) {
+				this.cron = val;
+			}
+		},
+		fieldValue: {
+			immediate: true,
+			handler(val) {
+				this.cron = val;
+			}
+		}
+	},
+
+	methods: {
+		changeCron(val) {
+			this.cron = val;
+		},
+		hidePopover() {
+			this.cronPopover = false;
+		}
+	},
+
+	render() {
+		const vnode = (
+			<el-popover vModel={this.cronPopover} disabled={this.disabled || this.readonly}>
+				<Cron
+					{...{
+						props: { i18n: "cn" },
+						on: {
+							change: this.changeCron,
+							close: this.hidePopover
+						}
+					}}></Cron>
+				<el-input
+					slot="reference"
+					clearable={true}
+					disabled={this.disabled}
+					readonly={this.readonly}
+					vModel={this.cron}
+					placeholder={this.placeholder}></el-input>
+			</el-popover>
+		);
+		return this.renderComponent(vnode);
+	}
+};

+ 2 - 6
src/cool/modules/task/views/task.vue

@@ -200,6 +200,7 @@
 import draggable from "vuedraggable";
 import { checkPerm } from "@/cool/modules/base";
 import { Form, ContextMenu } from "cl-admin-crud";
+import Cron from "../components/cron";
 
 export default {
 	name: "system-task",
@@ -467,12 +468,7 @@ export default {
 							return scope.taskType == 1;
 						},
 						value: info.cron,
-						component: {
-							name: "el-input",
-							attrs: {
-								placeholder: "* * * * * *"
-							}
-						},
+						component: Cron,
 						rules: {
 							required: true,
 							message: "cron不能为空"

+ 5 - 0
src/cool/modules/theme/components/index.js

@@ -0,0 +1,5 @@
+import Theme from "./theme";
+
+export default {
+	Theme
+};

+ 285 - 0
src/cool/modules/theme/components/theme.vue

@@ -0,0 +1,285 @@
+<template>
+	<div class="cl-theme">
+		<li @click="open">
+			<icon-svg :size="18" name="icon-theme"></icon-svg>
+		</li>
+
+		<!-- 系统设置 -->
+		<el-drawer title="系统设置" :visible.sync="drawer.visible" size="300px">
+			<div class="cl-theme__color is-card">
+				<p>主题</p>
+
+				<ul>
+					<el-tooltip
+						v-for="(item, name) in thems"
+						:key="name"
+						:content="item.label"
+						placement="top"
+					>
+						<li
+							:style="{
+								backgroundColor: item.color
+							}"
+							@click="setTheme(item)"
+						>
+							<i class="el-icon-check" v-if="item.color == app.theme.color"></i>
+						</li>
+					</el-tooltip>
+				</ul>
+			</div>
+
+			<div class="cl-theme__switch is-card">
+				<p>内容区域</p>
+
+				<ul>
+					<li>
+						<span>显示一级菜单栏</span>
+						<el-switch size="mini" v-model="app.conf.showAMenu"></el-switch>
+					</li>
+					<li>
+						<span>显示路由导航栏</span>
+						<el-switch size="mini" v-model="app.conf.showRouteNav"></el-switch>
+					</li>
+					<li>
+						<span>显示页面进程栏</span>
+						<el-switch size="mini" v-model="app.conf.showProcess"></el-switch>
+					</li>
+				</ul>
+			</div>
+
+			<div class="cl-theme__tips">
+				<el-alert
+					type="warning"
+					:closable="false"
+					show-icon
+					title="手动修改配置文件可设置为默认主题、布局。"
+				></el-alert>
+
+				<el-button
+					round
+					type="primary"
+					size="small"
+					style="width: 100%"
+					:disabled="!app.theme.url"
+					@click="openDesc"
+					>修改说明</el-button
+				>
+			</div>
+		</el-drawer>
+
+		<!-- 修改说明 -->
+		<cl-dialog
+			:visible.sync="desc.visible"
+			title="修改说明"
+			width="800px"
+			:props="{
+				'append-to-body': true
+			}"
+		>
+			<ul class="cl-theme__desc">
+				<li>
+					<p class="cl-theme__desc-label">修改主题色:</p>
+					<cl-codemirror v-model="desc.color"></cl-codemirror>
+				</li>
+
+				<li>
+					<p class="cl-theme__desc-label">修改应用配置:</p>
+					<cl-codemirror v-model="desc.conf"></cl-codemirror>
+				</li>
+			</ul>
+		</cl-dialog>
+	</div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import { isDev } from "@/config/env";
+
+export default {
+	name: "cl-theme",
+
+	data() {
+		return {
+			drawer: {
+				visible: false
+			},
+			desc: {
+				visible: false,
+				color: "",
+				conf: ""
+			},
+			thems: [
+				{
+					label: "钴蓝(默认)",
+					name: "blue",
+					color: "#4165d7"
+				},
+				{
+					label: "极黑",
+					name: "black",
+					color: "#2f3447"
+				},
+				{
+					label: "果绿色",
+					name: "green",
+					color: "#51C21A"
+				},
+				{
+					label: "酱紫色",
+					name: "purple",
+					color: "#d0378d"
+				}
+			],
+			isDev
+		};
+	},
+
+	computed: {
+		...mapGetters(["app", "modules"])
+	},
+
+	watch: {
+		app: {
+			deep: true,
+			handler() {
+				this.$store.commit("UPDATE_APP", this.app);
+			}
+		}
+	},
+
+	methods: {
+		open() {
+			this.drawer.visible = true;
+		},
+
+		close() {
+			this.drawer.visible = false;
+		},
+
+		// 设置主题
+		setTheme({ name, color, label }) {
+			this.$message.success(`切换主题:${label}`);
+
+			const theme = document.getElementById("theme-style");
+			const style = theme || document.createElement("link");
+
+			style.href = `${this.modules.theme.options.sourceUrl || "/theme/"}${name}.css`;
+
+			if (!theme) {
+				style.type = "text/css";
+				style.rel = "stylesheet";
+				style.id = "theme-style";
+
+				document
+					.getElementsByTagName("head")
+					.item(0)
+					.appendChild(style);
+			}
+
+			// 设置主题色和路径
+			this.app.theme.color = color;
+			this.app.theme.url = style.href;
+
+			// 设置 css 变量
+			document.getElementsByTagName("body")[0].style.setProperty("--color-primary", color);
+		},
+
+		// 打开修改说明
+		openDesc() {
+			this.desc.visible = true;
+
+			this.desc.color = `
+				// src/assets/css/common.scss
+				$primary: ${this.app.theme.color};
+			`;
+
+			this.desc.conf = `
+				// src/config/env.js
+				export const app = {
+					conf: ${JSON.stringify(this.app.conf)},
+					thems: {
+						url: "${this.app.theme.url}"
+					}
+				}
+			`;
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+.cl-theme {
+	.is-card {
+		padding: 20px 0;
+		margin: 0 20px 20px 20px;
+		border-bottom: 1px solid #f7f7f7;
+
+		& > p {
+			font-size: 15px;
+			font-weight: bold;
+			margin-bottom: 10px;
+		}
+	}
+
+	&__switch {
+		ul {
+			width: 100%;
+
+			li {
+				display: flex;
+				justify-content: space-between;
+				align-items: center;
+				height: 40px;
+				list-style: none;
+
+				span {
+					font-size: 13px;
+				}
+			}
+		}
+	}
+
+	&__color {
+		ul {
+			display: flex;
+			margin-top: 20px;
+
+			li {
+				list-style: none;
+				height: 20px;
+				width: 20px;
+				border-radius: 3px;
+				margin-right: 10px;
+				text-align: center;
+				color: #fff;
+				line-height: 20px;
+
+				&:hover {
+					opacity: 0.7;
+				}
+			}
+		}
+	}
+
+	&__tips {
+		padding: 10px 20px;
+
+		.el-button {
+			margin-top: 20px;
+		}
+	}
+
+	&__desc {
+		padding: 10px;
+
+		&-label {
+			margin-bottom: 10px;
+		}
+
+		li {
+			list-style: none;
+			margin-bottom: 20px;
+		}
+	}
+}
+</style>

+ 3 - 0
src/cool/modules/theme/index.js

@@ -0,0 +1,3 @@
+import components from "./components";
+
+export default { components };

+ 8 - 5
src/cool/modules/upload/components/index.vue

@@ -32,8 +32,6 @@
 				:http-request="action ? undefined : httpRequest"
 				:on-remove="_onRemove"
 				:on-preview="_onPreview"
-				:on-success="_onSuccess"
-				:on-error="onError"
 				:on-progress="onProgress"
 				:on-change="onChange"
 				:on-exceed="onExceed"
@@ -188,7 +186,7 @@ export default {
 	},
 
 	computed: {
-		...mapGetters(["token", "modules", "upload"]),
+		...mapGetters(["token", "modules"]),
 
 		conf() {
 			return this.modules.upload.options;
@@ -418,9 +416,9 @@ export default {
 		},
 
 		// 重设上传请求
-		httpRequest(req) {
+		async httpRequest(req) {
 			const isRename = isEmpty(this.rename) ? this.conf.rename : this.rename;
-			const { mode } = this.upload;
+			const mode = await this.uploadMode();
 
 			// 多种上传请求
 			const upload = file => {
@@ -509,6 +507,11 @@ export default {
 				.done(() => {
 					this.loading = false;
 				});
+		},
+
+		// 上传模式
+		uploadMode() {
+			return this.$service.common.uploadMode().then(res => res.mode);
 		}
 	}
 };

+ 2 - 2
src/pages/layout/index.vue

@@ -11,7 +11,7 @@
 				<topbar></topbar>
 			</div>
 
-			<div class="page-layout__process" v-if="conf.showProcess">
+			<div class="page-layout__process" v-if="app.conf.showProcess">
 				<cl-process />
 			</div>
 
@@ -41,7 +41,7 @@ export default {
 	},
 
 	computed: {
-		...mapGetters(["menuCollapse", "conf", "browser"]),
+		...mapGetters(["menuCollapse", "app", "browser"]),
 
 		isKeepAlive() {
 			return isEmpty(this.$route.meta.keepAlive) ? true : this.$route.meta.keepAlive;

+ 2 - 2
src/pages/layout/slider.vue

@@ -2,7 +2,7 @@
 	<div class="app-slider">
 		<div class="app-slider__logo" @click="toHome">
 			<img src="@/assets/icon/logo/silder-simple.png" />
-			<span v-if="!menuCollapse || browser.isMobile">{{ appInfo.name }}</span>
+			<span v-if="!menuCollapse || browser.isMobile">{{ app.name }}</span>
 		</div>
 
 		<div class="app-slider__menu">
@@ -16,7 +16,7 @@ import { mapGetters } from "vuex";
 
 export default {
 	computed: {
-		...mapGetters(["menuCollapse", "browser", "appInfo"])
+		...mapGetters(["menuCollapse", "browser", "app"])
 	},
 
 	methods: {

+ 3 - 3
src/pages/layout/topbar.vue

@@ -5,12 +5,12 @@
 		</div>
 
 		<!-- 一级菜单 -->
-		<div class="app-topbar__menu" v-if="conf.showAMenu">
+		<div class="app-topbar__menu" v-if="app.conf.showAMenu">
 			<cl-menu-topbar />
 		</div>
 
 		<!-- 路由导航 -->
-		<div class="app-topbar__route-nav" v-if="conf.showRouteNav">
+		<div class="app-topbar__route-nav" v-if="app.conf.showRouteNav">
 			<cl-route-nav />
 		</div>
 
@@ -52,7 +52,7 @@ import { href } from "cl-admin/utils";
 
 export default {
 	computed: {
-		...mapGetters(["userInfo", "menuCollapse", "conf", "modules"])
+		...mapGetters(["userInfo", "menuCollapse", "app", "modules"])
 	},
 
 	methods: {

+ 1 - 1
vue.config.js

@@ -13,7 +13,7 @@ const PROXY_LIST = {
 	},
 
 	"/ap": {
-		target: "https://admin.cn.utools.club",
+		target: "https://admin.cool-js.cool",
 		changeOrigin: true,
 		pathRewrite: {
 			"^/ap": ""