优化 cl-cron 组件

This commit is contained in:
icssoa 2021-03-17 13:35:30 +08:00
parent 8cd5671580
commit 6f0a889ffc
5 changed files with 58 additions and 516 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "cool-admin-vue", "name": "cool-admin-vue",
"version": "3.0.0", "version": "3.0.2",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
@ -10,9 +10,9 @@
}, },
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
"cl-admin": "^1.3.3", "cl-admin": "^1.3.4",
"cl-admin-crud": "^1.4.5", "cl-admin-crud": "^1.6.0",
"cl-admin-theme": "^0.0.3", "cl-admin-theme": "^0.0.4",
"clipboard": "^2.0.7", "clipboard": "^2.0.7",
"codemirror": "^5.59.4", "codemirror": "^5.59.4",
"core-js": "^3.6.5", "core-js": "^3.6.5",
@ -28,6 +28,7 @@
"uuid": "^8.3.2", "uuid": "^8.3.2",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-codemirror": "^4.0.6", "vue-codemirror": "^4.0.6",
"vue-cron": "^1.0.9",
"vue-echarts": "^5.0.0-beta.0", "vue-echarts": "^5.0.0-beta.0",
"vue-router": "^3.2.0", "vue-router": "^3.2.0",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",

View File

@ -4,17 +4,13 @@ import Theme from "cl-admin-theme";
export default { export default {
modules: [ modules: [
"base", "base",
"task",
{ {
name: "upload", name: "upload",
options: { options: {
icon: "el-icon-picture", icon: "el-icon-picture",
text: "选择图片", text: "选择图片"
limitSize: 1,
rename: true
} }
}, },
{ {
name: "crud", name: "crud",
value: Crud, value: Crud,
@ -29,6 +25,7 @@ export default {
} }
} }
}, },
"task",
"copy", "copy",
"distpicker", "distpicker",
"demo", "demo",

View File

@ -1,459 +0,0 @@
/**
* 表单项基础类, 所有输入组件都继承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);
}
}
};

View File

@ -3,7 +3,7 @@
<el-tabs type="border-card"> <el-tabs type="border-card">
<el-tab-pane> <el-tab-pane>
<span slot="label"><i class="el-icon-date"></i> {{ text.Seconds.name }}</span> <span slot="label"><i class="el-icon-date"></i> {{ text.Seconds.name }}</span>
<div class="tabBody"> <div class="vue-cron__item">
<el-row> <el-row>
<el-radio v-model="second.cronEvery" label="1">{{ <el-radio v-model="second.cronEvery" label="1">{{
text.Seconds.every text.Seconds.every
@ -59,9 +59,10 @@
</el-row> </el-row>
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane> <el-tab-pane>
<span slot="label"><i class="el-icon-date"></i> {{ text.Minutes.name }}</span> <span slot="label"><i class="el-icon-date"></i> {{ text.Minutes.name }}</span>
<div class="tabBody"> <div class="vue-cron__item">
<el-row> <el-row>
<el-radio v-model="minute.cronEvery" label="1">{{ <el-radio v-model="minute.cronEvery" label="1">{{
text.Minutes.every text.Minutes.every
@ -117,9 +118,10 @@
</el-row> </el-row>
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane> <el-tab-pane>
<span slot="label"><i class="el-icon-date"></i> {{ text.Hours.name }}</span> <span slot="label"><i class="el-icon-date"></i> {{ text.Hours.name }}</span>
<div class="tabBody"> <div class="vue-cron__item">
<el-row> <el-row>
<el-radio v-model="hour.cronEvery" label="1">{{ <el-radio v-model="hour.cronEvery" label="1">{{
text.Hours.every text.Hours.every
@ -175,9 +177,10 @@
</el-row> </el-row>
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane> <el-tab-pane>
<span slot="label"><i class="el-icon-date"></i> {{ text.Day.name }}</span> <span slot="label"><i class="el-icon-date"></i> {{ text.Day.name }}</span>
<div class="tabBody"> <div class="vue-cron__item">
<el-row> <el-row>
<el-radio v-model="day.cronEvery" label="1">{{ text.Day.every }}</el-radio> <el-radio v-model="day.cronEvery" label="1">{{ text.Day.every }}</el-radio>
</el-row> </el-row>
@ -319,9 +322,10 @@
</el-row> </el-row>
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane> <el-tab-pane>
<span slot="label"><i class="el-icon-date"></i> {{ text.Month.name }}</span> <span slot="label"><i class="el-icon-date"></i> {{ text.Month.name }}</span>
<div class="tabBody"> <div class="vue-cron__item">
<el-row> <el-row>
<el-radio v-model="month.cronEvery" label="1">{{ <el-radio v-model="month.cronEvery" label="1">{{
text.Month.every text.Month.every
@ -378,9 +382,10 @@
</el-row> </el-row>
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane v-if="showYear || false"> <el-tab-pane v-if="showYear || false">
<span slot="label"><i class="el-icon-date"></i> {{ text.Year.name }}</span> <span slot="label"><i class="el-icon-date"></i> {{ text.Year.name }}</span>
<div class="tabBody"> <div class="vue-cron__item">
<el-row> <el-row>
<el-radio v-model="year.cronEvery" label="1">{{ <el-radio v-model="year.cronEvery" label="1">{{
text.Year.every text.Year.every
@ -443,10 +448,10 @@
</div> </div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<div class="bottom">
<!-- <span class="value">{{this.cron}}</span> --> <div class="vue-cron__footer">
<el-button type="primary" @click="change">{{ text.Save }}</el-button> <el-button @click="close">{{ text.Close }}</el-button>
<el-button type="primary" @click="close">{{ text.Close }}</el-button> <el-button type="primary" @click="save">{{ text.Save }}</el-button>
</div> </div>
</div> </div>
</template> </template>
@ -457,7 +462,7 @@ import Language from "./cn";
export default { export default {
name: "vue-cron", name: "vue-cron",
props: ["data", "i18n", "showYear"], props: ["value", "data", "i18n", "showYear"],
data() { data() {
return { return {
@ -733,7 +738,8 @@ export default {
getValue() { getValue() {
return this.cron; return this.cron;
}, },
change() { save() {
this.$emit("input", this.cron);
this.$emit("change", this.cron); this.$emit("change", this.cron);
this.close(); this.close();
}, },
@ -759,16 +765,24 @@ export default {
} }
}; };
</script> </script>
<style lang="scss" scoped>
.bottom {
text-align: center;
margin-top: 20px;
}
.tabBody /deep/ .el-row {
margin: 10px 0;
}
.vue-cron /deep/ .el-tabs { <style lang="scss" scoped>
.vue-cron {
/deep/ .el-tabs {
box-shadow: none; box-shadow: none;
}
&__item {
/deep/ .el-row {
min-height: 32px;
line-height: 32px;
margin-bottom: 10px;
}
}
&__footer {
margin-top: 10px;
text-align: center;
}
} }
</style> </style>

View File

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