123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501 |
- /*
- 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()
- })
- }
- }
- }
|