/* NOTE: 思路: 使用场景 1 筛选条件在页面内,一般解决方案就能解决 2 筛选条件在弹层内,弹层未展示之前或者收起时,视图可能会被销毁 3 表单填写 应对情况2,把原mixins文件进行拆分,分为数据部分与视图部分, 数据部分负责持久化逻辑 ,可与视图mixins合并使用,也可依附于父级视图独立工作 视图部分负责渲染与UI交互,并将接收到值反应到数据部分 如果同时包含数据与视图,称为联合模式,UI方法会内部消化,而条件方法则会向外抛出事件, 如果分开,称为独立模式,则UI方法会向外抛出事件,以通知mixins-data更改,而条件方法则会内部消化。 下划线'_'开头的为内部使用, 非下划线开头为公开API 原则上只要有在2场景中使用的情况 mixins-data mixins-view就最好分开引用 但也可以引用在一起通过mixinsDataEnabled mixinsViewEnabled控制 注意:这两个开关千万别乱用!!! 应对情况3,将mixins-data, mixins-view同时引用即可 此情况下,视图可以用v-model将UI输入映射致model, 也可通过setModelValue触发 组件逻辑的核心在于解析元素实体依赖关系,并不一定局限于搜索功能!!! 即任何需要体现依赖关系的地方都可以使用此组件 TODO: 稳定后去除console.log */ // 22.2.22 hht 改进context动态引入 // noinspection JSUnresolvedFunction const conditionModules = require.context('./condition-object', false, /\.js$/) const conditions = conditionModules.keys().map(key => conditionModules(key).default) export default { props: { localData: { // 22.2.22 hht 有的选择项并不需要从API获取,定义些属性,用于传入一些本地数据 type: Object, default: _ => ({}) }, queryParams: { type: Object, default: _ => null }, queryRules: { type: Object, default: _ => null }, requireFields: { type: Array, default: _ => [] } }, data() { return { mixinsDataEnabled: true, // [联合/独立]模式识别标记 如非必要,不要改动这个值 ignoreUnmatched: true, // 如果model中有未匹配到的,乎略与否。乎略的话仅打印提示 fillRequiresWhenInit: false, // 如果model中有属性要求必填,初始化时是否自动填充 dataCache: {}, // getList数据容器,可加快请求速度,如果想增加缓存影响,可从外部引入作用域更广的对象 model: {}, // 条件模型,按需要按顺序传入,属性名对应condition-object.key requires: [], // [key1,key2]必选项,非必选项会添加'所有'; 另外会影响校验方法和事件触发 rules: null, // 22.3.3 hht 可以尝试用标准的rules把requires替换掉 formRef: 'form', conditionsOutput: [], // 用于展示的条件合集 [{key:'',value:'',title:'',list:[],error:''}] conditionsOutputTemporary: {}, // 每次需要重新构建条件时,存储条件数据的临时容器 // 如果图方便,可以把所有支持的条件都加进来, 但要确保condition-key不冲突 imports: conditions, useCache: true, // 缓存开关 initChangedNotified: false, // 已通知标记 modelTemporary: {}, // 临时存储属性,用于独立手动模式临时存储与还原 // 注意 _开头的变量会被VUE忽略掉?! 比如this._modelSnapshot -> undefined // TODO: _dependencyTree 其实不需要每次都全量组建,有优化空间 _modelSnapshot: null, // {} model传入时的快照,用于重置功能 _modelKeys: null, //[] 读取model属性名,对应imports中的condition-key,仅保留参与条件运算的key,允许model挂载一些其它属性 _dependencyTree: null, // {key1:[childKeys],key2:[childKeys]} // 因为条件影响向下传递,只需要两级,递归算法会读取所有依赖 _restoreProcessing: false // 正处于还原过程 } }, computed: { // 22.3.2 hht 将校验收拢到el-form,为尽量兼容历史写法,先保留requires属性 finalRules() { const rules = this.rules || {} this.requires.forEach(key => { const condition = this.imports.find(c => c.key == key) if (condition) { rules[key] = [{ required: true, message: `${condition.title}必选` }] } }) return rules } }, watch: { model(val) { console.log('_initialization from watch', val) this._initialization(val) }, queryParams: { immediate: true, handler: function(val) { console.log('model assign from watch', val) this.model = val } }, queryRules: { immediate: true, handler: function(val) { console.log('rule assign from watch', val) this.rules = val } }, requireFields: { immediate: true, handler: function(val) { console.log('requires assign from watch', val) this.requires = val } } }, methods: { // 联合模式:对外抛出事件(这些事件可能会重合, 请按需监听) == 独立模式:按需重写对应Action // singleChanged==singleChangedAction单项变化, 不考虑校验,改值就触发 // changed==changedAction有效变化,非初始化的有效变化 // invalidChanged==invalidChangedAction无效变化,非初始化的无效变化 // initChanged==initChangedAction初始化时的有效变化, 因为有必填或必选项, 借这个方法可以把触发外部查询的时机内聚到该组件内 // initInvalidChanged==initInvalidChangedAction初始化时的未通过校验的事件 // 公共API isCombineMode() { // 此方法请与mixins-view保持一致 return this.mixinsViewEnabled && this.mixinsDataEnabled }, setModelValue(key, newVal) { // reset this.conditionsOutputTemporary = {} // trigger this.model[key] = newVal this._rebuildConditionFrom(key, false).then(() => this._triggerEventIfNeed()) }, valid() { return this._validAndHandleErrors() }, reset() { const mSnapshot = this._modelSnapshot this.model = mSnapshot this._modelSnapshot = null this.modelTemporary = null }, resetInitNotify() { // init方法会重新触发initChanged事件 this.initChangedNotified = false }, resetAll(clearCache = true) { // 同页面场景切换可以先调用此方法,再给model赋值,触发初始化 if (clearCache) this.clearCache() this.initChangedNotified = false this._modelSnapshot = null this.modelTemporary = null this.conditionsOutput = [] }, clearCache(key = null) { if (!key) { this.dataCache = {} } else { // 清除某个分类的缓存 Object.keys(this.dataCache).forEach(cacheKey => { if (cacheKey.endsWith(key)) { delete this.dataCache[cacheKey] } }) } }, backup() { // 独立模式弹层手动搜索,建立快照 if (this.model) { this.modelTemporary = this.deepClone(this.model) console.log('建立数据备份', this.modelTemporary) } }, restore() { // 独立模式弹层手动搜索,从快照还原 if (this.modelTemporary) { console.log('将还原数据备份FROM-TO', this.model, this.modelTemporary) this._restoreProcessing = true this.model = this.modelTemporary } }, anyConditionFired() { return this._modelKeys?.some(k => this.model[k]) }, getConditionLabel(key, code) { let condition = this.conditionsOutput.find(c => c.key == key) return condition?.list.find(i => i.code == code)?.label }, isRequired(key) { return this.finalRules.hasOwnProperty(key) }, // 公共API 重写部分 供独立模式挂载主体重写 singleChangedAction(model, key) { // for override }, changedAction(model) { // for override }, invalidChangedAction(errors, model) { // for override }, initChangedAction(model) { // for override }, initInvalidChangedAction(errors, model) { // for override }, // 内部方法 _initialization(model) { if (!model) return console.log('_initialization switcher mixinsDataEnabled', this.mixinsDataEnabled) if (!this.mixinsDataEnabled) return this._modelKeys = this._readEffectiveModelKeys(model) if (!this._modelKeys.length) return // 无效传值 // 重置相关变量 console.log('_initialization begin', this._modelKeys) // {...obj}不使用浅表拷贝是因为可能存在数组属性 if (!this._modelSnapshot) { this._modelSnapshot = this.deepClone(model) console.log('_initialization _modelSnapshot', this._modelSnapshot) } this._dependencyTree = {} // 读取属性名 建立关系树 const buildDependencySuccess = this._rebuildDependencyTree() if (!buildDependencySuccess) return console.log('build dependency tree success:', this._dependencyTree) this._rebuildAllConditions().then(() => this._triggerEventIfNeed()) }, _readEffectiveModelKeys(model) { let allKeys = Object.keys(model) if (!allKeys.length) return allKeys if (!this.ignoreUnmatched) return allKeys let unmatched = [] allKeys.forEach(key => { if (!this.imports.some(i => i.key == key)) { unmatched.push(key) } }) if (unmatched.length) { console.log(`警告:因为配置为乎略未匹配条件,[${unmatched}]将不做处理`) console.log(`乎略前所有条件:${allKeys}`) unmatched.forEach(k => allKeys.remove(k)) console.log(`乎略后有效条件:${allKeys}`) } return allKeys }, _rebuildDependencyTree() { const unmatchedKeys = [] const conflictKeys = [] this._modelKeys.forEach(key => { const condition = this.imports.find(c => c.key == key) if (condition) { // 递归解析key相关的依赖项 const dependencyCheckLink = [] const buildNodeSuccess = this._buildDependencyNode(condition.key, dependencyCheckLink) if (!buildNodeSuccess) { conflictKeys.push(`{${key}:${dependencyCheckLink}}`) } return } unmatchedKeys.push(key) }) if (unmatchedKeys.length) { console.log(`发现条件实现缺失${unmatchedKeys},请检查condition-object实现`) this.msgError(`imports中未找到[${unmatchedKeys}]`) } else if (conflictKeys.length) { this.msgError(`[${conflictKeys}]发现依赖冲突`) } return !unmatchedKeys.length && !conflictKeys.length }, _buildDependencyNode(key, dependencyCheckLink, fromKey) { if (dependencyCheckLink.includes(key)) { console.log(`发现循环依赖,请检查condition-object.${key}.independentKeys:`, key, dependencyCheckLink, fromKey) return false } // 循环依赖 dependencyCheckLink.push(key) if (!this._modelKeys.includes(key)) { console.log(`发现依赖缺省,请检查model.${key}是否存在:`, key, dependencyCheckLink, fromKey) return false } let selfChildren = this._dependencyTree[key] || [] if (fromKey && !selfChildren.includes(fromKey)) selfChildren.push(fromKey) this._dependencyTree[key] = selfChildren const matchCondition = this.imports.find(c => c.key == key) if (!matchCondition) { console.log('发现依赖中断,请检查condition-object:', key, dependencyCheckLink, fromKey) return false } // 依赖中断 let ancestorChecked = true matchCondition.dependentKeys.forEach(ancestorKey => { const result = this._buildDependencyNode(ancestorKey, dependencyCheckLink, key) ancestorChecked = ancestorChecked && result }) return ancestorChecked }, async _rebuildAllConditions() { // 从依赖树的根结点开始创建 this.conditionsOutputTemporary = {} const rootKeys = Object.keys(this._dependencyTree) for (const key of rootKeys) { await this._rebuildConditionFrom(key, true) } }, async _rebuildConditionFrom(key, includesSelf) { // 从某个属性开始构建条件 if (includesSelf) { const condition = this.imports.find(c => c.key == key) const withoutAll = this.isRequired(key) || condition.disableAllByForce const hasData = this.conditionsOutputTemporary[key]?.list.length > (withoutAll ? 0 : 1) if (hasData) return console.log('_rebuildConditionFrom ignored', key) console.log('_rebuildConditionFrom previous', key) await this._loadConditionData(condition) console.log('_rebuildConditionFrom suffix', key) } const dependencyKeys = this._dependencyTree[key] for (const dependencyKey of dependencyKeys) { await this._rebuildConditionFrom(dependencyKey, true) } }, async _loadConditionData(condition) { const { cacheKey, param } = this._getDependentParamAndCacheKey(condition) if (!condition.isDependencyReady(param)) return this._handleConditionData(condition, [], cacheKey) let list = this.dataCache[cacheKey] if (!list) list = await condition.getList(param, this) this._handleConditionData(condition, list, cacheKey) }, _handleConditionData(condition, list, cacheKey) { console.log('_handleConditionData', condition, list, cacheKey) // 只缓存有数据的情况 if (this.useCache && list?.length && !condition.neverCache) this.dataCache[cacheKey] = list // 处理显示对象 /* TODO: 现在还不确定一个条件同时依赖多个条件的情况是否能正常工作, 理论上一个依赖多个可以强制转换成单依赖,因为交互时用户也无法同时点两个条件 */ let displayList = list.map(item => ({ code: condition.getCode(item), label: condition.getLabel(item), _raw: item // 原数据引用提供一份 })) // 追加'所有' const isConditionRequired = this.isRequired(condition.key) if (!isConditionRequired && !condition.disableAllByForce) { displayList.unshift({ code: condition.allCode, label: condition.allLabel }) } // 校验当前值还是否有效 const currentVal = this.model[condition.key] let displayValue = condition.transferToEffectiveDisplayValue(currentVal, displayList) if (!condition.codeEquals(currentVal, displayValue)) { // 初始化过程和还原过程不改值 if (isConditionRequired) { if (this.initChangedNotified || this.fillRequiresWhenInit) { if (!this._restoreProcessing) { this.model[condition.key] = displayValue } } } else { this.model[condition.key] = displayValue } } // 包装并保存 const wrappedList = { key: condition.key, value: this.model[condition.key], title: condition.title, list: displayList, template: condition.template } this.conditionsOutputTemporary[condition.key] = wrappedList }, _getDependentParamAndCacheKey(condition) { let cacheKey = '', param = {} condition.dependentKeys.forEach(key => { const currentVal = this.model[key] cacheKey += `${key}_${currentVal}$` param[key] = currentVal }) cacheKey += condition.key if (condition.modelAsParam) { param = this.model } return { cacheKey, param } }, _mergeConditionsWithTemporary() { const finalConditions = [] for (const key of this._modelKeys) { const list = this.conditionsOutputTemporary[key] || this.conditionsOutput.find(c => c.key == key) if (!list) { if (this.model[key]) this.model[key] = '' // clear invalid value } else { const condition = this.imports.find(c => c.key == key) const withoutAll = this.isRequired(key) || condition.disableAllByForce const hasData = list.list.length > (withoutAll ? 0 : 1) if (hasData || this.isRequired(key)) { finalConditions.push(list) } } } this.conditionsOutput = finalConditions }, _triggerEventIfNeed() { // 整理最终的conditionOutput this._mergeConditionsWithTemporary() // 校验&外抛事件 this._restoreProcessing = false this.valid().then(() => { if (this.initChangedNotified) { this._triggerChangedEvent(this.model) } else { this._triggerInitChangedEvent(this.model) this.initChangedNotified = true } }).catch(errors => { if (this.initChangedNotified) { this._triggerInvalidChangedEvent(errors, this.model) } else { this._triggerInitInvalidChangedEvent(errors, this.model) this.initChangedNotified = true } }) }, _triggerSingleChangedEvent(model, key) { console.log('_triggerSingleChangedEvent', model, key, this._initKeys) if (this.isCombineMode()) { this.$emit('singleChanged', model, key) } else { this.singleChangedAction(model, key) } }, _triggerChangedEvent(model) { console.log('_triggerChangedEvent validChanged', model) if (this.isCombineMode()) { this.$emit('changed', model) } else { this.changedAction(model) } }, _triggerInvalidChangedEvent(errors, model) { console.log('_triggerInvalidChangedEvent invalidChanged', model) if (this.isCombineMode()) { this.$emit('invalidChanged', errors, model) } else { this.invalidChangedAction(model) } }, _triggerInitChangedEvent(model) { console.log('_triggerInitChangedEvent initValidChanged', model) if (this.isCombineMode()) { this.$emit('initChanged', model) } else { this.initChangedAction(model) } }, _triggerInitInvalidChangedEvent(errors, model) { console.log('_triggerInitInvalidEvent initInvalidChanged', model) if (this.isCombineMode()) { this.$emit('initInvalidChanged', errors, model) } else { this.initInvalidChangedAction(errors, model) } }, _validAndHandleErrors() { const formRef = this.$refs[this.formRef] if (!formRef || !formRef.validate) { console.log('警告:您未正确配置表单引用,组件校验工作中断。参见`this.formRef`') return Promise.resolve() } // return formRef.validate() 这种方式没有外抛异常明细 return new Promise((resolve, reject) => { const localFields = Object.keys(this.finalRules) const formFields = formRef.fields const shouldDelay = localFields.length > 0 && formFields.length == 0 const invoker = function(){ formRef.validate((valid, errors) => { //debugger if (valid) resolve(valid) else reject(errors) // 经这一步转化,可以外抛errors }) } // NOTE: 5.14 hht 这里因为form组件的校验要在子组件加载好之后才能生效 if (shouldDelay) this.$nextTick(()=> invoker()) else invoker() }) } } }