

中高端软件定制开发服务商

13245491521 13245491521
我很好奇客户会用得懂这个组件吗? 点击关注公众号,“技术干货”及时达! 前言我们是公司是搞内网安全的,会收集日常电脑操作行为数据。基于前期设置的预警策略,对收集到的数据进行汇总处理分析形成报表。 因此经常要对数据进行分析,会涉及到对多数据字段进行过滤处理。前期界面上基本都是多个单一字段做过滤处理,并没有考虑多字段做「关系运算」后在做过滤处理。 由此产品经理设计了下面的组件,我也不知道这个组件叫啥名??,暂且称它为 「条件过滤树」 由于涉及到产品设计,原型图不方便贴出,但该组件是参考 神策数据中的某个组件,大家可以去那里看看。 一开始拿到手的时候,想着看看有没有写好的轮子,有找到但技术栈不适合,没办法只能硬写了??。 公司用的技术栈是 vue2,写完感觉就是在堆屎??,开发时间不够,一天硬搓出来,很多细节也没有细究,也是一个小 demo。 效果图: 后面比较闲,想着这个组件还是值去研究,去写好的。所以决定重新用 vue3(script setup)去重新封装(平时私下写 vue3 居多)。 顺便总结一下遇到问题,如有不对的地方或有更好的解决方案也请掘友们指出??。 vue3 实现的效果图: 主要功能「条件过滤树」 目前实现的功能点如下: 实现 「增加节点」、「删除节点」、「编辑节点」实现 「表单校验」实现 「自定义节点内容」设计思路3.1、数据结构一开始在公司自己设计的数据结构是这样的: const form = { relation: 1, // 关系 cond_child: [ { relation: 2, cond_child: [ { field: 'fileType', // 操作字段 oper: 1, // 操作符 value: 1 // 值 }, { field: 'fileType', // 操作字段 oper: 1, // 操作符 value: 1 // 值 } ] }, { field: 'operType', // 操作字段 oper: 2, // 操作符 value: 2 // 值 } ] }出现了几个问题??: 字段值 value: 只能定义一个值,如果出现多个值该如何处理没有字段标记该字段 field 是支持什么形式的输入(input、select)前面俩个问题还好,主要是下面这个问题: 没有字段可以判断该条件是否为 「叶子节点」,只能通过判断 cond_child 是否为空。这就引发了下面的问题如果当前 form 表单的数据如下: const form = { field: 'operType', // 操作字段 oper: 2, // 操作符 value: 2 // 值}在此条件下添加一个「且」的条件,数据结构则变成了: const form = { relation: 1, cond_children: [ { field: 'operType', // 操作字段 oper: 2, // 操作符 value: 2 // 值 }, { field: 'fileType', // 操作字段 oper: 2, // 操作符 value: 2 // 值 } ]}这一切看上去也没什么问题,监听addRule 事件,修改 this.form。但界面没有重新渲染,this.form 数据是正确的。 大概代码如下: const ruleItem = { field: this.form.field, ....} // 添加 cond_child 属性this.form.cond_child = [ruleItem, { /* 新的规则 */ }] delete this.form.field; // 删除没有的属性「原因」:vue2 监听的对象是 this.form 的引用,而我直接操作 this.form 修改,添加 cond_children 属性,导致监听不到。 「解决方法」:使用 vue.$set() 和 vue.$delete() 。但觉得复杂了,我改掉 this.form引用不就解决了。 后面改成了如下代码: // 拷贝一份数据const data = cloneDeep(this.form);// 对 data 进行修改....// 重新赋值给 formthis.form = data问题解决??。 由于出现了上面的问题, vue3 重新封装,重新定义一下数据结构。 ?? 具体结构如下: const formData = ref({ is_leaf: false, // 是否为叶子节点 relation: 1, // 关系 children: [ { is_leaf: false, relation: 0, children: [ { is_leaf: false, relation: 1, children: [ { is_leaf: true, data: { filter_type: 'userBehavior', // 字段 oper_type: 1, content: [''], // 内容 } }, { is_leaf: true, // 叶子节点 data: { filter_type: 'fileType', // 字段 oper_type: 1, content: [''], // 内容 } } ] }, { is_leaf: true, data: { filter_type: 'diskType', // 字段 oper_type: 1, content: [''], // 内容 } } ] } ]})3.2、组件设计?目前设计是:RuleTree (树)、RuleItem(结点)、index.vue(入口) ?? 「vue3 的调用使用方法」: template RuleTree :rules="rules" v-model:form="data" :dataMap="dataMap" //templatescript setupimport { reactive, ref } from 'vue'import RuleTree from '@/components/RuleTree/index'const rules = ref({}) // 表单规则const data = ref({}) // 规则数据const dataMap = reactive({ operList: [], // 操作符 dataList: [] // 字段字典})/script3.3、表单校验一开始使用 vue2 为了快??,直接往 「data」 里面注入了错误信息,通过 el-form-item 的 error 进行显示 !-- ruleItem.vue --el-form-item :error="rule.error" :prop="rule.filter_type"!-- 内容的显示 --/el-form-item但后面发现了几个问题??: 必须手动去触发校验,没法通过 trigger 的形式使内部表单元素自动触发,这就使得跟其他表单组件有点出入污染数据,需要在 data 注入 error 变量(用于标记校验信息)到了 vue3 这里,就没有使用上面的那种方式,而是配合 el-form、el-form-item 进行校验。 这里最大的问题:该组件是属于「递归组件」,如何通知到最底层的组件进行表单校验也是试了好多方法,一开始走了好多弯路。 后面接着细说。。。。 3.4、对节点的增加和删除这里最大的问题:除了上面提到的事件的传递外,还有如何确定节点的位置。 后面接着细说。。。。 开发过程4.1、数据驱动页面显示该部分主要解决的是能够根据数据在页面上能够展现出来。 ?? 「代码实现」 // RuleTree.vue template div class="rule-tree" template v-if="formData && !formData.is_leaf && formData.next.length != 0" div class="rule-item-container" !-- 修改关系 -- div class="relation" v-if="formData.next.length = 2" el-button type="text" size="small"/el-button el-button type="text" size="small"/el-button /div !-- 非叶子节点,往下接着递归 -- RuleTree v-for="(cond, i) in formData.next" :formData="cond" :key="i" / /div /template template v-else !-- 叶子节点 -- RuleItem :config="formData.data"/ /template /div/templatescript setupimport { defineProps, inject } from 'vue'import RuleItem from './RuleItem'const props = defineProps({ formData: { type: Object, default: () = ({}) }})/script// RuleItem.vuetemplate div{{ JSON.stringify(config) }}/div/templatescript setupimport { defineProps } from 'vue'const props = defineProps({ config: { type: Object, default: () = ({}) }})/script这部分没有遇到什么问题,顺利通过??... 4.2、如何确定 「ruleNode」 在 「ruleTree」 的位置目前该组件设计之初并没有考虑到可以套多层节点,仅设计了套俩层。(后续完善成通过配置进行处理) 这里通过三个参数来确定 「runleNode」 在 「ruleTree」 的位置: curDepth: 当前层级index: 下标(当前层级下的 next 中数据 config 所在的位置)branch: 分支(只有 0, 1)仅设置了俩层?? 「代码实现」: // RuleTree.vue template div class="rule-tree" template v-if="formData && !formData.is_leaf && formData.next.length != 0" div class="rule-item-container" !-- 修改关系 -- div class="relation" v-if="formData.next.length = 2" el-button type="text" size="small"/el-button el-button type="text" size="small"/el-button /div !-- 非叶子节点,往下接着递归 -- RuleTree v-for="(cond, i) in formData.next" :formData="cond" :key="i" :index="i" :curDepth="curDepth + 1" :branch="curDepth === 0 ? i : branch"/ /div /template template v-else !-- 叶子节点 -- RuleItem :config="formData.data" :index="index" :showAddBtn="showAddBtn" :branch="branch" :curDepth="curDepth"/ /template /div/templatescript setupimport { defineProps, inject } from 'vue'import RuleItem from './RuleItem'const props = defineProps({ formData: { type: Object, default: () = ({}) }, curDepth: { // 层级 type: Number, default: 0 }, branch: { // 分支 type: Number, default: 0 }, index: { // 下标 type: Number, default: 0 },})/script// RuleItem.vuetemplate div{{ JSON.stringify(config) }}/div/templatescript setupimport { defineProps } from 'vue'const props = defineProps({ config: { type: Object, default: () = ({}) }, index: { type: Number, default: 0 }, branch: { type: Number, default: 0 }, curDepth: { type: Number, default: 0 }})/script 4.3、如何进行增删上面已经确定好了三个参数,这里需要在 「ruleItem」 层级将事件进行抛出。现在问题是要抛到那一层,一开始是抛到了 ruleTree 层。 后面发现在 ruleTree 修改数据结构时(增加、删除节点),由于 上层 index(组件的入口文件)并没有进行双向数据绑定,导致数据没有同步到该层级。 template RuleTree :formData="form" / el-button size="small" @click="addRuleByTree" icon="Plus"/el-button/template因此,就设计成了将事件抛到组件入口 index 层级。ruleTree 和 ruleItem 仅做数据层面的渲染以及事件的抛出。 这又有一个问题,由于上面事件抛出又多了加了一层: 原本:孙(ruleItem) 抛给 子(ruleTree) 即可 现在:孙(ruleItem) 经过 子(ruleTree),在抛给父(index) vue2:使用的是事件总线的方式在 「ruleItem」 直接将参数(curDeth、index、depth)抛给了index ?之所以采用事件总线,一开始是使用 emit 一层层往外抛事件的,但在开发过程中发现,对于「递归组件」偶尔事件在 「index 层级」没有接收到,后面因为时间问题,也没有去细究就使用了事件总线的方式。 缺点:「组件挂载后需要监听事件,销毁后需要注销事件」 ?vue3:这里则使用的是依赖注入( provide, inject )的方式,在 「ruleItem」 通过 emits 事件抛给「ruleTree」,拿到参数后,在调用 inject 方法触发事件。 ?事件抛出也可以绕过 「RuleTree」,直接抛到 「index」。 这里只不过说修改 relation 是在 RuleTree 抛给了 index。 为了统一所以增加删除节点才通过 「RuleTree」 在抛出。 ?「代码实现」 1、?? 添加节点 // 添加节点const addRule = (params) = { const { depth, index, branch } = params let data = cloneDeep(props.form) const isAddLayer = depth === 0 let customRule = ruleNode.value if (typeof props.addRules === 'function') { const res = props.addRules() if (res) customRule = res } const addRuleNode = (obj, depth, i, branch) = { if (depth === 0) { // 到达对应层级 if (isAddLayer) { const clickRule = cloneDeep(obj.next[i].data) obj.next[i].relation = 1 obj.next[i].is_leaf = false obj.next[i].next = [{ is_leaf: true, data: { ...clickRule } }, customRule] } else { i++ obj.next.splice(i, 0, customRule) } return } if (Array.isArray(obj.next)) { if (addRuleNode(obj.next[branch], depth - 1, i)) { return true } } } addRuleNode(data, depth, index, branch) emits('update:form', data)}2、?? 删除节点 // 删除节点const delRule = (params) = { const { depth, index, branch } = params let data = cloneDeep(props.form) const delRuleNode = (obj, depth, i, branch) = { // 如果已经到达目标层级 if (depth === 0) { if (Array.isArray(obj.next) && obj.next.length i) { // 删除指定下标的对象 obj.next.splice(i, 1) if (obj.next.length === 1 && obj.next[0].is_leaf) { // 变成叶子结点 obj.is_leaf = true obj.data = obj.next[0].data obj.next = [] } return true // 删除成功 } return false // 删除失败 } // 如果还没到达目标层级,继续递归 if (Array.isArray(obj.next)) { if (delRuleNode(obj.next[branch], depth - 1, i)) { return true } } } data.is_leaf ? data = {} : delRuleNode(data, depth, index, branch) data = formatForm(data) // 调整数据结构,后面【注意】会细讲 emits('update:form', data)}「注意」 删除节点后可能会出现下面这种情况: 原本: 删除节点后: 删除后数据结构: { "is_leaf": false, "data": {}, "next": [ { "is_leaf": false, "data": { "filter_type": "operType", "opr_type": 1, "content": [] }, "relation": 1, "next": [ { "is_leaf": true, "data": { "filter_type": "operType", "opr_type": 1, "content": [] } }, { "is_leaf": true, "data": { "filter_type": "operType", "opr_type": 1, "content": [] } } ] } ], "relation": 1}查看数据结构,组件的渲染是没有问题,这部分在渲染且或操作符时,需要判断当前的 next 长度是否为大于 1 template v-if="formData && !formData.is_leaf && formData.next.length != 0" div class="rule-item-container" div class="relation" v-if="formData.next.length = 2" el-button type="text" size="small" :class="+formData.relation === 1 ? 'active' : ''" @click="changeRelation(1)"/el-button el-button type="text" size="small" :class="+formData.relation === 2 ? 'active' : ''" @click="changeRelation(2)"/el-button /div RuleTree v-for="(cond, i) in formData.next" :formData="cond" :key="i" :index="i" :curDepth="curDepth + 1" :branch="curDepth === 0 ? i : branch" :showAddBtn="showAddBtn" template #default="slotProps" slot v-bind="slotProps" / /template /RuleTree /div/template界面显示是小问题,问题是该数据结构是要发送给到后端的,relation 标记了同级目录下的 next中所有条件的关系,但 next 只有一个。 这里觉得不是很好,因此这里删除节点后,又对数据结构的层级做了格式化的操作 const formatForm = (data) = { if (!data.is_leaf) { if (data.next && data.next.length === 1) { data = data.next[0] return data } return data } return data}因为这里的组件设计只能套俩层,故只要处理上面这种情况即可。 3、?? provide inject 事件的注入 // index.vuetemplate div class="tree" RuleTree :formData="form"/RuleTree el-button size="small" @click="addRuleByTree" icon="Plus"/el-button /div/templatescript setupimport { ref, defineProps, defineEmits, defineExpose, provide, watch, computed } from 'vue'import RuleTree from './RuleTree'import { cloneDeep } from 'lodash'const emits = defineEmits(['update:form'])const props = defineProps({ form: { type: Object, default: () = ({}) }, rules: { type: Object, default: () = ({}) }, addRules: { type: Function, default: () = { } }, depth: { type: Number, default: 2 }}) // 添加节点const addRule = (params) = {} // 删除节点const delRule = (params) = {} // 修改关系 (跟只有俩层,比较简单,不做更多描述)const changeRelation = (params) = { const { value, depth, branch } = params const data = cloneDeep(props.form) if (depth === 0) { data.relation = value } else { data.next[branch].relation = value } emits('update:form', data)} // 调整数据结构const formatForm = (data) = { if (!data.is_leaf) { if (data.next && data.next.length === 1) { data = data.next[0] return data } return data } return data} // 注入provide('addRule', addRule)provide('delRule', delRule)provide('changeRelation', changeRelation) /script // ruleTree.vuetemplate div class="rule-tree" :style="curDepth 0 ? { paddingLeft: '60px' } : {}" template v-if="formData && !formData.is_leaf && formData.next.length != 0" div class="rule-item-container" div class="relation" v-if="formData.next.length = 2" el-button type="text" size="small" :class="+formData.relation === 1 ? 'active' : ''" @click="changeRelation(1)"/el-button el-button type="text" size="small" :class="+formData.relation === 2 ? 'active' : ''" @click="changeRelation(2)"/el-button /div RuleTree v-for="(cond, i) in formData.next" :formData="cond" :key="i" :index="i" :curDepth="curDepth + 1" :branch="curDepth === 0 ? i : branch" :showAddBtn="showAddBtn" /RuleTree /div /template template v-else RuleItem :config="formData.data" :index="index" :showAddBtn="showAddBtn" :branch="branch" :curDepth="curDepth" @addRule="addRule" @delRule="delRule" /RuleItem /template /div/templatescript setupimport { defineProps, inject } from 'vue'import RuleItem from './RuleItem'const changeRelationFuncInject = inject('changeRelation')const addRuleFuncInject = inject('addRule')const delRuleFuncInject = inject('delRule')const props = defineProps({ formData: { type: Object, default: () = ({}) }, curDepth: { // 层级 type: Number, default: 0 }, branch: { // 分支 type: Number, default: 0 }, index: { // 下标 type: Number, default: 0 }, showAddBtn: { type: Boolean, default: false }}) const changeRelation = (value) = { changeRelationFuncInject({ depth: props.curDepth, branch: props.branch, value })} const addRule = () = { addRuleFuncInject({ branch: props.branch, depth: props.curDepth - 1, index: props.index })} const delRule = () = { delRuleFuncInject({ branch: props.branch, depth: props.curDepth - 1, index: props.index })}/script// ruleItem.vuetemplate div class="rule-item" div class="rule-node" div{{ JSON.stringify(config) }}/div /div div class="oper-btn" el-button size="small" v-if="showAddBtn" @click="addRule"/el-button el-button size="small" @click="delRule"/el-button /div /div/templatescript setupimport { defineProps, defineEmits, watch, ref, inject, onMounted, computed } from 'vue'const emits = defineEmits(['addRule', 'delRule'])const props = defineProps({ config: { type: Object, default: () = ({}) }, index: { type: Number, default: 0 }, showAddBtn: { type: Boolean, default: false }, branch: { type: Number, default: 0 }, curDepth: { type: Number, default: 0 }}) const addRule = () = emits('addRule')const delRule = () = emits('delRule')/script4.4、如何自定义内容该部分使用的是「插槽」的形式进行实现的,这里考虑到需要 增加删除节点,如果由上层去定位节点位置可能会比较麻烦。 上层通过 id 去定位可能就不会很复杂故:内部使用插槽时除了将数据抛出外,还好会将增加节点、删除节点事件方法抛出,由上层自行决定是否使用。 ?? 「代码实现」 // RuleTree.vuetemplate div class="rule-tree" :style="curDepth 0 ? { paddingLeft: '60px' } : {}" template v-if="formData && !formData.is_leaf && formData.next.length != 0" div class="rule-item-container" div class="relation" v-if="formData.next.length = 2" el-button type="text" size="small" :class="+formData.relation === 1 ? 'active' : ''" @click="changeRelation(1)"/el-button el-button type="text" size="small" :class="+formData.relation === 2 ? 'active' : ''" @click="changeRelation(2)"/el-button /div RuleTree v-for="(cond, i) in formData.next" :formData="cond" :key="i" :index="i" :curDepth="curDepth + 1" :branch="curDepth === 0 ? i : branch" :showAddBtn="showAddBtn" !-- 插槽 -- template #default="slotProps" slot v-bind="slotProps" / /template /RuleTree /div /template template v-else RuleItem :config="formData.data" :index="index" :showAddBtn="showAddBtn" :branch="branch" :curDepth="curDepth" @addRule="addRule" @delRule="delRule" !-- 插槽 -- template #default="slotProps" slot v-bind="slotProps" / /template /RuleItem /template /div/templatescript setupimport { defineProps } from 'vue'import RuleItem from './RuleItem'const props = defineProps({ formData: { type: Object, default: () = ({}) }, curDepth: { // 层级 type: Number, default: 0 }, branch: { // 分支 type: Number, default: 0 }, index: { // 下标 type: Number, default: 0 }, showAddBtn: { type: Boolean, default: false }})/script// ruleItem.vuetemplate slot :data="config" :addRule="addRule" :delRule="delRule" div class="rule-item" div class="rule-node" div{{ JSON.stringify(config) }}/div /div div class="oper-btn" el-button size="small" v-if="showAddBtn" @click="addRule"/el-button el-button size="small" @click="delRule"/el-button /div /div /slot/templatescript setupimport { defineProps, defineEmits, watch, ref, inject, onMounted, computed } from 'vue'const emits = defineEmits(['addRule', 'delRule'])const props = defineProps({ config: { type: Object, default: () = ({}) }, index: { type: Number, default: 0 }, showAddBtn: { type: Boolean, default: false }, branch: { type: Number, default: 0 }, curDepth: { type: Number, default: 0 }}) const addRule = () = emits('addRule')const delRule = () = emits('delRule')/script4.5、如何进行表单校验由于是配合el-form 和 el-form-item进行使用的,这里的第一个问题是: 这个两个组件是要套在哪个位置上最为合适,一开始我是套在 RuleTree// RuleTreetemplate div class="rule-tree" :style="curDepth 0 ? { paddingLeft: '60px' } : {}" template v-if="formData && !formData.is_leaf && formData.next.length != 0" div class="rule-item-container" el-form el-form-item RuleTree v-for="(cond, i) in formData.next" :formData="cond" :key="i"/ /el-form-item /el-form /div /template template v-else RuleItem :config="formData.data" / /template /div/template之所以放到这里是想着一个「子树」 对应一个表单,但这里就发现了一个问题,又出现了「递归现象」(el-form-item 套 el-form)一直循环套下去,如果后续层级修改成可配置则递归的层级会更深。 而且如果出现「递归现象」,上层要通知底层组件进行校验也很麻烦(el-form校验的方式是通过 ref) 经过考虑后:将 el-form 和 el-form-item 放到 「RuleItem」。 放到这里的好处: 收集表单ref 比较好收集表单需要绑定数据,在这一层拿到的这个节点的数据比较简单// RuleItem.vuetemplate el-form size="small" ref="formRef" :model="config" el-form-item prop="content" :rules="rules[config.filter_type]" div class="rule-item" div class="rule-node" div{{ JSON.stringify(config) }}/div /div div class="oper-btn" el-button size="small" v-if="showAddBtn" @click="addRule"/el-button el-button size="small" @click="delRule"/el-button /div /div /el-form-item /el-form /template到这里,就能实现通过配置 trigger 实现表单组件自动触发校验。 接下来:上层需要通过 ref 去触发校验,代码如下: // 调用方template RuleTree ref="ruleTreeRef" :rules="rules" v-model:form="formData" :dataMap="dataMap" / el-button type="primary" @click="validate"/el-button/templatescript setupimport { reactive, ref } from 'vue'import RuleTree from '@/components/RuleTree/index'import { ElMessage } from 'element-plus' const isNotEmpty = (rule, value, callback) = { if (value.length 0) return callback() return callback(new Error('不能为空'))} const dataMap = reactive({}) // 不重要 const formData = ref({})// 表单规则(el-form 一致)const rules = ref({ userBehavior: [ { validator: isNotEmpty, trigger: 'change' } ], fileSize: [ { validator: isNotEmpty, trigger: 'blur' } ], operType: [ { validator: (rule, value, callback) = { if (value.length = 1) callback(new Error('长度需要大于 1')) return callback() }, trigger: 'blur' } ]}) // 校验const validate = async () = { ruleTreeRef.value.validate((valid) = { if (valid) return ElMessage.success('校验通过') return ElMessage.error('校验失败') })}/script这里很明显在 「组件入口文件 index」 中需要提供 validate 方法供上层触发校验。 // index.vuescript setupimport { defineExpose } from 'vue'const validate = (callback) = { // TODO 这里需要通知 RuleItem 触发校验,并把结果返回 // TODO 汇总所有结果,将最终的校验结果给到上层} defineExpose({ validate })/script一开始想怎么通知 ruleItem 触发校验也是想了很久,这里是有组件递归的现象。 后面想到可以通过 provide、inject先收集到所有 el-form对象保存到数组中,手动触发校验时遍历该数组进行校验即可。 ?? 「代码实现」 // index.vuescript setupimport { defineExpose, provide, ref } from 'vue' const ruleNodeList = ref([])const collectRuleNode = (ruleNode) = { ruleNodeList.value.push(ruleNode)} const validate = (callback) = { return new Promise((resolve) = { Promise.all(ruleNodeList.value.filter((item) = item.value).map(ruleNode = ruleNode.value.validate())).then(res = { typeof callback === 'function' ? callback(true) : resolve(true) }).catch(() = { typeof callback === 'function' ? callback(false) : resolve(false) }) })} provide('collectRuleNode', collectRuleNode)defineExpose({ validate })/scripttemplate el-form ref="elFormRef" size="small" ref="formRef" :model="config" el-form-item prop="content" :rules="rules[config.filter_type]" div class="rule-item" div class="rule-node" div{{ JSON.stringify(config) }}/div /div div class="oper-btn" el-button size="small" v-if="showAddBtn" @click="addRule"/el-button el-button size="small" @click="delRule"/el-button /div /div /el-form-item /el-form /templatescript setupimport { ref, inject, onMounted } from 'vue'const elFormRef = ref(null)const collectRuleNodeFuncInject = inject('collectRuleNode') onMounted(() = { collectRuleNodeFuncInject(elFormRef)})/script使用说明5.1、基本使用template RuleTree ref="ruleTreeRef" v-model:form="formData" :dataMap="dataMap" //templatescript setupimport { reactive, ref } from 'vue'import RuleTree from '@/components/RuleTree/index' const dataMap = reactive({ operList: [ { label: '大于', value: 1 }, { label: '等于', value: 2 }, { label: '小于', value: 3 }, { label: '介于', value: 4 } ], dataList: [ { label: '文件类型', field: 'fileType', type: 'select', valueList: [ { label: '文件类型-1', value: 1 }, { label: '文件类型-2', value: 2 }, { label: '文件类型-3', value: 3 }, { label: '文件类型-4', value: 4 }, { label: '文件类型-5', value: 5 } ] }, { label: '磁盘类型', field: 'diskType', type: 'select', valueList: [ { label: '磁盘类型-1', value: 1 }, { label: '磁盘类型-2', value: 2 }, { label: '磁盘类型-3', value: 3 }, { label: '磁盘类型-4', value: 4 }, { label: '磁盘类型-5', value: 5 } ] } ]}) const formData = ref({})/script5.2、使用自定义内容template RuleTree ref="ruleTreeRef" v-model:form="formData" template #default="{ data, addRule, delRule }" div 数据:{{ JSON.stringify(data) }} el-button type="primary" @click="addRule"/el-button el-button type="danger" @click="delRule"/el-button /div /template /RuleTree/templatescript setupimport { reactive, ref } from 'vue'import RuleTree from '@/components/RuleTree/index'const formData = ref({})/script5.3、表单校验template RuleTree ref="ruleTreeRef" :rules="rules" v-model:form="formData" :dataMap="dataMap" / el-button type="primary" @click="validate"/el-button/templatescript setupimport { reactive, ref } from 'vue'import RuleTree from '@/components/RuleTree/index'import { ElMessage } from 'element-plus' const isNotEmpty = (rule, value, callback) = { if (value.length 0) return callback() return callback(new Error('不能为空'))} const dataMap = reactive({ operList: [ { label: '大于', value: 1 }, { label: '等于', value: 2 }, { label: '小于', value: 3 }, { label: '介于', value: 4 } ], dataList: [ { label: '文件类型', field: 'fileType', type: 'select', valueList: [ { label: '文件类型-1', value: 1 }, { label: '文件类型-2', value: 2 }, { label: '文件类型-3', value: 3 }, { label: '文件类型-4', value: 4 }, { label: '文件类型-5', value: 5 } ] }, { label: '磁盘类型', field: 'diskType', type: 'select', valueList: [ { label: '磁盘类型-1', value: 1 }, { label: '磁盘类型-2', value: 2 }, { label: '磁盘类型-3', value: 3 }, { label: '磁盘类型-4', value: 4 }, { label: '磁盘类型-5', value: 5 } ] } ]}) const formData = ref({})const rules = ref({ userBehavior: [ { validator: isNotEmpty, trigger: 'change' } ], fileSize: [ { validator: isNotEmpty, trigger: 'blur' } ], operType: [ { validator: (rule, value, callback) = { if (value.length = 1) callback(new Error('长度需要大于 1')) return callback() }, trigger: 'blur' } ]}) const validate = async () = { ruleTreeRef.value.validate((valid) = { if (valid) return ElMessage.success('校验通过') return ElMessage.error('校验失败') })}/script不足及后续开发目前存在的不足: 目前组件层级只能套俩层,后续会调整成可配置的没法实现鼠标拖拽节点(el-tree 有点类似)修改其相对位置(后续看看可行性)针对第二点:这里请教一下掘友有没有好用的拖拽插件可以使用。 组件源码vue2 写得比较简陋也有些问题,这里就不贴出来如果有需要的话,后续我修改后会贴出来。 「注」:想着每个业务场景使用该组件时,除非很通用(对于操作符「介于」,要显示一个还是俩个输入框,每个场景可能有所不同)才会使用到ruleItem设置好的组件进行渲染,不然会使用「自定义内容」的形式进行渲染。故「ruleItem」 渲染处理的比较简单。 7.1、入口文件 index.vuetemplate div class="tree" RuleTree v-if="JSON.stringify(form) !== '{}'" :formData="form" :showAddBtn="showAddBtn" template #default="slotProps" slot v-bind="slotProps" / /template /RuleTree el-button size="small" @click="addRuleByTree" icon="Plus"/el-button /div/templatescript setupimport { ref, defineProps, defineEmits, defineExpose, provide, watch, computed } from 'vue'import RuleTree from './RuleTree'import { cloneDeep } from 'lodash'const emits = defineEmits(['update:form'])const props = defineProps({ form: { type: Object, default: () = ({}) }, rules: { type: Object, default: () = ({}) }, addRules: { type: Function, default: () = { } }, depth: { type: Number, default: 2 }, dataMap: { type: Array, default: () = ([]) }}) const showAddBtn = ref(false)const ruleNodeList = ref([]) const ruleNode = computed(() = { const rule = { is_leaf: true, data: {} } const { dataList = [], operList = [] } = props.dataMap const { valueList = [] } = dataList if (dataList.length 0 && operList.length 0) { rule.data.filter_type = dataList[0].field rule.data.opr_type = operList[0].value rule.data.content = valueList.map(item = item.value) } return rule}) watch(() = props.form, (data) = { showAddBtn.value = data.next && data.next.length 1}, { immediate: true }) // 往树上添加结点const addRuleByTree = () = { let data = cloneDeep(props.form) const isInit = JSON.stringify(data) === '{}' let rule = ruleNode.value if (typeof props.addRules === 'function') { const res = props.addRules() if (res) rule = res } if (isInit) { // 未初始化 const form = {} form.is_leaf = true form.data = rule.data form.next = [] data = form } else { const len = data.next.length data.is_leaf = false if (len === 0) { data.relation = 1 data.next = [{ is_leaf: true, data: data.data }, rule] data.data = {} } else { data.next.push(rule) } } emits('update:form', data)} // 添加节点const addRule = (params) = { const { depth, index, branch } = params let data = cloneDeep(props.form) const isAddLayer = depth === 0 let customRule = ruleNode.value if (typeof props.addRules === 'function') { const res = props.addRules() if (res) customRule = res } const addRuleNode = (obj, depth, i, branch) = { if (depth === 0) { // 到达对应层级 if (isAddLayer) { const clickRule = cloneDeep(obj.next[i].data) obj.next[i].relation = 1 obj.next[i].is_leaf = false obj.next[i].next = [{ is_leaf: true, data: { ...clickRule } }, customRule] } else { i++ obj.next.splice(i, 0, customRule) } return } if (Array.isArray(obj.next)) { if (addRuleNode(obj.next[branch], depth - 1, i)) { return true } } } addRuleNode(data, depth, index, branch) data = formatForm(data) emits('update:form', data)} // 删除节点const delRule = (params) = { const { depth, index, branch } = params let data = cloneDeep(props.form) const delRuleNode = (obj, depth, i, branch) = { // 如果已经到达目标层级 if (depth === 0) { if (Array.isArray(obj.next) && obj.next.length i) { // 删除指定下标的对象 obj.next.splice(i, 1) if (obj.next.length === 1 && obj.next[0].is_leaf) { // 变成叶子结点 obj.is_leaf = true obj.data = obj.next[0].data obj.next = [] } return true // 删除成功 } return false // 删除失败 } // 如果还没到达目标层级,继续递归 if (Array.isArray(obj.next)) { if (delRuleNode(obj.next[branch], depth - 1, i)) { return true } } } data.is_leaf ? data = {} : delRuleNode(data, depth, index, branch) data = formatForm(data) emits('update:form', data)} // 修改关系const changeRelation = (params) = { const { value, depth, branch } = params const data = cloneDeep(props.form) if (depth === 0) { data.relation = value } else { data.next[branch].relation = value } emits('update:form', data)} // 调整数据结构const formatForm = (data) = { if (!data.is_leaf) { if (data.next && data.next.length === 1) { data = data.next[0] return data } return data } return data} const collectRuleNode = (ruleNode) = { ruleNodeList.value.push(ruleNode)} const validate = (callback) = { return new Promise((resolve) = { Promise.all(ruleNodeList.value.filter((item) = item.value).map(ruleNode = ruleNode.value.validate())).then(res = { typeof callback === 'function' ? callback(true) : resolve(true) }).catch(() = { typeof callback === 'function' ? callback(false) : resolve(false) }) })}provide('addRule', addRule)provide('delRule', delRule)provide('changeRelation', changeRelation)provide('collectRuleNode', collectRuleNode)provide('rules', props.rules)provide('dataMap', props.dataMap)provide('addRuleByTree', addRuleByTree) defineExpose({ validate})/script7.2、RuleTree.vuetemplate div class="rule-tree" :style="curDepth 0 ? { paddingLeft: '60px' } : {}" template v-if="formData && !formData.is_leaf && formData.next.length != 0" div class="rule-item-container" div class="relation" v-if="formData.next.length = 2" el-button type="text" size="small" :class="+formData.relation === 1 ? 'active' : ''" @click="changeRelation(1)"/el-button el-button type="text" size="small" :class="+formData.relation === 2 ? 'active' : ''" @click="changeRelation(2)"/el-button /div RuleTree v-for="(cond, i) in formData.next" :formData="cond" :key="i" :index="i" :curDepth="curDepth + 1" :branch="curDepth === 0 ? i : branch" :showAddBtn="showAddBtn" template #default="slotProps" slot v-bind="slotProps" / /template /RuleTree /div /template template v-else RuleItem :config="formData.data" :index="index" :showAddBtn="showAddBtn" :branch="branch" :curDepth="curDepth" @addRule="addRule" @delRule="delRule" template #default="slotProps" slot v-bind="slotProps" / /template /RuleItem /template /div/templatescript setupimport { defineProps, inject } from 'vue'import RuleItem from './RuleItem'const changeRelationFuncInject = inject('changeRelation')const addRuleFuncInject = inject('addRule')const delRuleFuncInject = inject('delRule')const props = defineProps({ formData: { type: Object, default: () = ({}) }, curDepth: { // 层级 type: Number, default: 0 }, branch: { // 分支 type: Number, default: 0 }, index: { // 下标 type: Number, default: 0 }, showAddBtn: { type: Boolean, default: false }}) const changeRelation = (value) = { changeRelationFuncInject({ depth: props.curDepth, branch: props.branch, value })} const addRule = () = { addRuleFuncInject({ branch: props.branch, depth: props.curDepth - 1, index: props.index })} const delRule = () = { delRuleFuncInject({ branch: props.branch, depth: props.curDepth - 1, index: props.index })}/scriptstyle lang="scss" scoped.rule-tree { margin-bottom: 20px; .rule-item-container { position: relative; .relation { position: absolute; top: 0px; bottom: 0px; left: 12px; display: flex; flex-direction: column; justify-content: center; width: 40px; border-right: 1px solid var(--el-color-primary); .el-button { color: #c0c4cc; &.active { color: var(--el-color-primary); } } } } .el-button { margin: 0px; }}/style7.3、ruleItem.vuetemplate el-form size="small" ref="formRef" v-if="show" :model="config" el-form-item prop="content" :rules="rules[config.filter_type]" slot :data="config" :addRule="addRule" :delRule="delRule" div class="rule-item" div class="rule-node" el-select v-model="config.filter_type" el-option v-for="item in map.labelList" :key="item.field" :value="item.field" :label="item.label" / /el-select el-select v-model="config.opr_type" el-option v-for="item in map.operList" :key="item.value" :value="item.value" :label="item.label" / /el-select el-select class="value-select" v-model="config.content" multiple collapse-tags collapse-tags-tooltip :max-collapse-tags="2" el-option v-for="(item) in map.dataList[config.filter_type].valueList" :key="item.value" :value="item.value" :label="item.label" / /el-select /div div class="oper-btn" el-button size="small" v-if="showAddBtn" @click="addRule"/el-button el-button size="small" @click="delRule"/el-button /div /div /slot /el-form-item /el-form/templatescript setupimport { defineProps, defineEmits, watch, ref, inject, onMounted, computed } from 'vue'const emits = defineEmits(['addRule', 'delRule'])const props = defineProps({ config: { type: Object, default: () = ({}) }, index: { type: Number, default: 0 }, showAddBtn: { type: Boolean, default: false }, branch: { type: Number, default: 0 }, curDepth: { type: Number, default: 0 }})const collectRuleNodeFuncInject = inject('collectRuleNode')const rules = inject('rules')const dataMap = inject('dataMap')const addRuleByTreeFunc = inject('addRuleByTree')const formRef = ref(null)const show = ref(false) const map = computed(() = { const operList = dataMap.operList const labelList = dataMap.dataList.map(item = { return { label: item.label, field: item.field } }) const dataList = dataMap.dataList.reduce((acc, cur) = { acc[cur.field] = cur return acc }, {}) return { operList, labelList, dataList }}) watch(() = props.config, (data) = { show.value = data.filter_type && data.opr_type && data.content}, { immediate: true, deep: 2 }) const addRule = () = { if (!props.showAddBtn) { addRuleByTreeFunc() } else { emits('addRule') }}const delRule = () = emits('delRule') onMounted(() = { collectRuleNodeFuncInject(formRef)})/scriptstyle lang="scss" scoped.rule-item { display: flex; gap: 6px; .rule-node { .el-select { width: 120px; &.value-select { width: 300px; } } display: flex; gap: 6px; }}/style7.4、使用(完整代码)template div el-form size="small" el-form-item label="满足条件" !-- 不使用插槽 -- RuleTree ref="ruleTreeRef" :rules="rules" v-model:form="formData" :dataMap="dataMap" / !-- 使用插槽 -- RuleTree ref="ruleTreeRef" :rules="rules" v-model:form="formData" :dataMap="dataMap" template #default="{ data, addRule, delRule }" div 数据:{{ JSON.stringify(data) }} el-button type="primary" @click="addRule"/el-button el-button type="danger" @click="delRule"/el-button /div /template /RuleTree /el-form-item el-form-item el-button type="primary" @click="validate"/el-button /el-form-item /el-form /div/templatescript setupimport { reactive, ref } from 'vue'import RuleTree from '@/components/RuleTree/index'import { ElMessage } from 'element-plus' const isNotEmpty = (rule, value, callback) = { if (value.length 0) return callback() return callback(new Error('不能为空'))} const dataMap = reactive({ operList: [ { label: '大于', value: 1 }, { label: '等于', value: 2 }, { label: '小于', value: 3 }, { label: '介于', value: 4 } ], dataList: [ { label: '操作类型', field: 'operType', type: 'select', valueList: [ { label: '操作类型-1', value: 1 }, { label: '操作类型-2', value: 2 }, { label: '操作类型-3', value: 3 }, { label: '操作类型-4', value: 4 }, { label: '操作类型-5', value: 5 } ] }, { label: '文件类型', field: 'fileType', type: 'select', valueList: [ { label: '文件类型-1', value: 1 }, { label: '文件类型-2', value: 2 }, { label: '文件类型-3', value: 3 }, { label: '文件类型-4', value: 4 }, { label: '文件类型-5', value: 5 } ] }, { label: '磁盘类型', field: 'diskType', type: 'select', valueList: [ { label: '磁盘类型-1', value: 1 }, { label: '磁盘类型-2', value: 2 }, { label: '磁盘类型-3', value: 3 }, { label: '磁盘类型-4', value: 4 }, { label: '磁盘类型-5', value: 5 } ] }, { label: '用户行为', field: 'userBehavior', type: 'select', valueList: [ { label: '用户行为-1', value: 1 }, { label: '用户行为-2', value: 2 }, { label: '用户行为-3', value: 3 }, { label: '用户行为-4', value: 4 }, { label: '用户行为-5', value: 5 } ] }, { label: '文件大小', field: 'fileSize', type: 'input' } ]}) const ruleTreeRef = ref(null) const rules = ref({ userBehavior: [ { validator: isNotEmpty, trigger: 'change' } ], fileSize: [ { validator: isNotEmpty, trigger: 'blur' } ], operType: [ { validator: (rule, value, callback) = { if (value.length = 1) callback(new Error('长度需要大于 1')) return callback() }, trigger: 'blur' } ]}) const formData = ref({}) const validate = async () = { ruleTreeRef.value.validate((valid) = { if (valid) return ElMessage.success('校验通过') return ElMessage.error('校验失败') })}/scriptstyle lang="scss" scoped/style点击关注公众号,“技术干货”及时达! 阅读原文
| 上一篇:2019-10-17_奥运开幕式竞标秘史,张艺谋凭什么赢了李安和凯歌? | 下一篇:2020-06-12_这么热的天,我就在家游故宫了 |
TAG标签: |
17 |
|
我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!
|
|
不达标就退款 高性价比建站 免费网站代备案 1对1原创设计服务 7×24小时售后支持 |
|
|
