condition-mixins-data.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. /*
  2. NOTE: 思路:
  3. 使用场景
  4. 1 筛选条件在页面内,一般解决方案就能解决
  5. 2 筛选条件在弹层内,弹层未展示之前或者收起时,视图可能会被销毁
  6. 3 表单填写
  7. 应对情况2,把原mixins文件进行拆分,分为数据部分与视图部分,
  8. 数据部分负责持久化逻辑 ,可与视图mixins合并使用,也可依附于父级视图独立工作
  9. 视图部分负责渲染与UI交互,并将接收到值反应到数据部分
  10. 如果同时包含数据与视图,称为联合模式,UI方法会内部消化,而条件方法则会向外抛出事件,
  11. 如果分开,称为独立模式,则UI方法会向外抛出事件,以通知mixins-data更改,而条件方法则会内部消化。
  12. 下划线'_'开头的为内部使用, 非下划线开头为公开API
  13. 原则上只要有在2场景中使用的情况 mixins-data mixins-view就最好分开引用
  14. 但也可以引用在一起通过mixinsDataEnabled mixinsViewEnabled控制
  15. 注意:这两个开关千万别乱用!!!
  16. 应对情况3,将mixins-data, mixins-view同时引用即可
  17. 此情况下,视图可以用v-model将UI输入映射致model, 也可通过setModelValue触发
  18. 组件逻辑的核心在于解析元素实体依赖关系,并不一定局限于搜索功能!!!
  19. 即任何需要体现依赖关系的地方都可以使用此组件
  20. TODO: 稳定后去除console.log
  21. */
  22. // 22.2.22 hht 改进context动态引入
  23. // noinspection JSUnresolvedFunction
  24. const conditionModules = require.context('./condition-object', false, /\.js$/)
  25. const conditions = conditionModules.keys().map(key => conditionModules(key).default)
  26. export default {
  27. props: {
  28. localData: {
  29. // 22.2.22 hht 有的选择项并不需要从API获取,定义些属性,用于传入一些本地数据
  30. type: Object,
  31. default: _ => ({})
  32. },
  33. queryParams: {
  34. type: Object,
  35. default: _ => null
  36. },
  37. queryRules: {
  38. type: Object,
  39. default: _ => null
  40. },
  41. requireFields: {
  42. type: Array,
  43. default: _ => []
  44. }
  45. },
  46. data() {
  47. return {
  48. mixinsDataEnabled: true, // [联合/独立]模式识别标记 如非必要,不要改动这个值
  49. ignoreUnmatched: true, // 如果model中有未匹配到的,乎略与否。乎略的话仅打印提示
  50. fillRequiresWhenInit: false, // 如果model中有属性要求必填,初始化时是否自动填充
  51. dataCache: {}, // getList数据容器,可加快请求速度,如果想增加缓存影响,可从外部引入作用域更广的对象
  52. model: {}, // 条件模型,按需要按顺序传入,属性名对应condition-object.key
  53. requires: [], // [key1,key2]必选项,非必选项会添加'所有'; 另外会影响校验方法和事件触发
  54. rules: null, // 22.3.3 hht 可以尝试用标准的rules把requires替换掉
  55. formRef: 'form',
  56. conditionsOutput: [], // 用于展示的条件合集 [{key:'',value:'',title:'',list:[],error:''}]
  57. conditionsOutputTemporary: {}, // 每次需要重新构建条件时,存储条件数据的临时容器
  58. // 如果图方便,可以把所有支持的条件都加进来, 但要确保condition-key不冲突
  59. imports: conditions,
  60. useCache: true, // 缓存开关
  61. initChangedNotified: false, // 已通知标记
  62. modelTemporary: {}, // 临时存储属性,用于独立手动模式临时存储与还原
  63. // 注意 _开头的变量会被VUE忽略掉?! 比如this._modelSnapshot -> undefined
  64. // TODO: _dependencyTree 其实不需要每次都全量组建,有优化空间
  65. _modelSnapshot: null, // {} model传入时的快照,用于重置功能
  66. _modelKeys: null, //[] 读取model属性名,对应imports中的condition-key,仅保留参与条件运算的key,允许model挂载一些其它属性
  67. _dependencyTree: null, // {key1:[childKeys],key2:[childKeys]} // 因为条件影响向下传递,只需要两级,递归算法会读取所有依赖
  68. _restoreProcessing: false // 正处于还原过程
  69. }
  70. },
  71. computed: {
  72. // 22.3.2 hht 将校验收拢到el-form,为尽量兼容历史写法,先保留requires属性
  73. finalRules() {
  74. const rules = this.rules || {}
  75. this.requires.forEach(key => {
  76. const condition = this.imports.find(c => c.key == key)
  77. if (condition) {
  78. rules[key] = [{
  79. required: true,
  80. message: `${condition.title}必选`
  81. }]
  82. }
  83. })
  84. return rules
  85. }
  86. },
  87. watch: {
  88. model(val) {
  89. console.log('_initialization from watch', val)
  90. this._initialization(val)
  91. },
  92. queryParams: {
  93. immediate: true,
  94. handler: function(val) {
  95. console.log('model assign from watch', val)
  96. this.model = val
  97. }
  98. },
  99. queryRules: {
  100. immediate: true,
  101. handler: function(val) {
  102. console.log('rule assign from watch', val)
  103. this.rules = val
  104. }
  105. },
  106. requireFields: {
  107. immediate: true,
  108. handler: function(val) {
  109. console.log('requires assign from watch', val)
  110. this.requires = val
  111. }
  112. }
  113. },
  114. methods: {
  115. // 联合模式:对外抛出事件(这些事件可能会重合, 请按需监听) == 独立模式:按需重写对应Action
  116. // singleChanged==singleChangedAction单项变化, 不考虑校验,改值就触发
  117. // changed==changedAction有效变化,非初始化的有效变化
  118. // invalidChanged==invalidChangedAction无效变化,非初始化的无效变化
  119. // initChanged==initChangedAction初始化时的有效变化, 因为有必填或必选项, 借这个方法可以把触发外部查询的时机内聚到该组件内
  120. // initInvalidChanged==initInvalidChangedAction初始化时的未通过校验的事件
  121. // 公共API
  122. isCombineMode() {
  123. // 此方法请与mixins-view保持一致
  124. return this.mixinsViewEnabled && this.mixinsDataEnabled
  125. },
  126. setModelValue(key, newVal) {
  127. // reset
  128. this.conditionsOutputTemporary = {}
  129. // trigger
  130. this.model[key] = newVal
  131. this._rebuildConditionFrom(key, false).then(() => this._triggerEventIfNeed())
  132. },
  133. valid() {
  134. return this._validAndHandleErrors()
  135. },
  136. reset() {
  137. const mSnapshot = this._modelSnapshot
  138. this.model = mSnapshot
  139. this._modelSnapshot = null
  140. this.modelTemporary = null
  141. },
  142. resetInitNotify() {
  143. // init方法会重新触发initChanged事件
  144. this.initChangedNotified = false
  145. },
  146. resetAll(clearCache = true) {
  147. // 同页面场景切换可以先调用此方法,再给model赋值,触发初始化
  148. if (clearCache) this.clearCache()
  149. this.initChangedNotified = false
  150. this._modelSnapshot = null
  151. this.modelTemporary = null
  152. this.conditionsOutput = []
  153. },
  154. clearCache(key = null) {
  155. if (!key) {
  156. this.dataCache = {}
  157. } else {
  158. // 清除某个分类的缓存
  159. Object.keys(this.dataCache).forEach(cacheKey => {
  160. if (cacheKey.endsWith(key)) {
  161. delete this.dataCache[cacheKey]
  162. }
  163. })
  164. }
  165. },
  166. backup() {
  167. // 独立模式弹层手动搜索,建立快照
  168. if (this.model) {
  169. this.modelTemporary = this.deepClone(this.model)
  170. console.log('建立数据备份', this.modelTemporary)
  171. }
  172. },
  173. restore() {
  174. // 独立模式弹层手动搜索,从快照还原
  175. if (this.modelTemporary) {
  176. console.log('将还原数据备份FROM-TO', this.model, this.modelTemporary)
  177. this._restoreProcessing = true
  178. this.model = this.modelTemporary
  179. }
  180. },
  181. anyConditionFired() {
  182. return this._modelKeys?.some(k => this.model[k])
  183. },
  184. getConditionLabel(key, code) {
  185. let condition = this.conditionsOutput.find(c => c.key == key)
  186. return condition?.list.find(i => i.code == code)?.label
  187. },
  188. isRequired(key) {
  189. return this.finalRules.hasOwnProperty(key)
  190. },
  191. // 公共API 重写部分 供独立模式挂载主体重写
  192. singleChangedAction(model, key) {
  193. // for override
  194. },
  195. changedAction(model) {
  196. // for override
  197. },
  198. invalidChangedAction(errors, model) {
  199. // for override
  200. },
  201. initChangedAction(model) {
  202. // for override
  203. },
  204. initInvalidChangedAction(errors, model) {
  205. // for override
  206. },
  207. // 内部方法
  208. _initialization(model) {
  209. if (!model) return
  210. console.log('_initialization switcher mixinsDataEnabled', this.mixinsDataEnabled)
  211. if (!this.mixinsDataEnabled) return
  212. this._modelKeys = this._readEffectiveModelKeys(model)
  213. if (!this._modelKeys.length) return // 无效传值
  214. // 重置相关变量
  215. console.log('_initialization begin', this._modelKeys)
  216. // {...obj}不使用浅表拷贝是因为可能存在数组属性
  217. if (!this._modelSnapshot) {
  218. this._modelSnapshot = this.deepClone(model)
  219. console.log('_initialization _modelSnapshot', this._modelSnapshot)
  220. }
  221. this._dependencyTree = {}
  222. // 读取属性名 建立关系树
  223. const buildDependencySuccess = this._rebuildDependencyTree()
  224. if (!buildDependencySuccess) return
  225. console.log('build dependency tree success:', this._dependencyTree)
  226. this._rebuildAllConditions().then(() => this._triggerEventIfNeed())
  227. },
  228. _readEffectiveModelKeys(model) {
  229. let allKeys = Object.keys(model)
  230. if (!allKeys.length) return allKeys
  231. if (!this.ignoreUnmatched) return allKeys
  232. let unmatched = []
  233. allKeys.forEach(key => {
  234. if (!this.imports.some(i => i.key == key)) {
  235. unmatched.push(key)
  236. }
  237. })
  238. if (unmatched.length) {
  239. console.log(`警告:因为配置为乎略未匹配条件,[${unmatched}]将不做处理`)
  240. console.log(`乎略前所有条件:${allKeys}`)
  241. unmatched.forEach(k => allKeys.remove(k))
  242. console.log(`乎略后有效条件:${allKeys}`)
  243. }
  244. return allKeys
  245. },
  246. _rebuildDependencyTree() {
  247. const unmatchedKeys = []
  248. const conflictKeys = []
  249. this._modelKeys.forEach(key => {
  250. const condition = this.imports.find(c => c.key == key)
  251. if (condition) {
  252. // 递归解析key相关的依赖项
  253. const dependencyCheckLink = []
  254. const buildNodeSuccess = this._buildDependencyNode(condition.key, dependencyCheckLink)
  255. if (!buildNodeSuccess) {
  256. conflictKeys.push(`{${key}:${dependencyCheckLink}}`)
  257. }
  258. return
  259. }
  260. unmatchedKeys.push(key)
  261. })
  262. if (unmatchedKeys.length) {
  263. console.log(`发现条件实现缺失${unmatchedKeys},请检查condition-object实现`)
  264. this.msgError(`imports中未找到[${unmatchedKeys}]`)
  265. } else if (conflictKeys.length) {
  266. this.msgError(`[${conflictKeys}]发现依赖冲突`)
  267. }
  268. return !unmatchedKeys.length && !conflictKeys.length
  269. },
  270. _buildDependencyNode(key, dependencyCheckLink, fromKey) {
  271. if (dependencyCheckLink.includes(key)) {
  272. console.log(`发现循环依赖,请检查condition-object.${key}.independentKeys:`, key, dependencyCheckLink, fromKey)
  273. return false
  274. } // 循环依赖
  275. dependencyCheckLink.push(key)
  276. if (!this._modelKeys.includes(key)) {
  277. console.log(`发现依赖缺省,请检查model.${key}是否存在:`, key, dependencyCheckLink, fromKey)
  278. return false
  279. }
  280. let selfChildren = this._dependencyTree[key] || []
  281. if (fromKey && !selfChildren.includes(fromKey)) selfChildren.push(fromKey)
  282. this._dependencyTree[key] = selfChildren
  283. const matchCondition = this.imports.find(c => c.key == key)
  284. if (!matchCondition) {
  285. console.log('发现依赖中断,请检查condition-object:', key, dependencyCheckLink, fromKey)
  286. return false
  287. } // 依赖中断
  288. let ancestorChecked = true
  289. matchCondition.dependentKeys.forEach(ancestorKey => {
  290. const result = this._buildDependencyNode(ancestorKey, dependencyCheckLink, key)
  291. ancestorChecked = ancestorChecked && result
  292. })
  293. return ancestorChecked
  294. },
  295. async _rebuildAllConditions() {
  296. // 从依赖树的根结点开始创建
  297. this.conditionsOutputTemporary = {}
  298. const rootKeys = Object.keys(this._dependencyTree)
  299. for (const key of rootKeys) {
  300. await this._rebuildConditionFrom(key, true)
  301. }
  302. },
  303. async _rebuildConditionFrom(key, includesSelf) {
  304. // 从某个属性开始构建条件
  305. if (includesSelf) {
  306. const condition = this.imports.find(c => c.key == key)
  307. const withoutAll = this.isRequired(key) || condition.disableAllByForce
  308. const hasData = this.conditionsOutputTemporary[key]?.list.length > (withoutAll ? 0 : 1)
  309. if (hasData) return console.log('_rebuildConditionFrom ignored', key)
  310. console.log('_rebuildConditionFrom previous', key)
  311. await this._loadConditionData(condition)
  312. console.log('_rebuildConditionFrom suffix', key)
  313. }
  314. const dependencyKeys = this._dependencyTree[key]
  315. for (const dependencyKey of dependencyKeys) {
  316. await this._rebuildConditionFrom(dependencyKey, true)
  317. }
  318. },
  319. async _loadConditionData(condition) {
  320. const { cacheKey, param } = this._getDependentParamAndCacheKey(condition)
  321. if (!condition.isDependencyReady(param)) return this._handleConditionData(condition, [], cacheKey)
  322. let list = this.dataCache[cacheKey]
  323. if (!list) list = await condition.getList(param, this)
  324. this._handleConditionData(condition, list, cacheKey)
  325. },
  326. _handleConditionData(condition, list, cacheKey) {
  327. console.log('_handleConditionData', condition, list, cacheKey)
  328. // 只缓存有数据的情况
  329. if (this.useCache && list?.length && !condition.neverCache) this.dataCache[cacheKey] = list
  330. // 处理显示对象
  331. /* TODO: 现在还不确定一个条件同时依赖多个条件的情况是否能正常工作,
  332. 理论上一个依赖多个可以强制转换成单依赖,因为交互时用户也无法同时点两个条件 */
  333. let displayList = list.map(item => ({
  334. code: condition.getCode(item),
  335. label: condition.getLabel(item),
  336. _raw: item // 原数据引用提供一份
  337. }))
  338. // 追加'所有'
  339. const isConditionRequired = this.isRequired(condition.key)
  340. if (!isConditionRequired && !condition.disableAllByForce) {
  341. displayList.unshift({ code: condition.allCode, label: condition.allLabel })
  342. }
  343. // 校验当前值还是否有效
  344. const currentVal = this.model[condition.key]
  345. let displayValue = condition.transferToEffectiveDisplayValue(currentVal, displayList)
  346. if (!condition.codeEquals(currentVal, displayValue)) {
  347. // 初始化过程和还原过程不改值
  348. if (isConditionRequired) {
  349. if (this.initChangedNotified || this.fillRequiresWhenInit) {
  350. if (!this._restoreProcessing) {
  351. this.model[condition.key] = displayValue
  352. }
  353. }
  354. } else {
  355. this.model[condition.key] = displayValue
  356. }
  357. }
  358. // 包装并保存
  359. const wrappedList = {
  360. key: condition.key,
  361. value: this.model[condition.key],
  362. title: condition.title,
  363. list: displayList,
  364. template: condition.template
  365. }
  366. this.conditionsOutputTemporary[condition.key] = wrappedList
  367. },
  368. _getDependentParamAndCacheKey(condition) {
  369. let cacheKey = '',
  370. param = {}
  371. condition.dependentKeys.forEach(key => {
  372. const currentVal = this.model[key]
  373. cacheKey += `${key}_${currentVal}$`
  374. param[key] = currentVal
  375. })
  376. cacheKey += condition.key
  377. if (condition.modelAsParam) {
  378. param = this.model
  379. }
  380. return {
  381. cacheKey,
  382. param
  383. }
  384. },
  385. _mergeConditionsWithTemporary() {
  386. const finalConditions = []
  387. for (const key of this._modelKeys) {
  388. const list = this.conditionsOutputTemporary[key] ||
  389. this.conditionsOutput.find(c => c.key == key)
  390. if (!list) {
  391. if (this.model[key]) this.model[key] = '' // clear invalid value
  392. } else {
  393. const condition = this.imports.find(c => c.key == key)
  394. const withoutAll = this.isRequired(key) || condition.disableAllByForce
  395. const hasData = list.list.length > (withoutAll ? 0 : 1)
  396. if (hasData || this.isRequired(key)) {
  397. finalConditions.push(list)
  398. }
  399. }
  400. }
  401. this.conditionsOutput = finalConditions
  402. },
  403. _triggerEventIfNeed() {
  404. // 整理最终的conditionOutput
  405. this._mergeConditionsWithTemporary()
  406. // 校验&外抛事件
  407. this._restoreProcessing = false
  408. this.valid().then(() => {
  409. if (this.initChangedNotified) {
  410. this._triggerChangedEvent(this.model)
  411. } else {
  412. this._triggerInitChangedEvent(this.model)
  413. this.initChangedNotified = true
  414. }
  415. }).catch(errors => {
  416. if (this.initChangedNotified) {
  417. this._triggerInvalidChangedEvent(errors, this.model)
  418. } else {
  419. this._triggerInitInvalidChangedEvent(errors, this.model)
  420. this.initChangedNotified = true
  421. }
  422. })
  423. },
  424. _triggerSingleChangedEvent(model, key) {
  425. console.log('_triggerSingleChangedEvent', model, key, this._initKeys)
  426. if (this.isCombineMode()) {
  427. this.$emit('singleChanged', model, key)
  428. } else {
  429. this.singleChangedAction(model, key)
  430. }
  431. },
  432. _triggerChangedEvent(model) {
  433. console.log('_triggerChangedEvent validChanged', model)
  434. if (this.isCombineMode()) {
  435. this.$emit('changed', model)
  436. } else {
  437. this.changedAction(model)
  438. }
  439. },
  440. _triggerInvalidChangedEvent(errors, model) {
  441. console.log('_triggerInvalidChangedEvent invalidChanged', model)
  442. if (this.isCombineMode()) {
  443. this.$emit('invalidChanged', errors, model)
  444. } else {
  445. this.invalidChangedAction(model)
  446. }
  447. },
  448. _triggerInitChangedEvent(model) {
  449. console.log('_triggerInitChangedEvent initValidChanged', model)
  450. if (this.isCombineMode()) {
  451. this.$emit('initChanged', model)
  452. } else {
  453. this.initChangedAction(model)
  454. }
  455. },
  456. _triggerInitInvalidChangedEvent(errors, model) {
  457. console.log('_triggerInitInvalidEvent initInvalidChanged', model)
  458. if (this.isCombineMode()) {
  459. this.$emit('initInvalidChanged', errors, model)
  460. } else {
  461. this.initInvalidChangedAction(errors, model)
  462. }
  463. },
  464. _validAndHandleErrors() {
  465. const formRef = this.$refs[this.formRef]
  466. if (!formRef || !formRef.validate) {
  467. console.log('警告:您未正确配置表单引用,组件校验工作中断。参见`this.formRef`')
  468. return Promise.resolve()
  469. }
  470. // return formRef.validate() 这种方式没有外抛异常明细
  471. return new Promise((resolve, reject) => {
  472. const localFields = Object.keys(this.finalRules)
  473. const formFields = formRef.fields
  474. const shouldDelay = localFields.length > 0 && formFields.length == 0
  475. const invoker = function(){
  476. formRef.validate((valid, errors) => {
  477. //debugger
  478. if (valid) resolve(valid)
  479. else reject(errors) // 经这一步转化,可以外抛errors
  480. })
  481. }
  482. // NOTE: 5.14 hht 这里因为form组件的校验要在子组件加载好之后才能生效
  483. if (shouldDelay) this.$nextTick(()=> invoker())
  484. else invoker()
  485. })
  486. }
  487. }
  488. }