手写一个多选级联选择器,我们首先要创建一个外壳组件,用来处理默认数据增加一些显示隐藏,选中属性和回显功能。<\/h2>我们把外壳组件起名为multipleChoice.vue,这里先放上template代码,已选显示的列表我没做,已选的ID会放在数据selected里,你们可以自行寻找。<\/blockquote> <div v-click-outside=\"close\" class=\"department-option\" @click.stop=\"show = true\">\n <div class=\"now-text\">\n 选择部门\n <\/div>\n <transition name=\"transition-drop\">\n <div class=\"department-box\" v-show=\"show\">\n <MultipleChoiceItem v-model=\"listData\" @child-change=\"childChange\" :selectKey=\"selectKey\" \/>\n <\/div>\n <\/transition>\n <\/div>\n<\/pre>
<\/p>
接下来就是script代码。我们要引入组件 MultipleChoiceItem 组件,这个组件会放在下面。这是用来递归显示的组件。<\/blockquote>注意:一下代码是multipleChoice.vue的script代码。内部实现了对数据的处理,给已选中的定义选中状态,给原始数据增加是否选中字段,显示隐藏字段,给每条数据都赋值一个父级原型链。为的是当前状态修改后,所有父级需要变更状态如(半选,全选,未选)。<\/blockquote>props主要传入列表数据,是否需要自定义key包括返回的id,要显示的name,树结构的子级字段,v-model需要绑定的数据。<\/blockquote><script>\nimport MultipleChoiceItem from \".\/multipleChoiceItem.vue\"\nexport default {\n name: 'multipleChoice',\n data() {\n return {\n show: false,\n listData: [], \/\/ 列表数据\n selected: [], \/\/ 最终选中\n }\n },\n props: {\n data: {\n type: Array,\n default: () => []\n },\n \/\/ 未选提示\n placeholder: {\n type: String,\n default: '请选择'\n },\n \/**\n * 用到的key\n * 默认 id name child\n *\/\n selectKey: {\n type: Object,\n default: () => {\n return { value: 'id', lable: 'name', child: 'child' }\n }\n },\n value: {\n type: Array,\n default: () => []\n }\n },\n watch: {\n show(newVal) {\n if(!newVal) {\n this.updateData() \n }\n }\n },\n created() {\n this.init();\n },\n methods: {\n childChange(val) {\n \/\/ console.log(val)\n },\n openCascader() {\n this.show = true;\n this.init()\n },\n init() {\n if (this.data && this.data.length) {\n let arr = JSON.parse(JSON.stringify(this.data))\n this.washData(arr, null); \/\/ 洗数据\n this.listData = arr;\n }\n },\n washData(treeList, parent, flag) {\n for (let i = 0; i < treeList.length; i++) {\n let flag_n = false; \/\/ 父级有选中子级不显示 标识符\n let jsonTree = JSON.parse(JSON.stringify(treeList[i])); \/\/ 赋值已选\n const tree = treeList[i];\n tree.parent = parent;\n if(this.value.includes(tree[this.selectKey.value])) {\n flag_n = true;\n \/\/ 默认回选\n if (tree.parent) {\n const diffParnet = (child_item) => {\n if(child_item.check != 1) { \/\/ 只有没有选中的父级才能是半选状态\n child_item.check = 2;\n }\n if(child_item.parent) {\n diffParnet(child_item.parent);\n }\n }\n diffParnet(tree.parent)\n }\n tree.check = 1;\n tree.show = true;\n if(!flag) {\n this.selected_item.push(jsonTree); \/\/ 回源默认选中\n }\n } else {\n \/\/ 默认为0\n tree.check = 0;\n tree.show = false;\n }\n if (tree[this.selectKey.child]) {\n this.washData(tree[this.selectKey.child], tree, flag_n);\n }\n }\n },\n \/\/ 弹窗关闭\n updateData() {\n this.selected = [];\n this.findSelected(this.listData);\n this.$emit('input', this.selected);\n this.$emit('on-change', this.selected);\n },\n close() {\n this.show = false;\n },\n \/\/ 查询已被选中的数据\n findSelected(arr) {\n arr.forEach(v => {\n if (v.check == 1) {\n this.selected.push(v.id)\n }\n if (v[this.selectKey.child] && v[this.selectKey.child].length) {\n this.findSelected(v[this.selectKey.child])\n }\n })\n }\n },\n components: {\n MultipleChoiceItem\n }\n}\n<\/script>\n<\/pre>
<\/p>
接下来是multipleChoice.vue的style代码<\/blockquote><style lang=\"less\" scoped>\n.department-option {\n width: 300px;\n height: 32px;\n margin-top: 20px;\n position: relative;\n border: 1px solid #d9d9d9;\n cursor: pointer;\n\n\n .department-box {\n width: 298px;\n max-height: 180px;\n overflow-y: auto;\n }\n\n\n .select-list {\n display: inline-block;\n background-color: #f5f5f5;\n color: rgba(0, 0, 0, .85);\n margin: 3px 0 3px 3px;\n height: 24px;\n line-height: 24px;\n padding: 0 10px;\n\n\n .icon-close {\n margin-left: 6px;\n font-size: 10px;\n color: rgba(0, 0, 0, .45);\n }\n }\n\n\n .now-text {\n overflow: hidden;\n color: #bfbfbf;\n white-space: nowrap;\n text-overflow: ellipsis;\n pointer-events: none;\n padding: 0 6px;\n line-height: 30px;\n }\n\n\n input {\n position: absolute;\n width: 0;\n border: 0;\n left: 6px;\n top: 0;\n height: 30px;\n padding: 6px 6px;\n }\n}\n<\/style>\n<\/pre>
<\/p>
我们写好multipleChoice.vue组件后,要继续创建multipleChoiceItem.vue组件<\/h2>multipleChoiceItem.vue组件是为了递归显示每条数据的,里面包括了多选,上级下级联动的逻辑。<\/blockquote>下方是template代码,重复使用组件multipleChoiceItem(自己)来实现递归。<\/blockquote><template>\n <div>\n <div class=\"list\" :class=\"{ 'child-list': is_child }\" v-for=\"(item, index) in dataList\">\n <div class=\"main-body\">\n <span class=\"more-icon\" :class=\"{ 'body-icon': item[selectKey.child], 'active': item.show }\"\n @click=\"changeShow(item, index)\"><\/span>\n <span class=\"check-box\" @click=\"changeCheckStatus(item, index)\">\n <span class=\"chech-item\" :class=\"isCheckClass(item.check)\"><\/span>\n {{ item[selectKey.lable] }}\n <\/span>\n <\/div>\n <multipleChoiceItem v-if=\"item[selectKey.child] && item.show\" v-model=\"item[selectKey.child]\" :is_child=\"true\" \/>\n <\/div>\n <\/div>\n<\/template>\n<\/pre>
<\/p>
下方是script代码,内部实现了选中后递归所有子级选中,父级递归检测是全选还是半选状态。<\/blockquote><script>\n\/\/ 多选级联选择器\nexport default {\n name: 'multipleChoiceItem',\n data() {\n return {\n dataList: []\n }\n },\n props: {\n value: {\n type: Array,\n default: () => []\n },\n is_child: {\n type: Boolean,\n default: false\n },\n \/**\n * 用到的key\n * 默认 id name child\n *\/\n selectKey: {\n type: Object,\n default: () => {\n return { value: 'id', lable: 'name', child: 'child' }\n }\n },\n },\n created() {\n this.dataList = this.value;\n },\n watch: {\n\n\n },\n methods: {\n isCheckClass(val) {\n if(val == 0) { \/\/ 未选中\n return ''\n } else if(val == 1) {\n return 'pitch-on'; \/\/ 选中\n } else if (val == 2) {\n return 'half-select'; \/\/ 半选中\n }\n },\n \/\/ 修改选中状态\n changeCheckStatus(item, i) {\n if(item.check == 0 || item.check == 2) {\n item.check = 1;\n } else {\n item.check = 0;\n }\n try {\n this.changeStatus(item, i)\n } catch(err) {\n console.log('err=', err)\n }\n },\n \/\/ 父级自己联动\n changeStatus(item, i) {\n \/\/ console.log(item)\n \/\/ 子级全部修改相同\n this.diffChangeStatus(item)\n \/\/ 父级联动\n if (item.parent) {\n const diffParnet = (child_item) => {\n child_item.check = this.findParent(child_item);\n if(child_item.parent) {\n diffParnet(child_item.parent);\n }\n }\n diffParnet(item.parent)\n }\n },\n diffChangeStatus(item, i) {\n item[this.selectKey.child] && item[this.selectKey.child].forEach(v => {\n v.check = item.check;\n if (v[this.selectKey.child] && v[this.selectKey.child].length) {\n this.diffChangeStatus(v)\n }\n })\n },\n \/\/ 查找父级的选中的状态\n findParent(item) {\n let flag = 0;\n let flag_arr = [];\n let THIS = this;\n function filterData(arr) {\n arr.forEach(v => {\n if (v.check == 1) {\n flag = 1;\n }\n if(!flag_arr.includes(v.check)) {\n flag_arr.push(v.check)\n }\n \/\/ if (v[THIS.selectKey.child]) {\n \/\/ filterData(v[THIS.selectKey.child])\n \/\/ }\n })\n }\n filterData(item[THIS.selectKey.child]);\n if(flag_arr.length > 1) {\n return 2\n } else {\n return flag\n }\n },\n \/\/ 修改当前显示\n changeShow(item, i) {\n item.show = !item.show;\n },\n },\n}\n<\/script>\n<\/pre>
<\/p>
下方是style代码<\/blockquote><style lang=\"less\" scoped>\n.check-box {\n display: flex;\n align-items: center;\n}\n.chech-item {\n display: inline-block;\n width: 14px;\n height: 14px;\n border: 1px solid #D7D7D7;\n position: relative;\n margin-right: 4px;\n transition: all .3s;\n \/\/ 半选中\n &.half-select {\n border-color: #2d8cf0;\n background-color: #2d8cf0;\n color: #2d8cf0;\n &::after {\n content: \"\";\n width: 8px;\n height: 2px;\n background-color: #fff;\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-4px, -1px);\n }\n }\n \/\/ 全选\n &.pitch-on {\n border-color: #2d8cf0;\n background-color: #2d8cf0;\n color: #2d8cf0;\n &::after {\n content: \"\";\n width: 4px;\n height: 8px;\n position: absolute;\n top: 1px;\n left: 4px;\n border: 2px solid #fff;\n border-top: 0;\n border-left: 0;\n transition: all 0.2s ease-in-out;\n transform: rotate(45deg) scale(1);\n }\n }\n}\n.list {\n width: 100%;\n\n\n &.child-list {\n padding-left: 20px;\n }\n\n\n .more-icon {\n display: inline-block;\n position: relative;\n width: 30px;\n height: 100%;\n\n\n &.body-icon::after {\n content: \"\";\n position: absolute;\n left: 50%;\n top: 50%;\n margin-top: -4px;\n margin-left: 0px;\n width: 0;\n height: 0;\n border-left: 4px solid #787878;\n border-right: 4px solid transparent;\n border-top: 4px solid transparent;\n border-bottom: 4px solid transparent;\n }\n\n\n &.active::after {\n transform: rotate(90deg);\n }\n }\n\n\n .main-body {\n display: flex;\n align-items: center;\n height: 30px;\n }\n}\n<\/style>\n<\/pre>
<\/p>
<\/h2>
<div v-click-outside=\"close\" class=\"department-option\" @click.stop=\"show = true\">\n <div class=\"now-text\">\n 选择部门\n <\/div>\n <transition name=\"transition-drop\">\n <div class=\"department-box\" v-show=\"show\">\n <MultipleChoiceItem v-model=\"listData\" @child-change=\"childChange\" :selectKey=\"selectKey\" \/>\n <\/div>\n <\/transition>\n <\/div>\n<\/pre>
<\/p>接下来就是script代码。我们要引入组件 MultipleChoiceItem 组件,这个组件会放在下面。这是用来递归显示的组件。<\/blockquote>注意:一下代码是multipleChoice.vue的script代码。内部实现了对数据的处理,给已选中的定义选中状态,给原始数据增加是否选中字段,显示隐藏字段,给每条数据都赋值一个父级原型链。为的是当前状态修改后,所有父级需要变更状态如(半选,全选,未选)。<\/blockquote>props主要传入列表数据,是否需要自定义key包括返回的id,要显示的name,树结构的子级字段,v-model需要绑定的数据。<\/blockquote><script>\nimport MultipleChoiceItem from \".\/multipleChoiceItem.vue\"\nexport default {\n name: 'multipleChoice',\n data() {\n return {\n show: false,\n listData: [], \/\/ 列表数据\n selected: [], \/\/ 最终选中\n }\n },\n props: {\n data: {\n type: Array,\n default: () => []\n },\n \/\/ 未选提示\n placeholder: {\n type: String,\n default: '请选择'\n },\n \/**\n * 用到的key\n * 默认 id name child\n *\/\n selectKey: {\n type: Object,\n default: () => {\n return { value: 'id', lable: 'name', child: 'child' }\n }\n },\n value: {\n type: Array,\n default: () => []\n }\n },\n watch: {\n show(newVal) {\n if(!newVal) {\n this.updateData() \n }\n }\n },\n created() {\n this.init();\n },\n methods: {\n childChange(val) {\n \/\/ console.log(val)\n },\n openCascader() {\n this.show = true;\n this.init()\n },\n init() {\n if (this.data && this.data.length) {\n let arr = JSON.parse(JSON.stringify(this.data))\n this.washData(arr, null); \/\/ 洗数据\n this.listData = arr;\n }\n },\n washData(treeList, parent, flag) {\n for (let i = 0; i < treeList.length; i++) {\n let flag_n = false; \/\/ 父级有选中子级不显示 标识符\n let jsonTree = JSON.parse(JSON.stringify(treeList[i])); \/\/ 赋值已选\n const tree = treeList[i];\n tree.parent = parent;\n if(this.value.includes(tree[this.selectKey.value])) {\n flag_n = true;\n \/\/ 默认回选\n if (tree.parent) {\n const diffParnet = (child_item) => {\n if(child_item.check != 1) { \/\/ 只有没有选中的父级才能是半选状态\n child_item.check = 2;\n }\n if(child_item.parent) {\n diffParnet(child_item.parent);\n }\n }\n diffParnet(tree.parent)\n }\n tree.check = 1;\n tree.show = true;\n if(!flag) {\n this.selected_item.push(jsonTree); \/\/ 回源默认选中\n }\n } else {\n \/\/ 默认为0\n tree.check = 0;\n tree.show = false;\n }\n if (tree[this.selectKey.child]) {\n this.washData(tree[this.selectKey.child], tree, flag_n);\n }\n }\n },\n \/\/ 弹窗关闭\n updateData() {\n this.selected = [];\n this.findSelected(this.listData);\n this.$emit('input', this.selected);\n this.$emit('on-change', this.selected);\n },\n close() {\n this.show = false;\n },\n \/\/ 查询已被选中的数据\n findSelected(arr) {\n arr.forEach(v => {\n if (v.check == 1) {\n this.selected.push(v.id)\n }\n if (v[this.selectKey.child] && v[this.selectKey.child].length) {\n this.findSelected(v[this.selectKey.child])\n }\n })\n }\n },\n components: {\n MultipleChoiceItem\n }\n}\n<\/script>\n<\/pre>
<\/p>接下来是multipleChoice.vue的style代码<\/blockquote><style lang=\"less\" scoped>\n.department-option {\n width: 300px;\n height: 32px;\n margin-top: 20px;\n position: relative;\n border: 1px solid #d9d9d9;\n cursor: pointer;\n\n\n .department-box {\n width: 298px;\n max-height: 180px;\n overflow-y: auto;\n }\n\n\n .select-list {\n display: inline-block;\n background-color: #f5f5f5;\n color: rgba(0, 0, 0, .85);\n margin: 3px 0 3px 3px;\n height: 24px;\n line-height: 24px;\n padding: 0 10px;\n\n\n .icon-close {\n margin-left: 6px;\n font-size: 10px;\n color: rgba(0, 0, 0, .45);\n }\n }\n\n\n .now-text {\n overflow: hidden;\n color: #bfbfbf;\n white-space: nowrap;\n text-overflow: ellipsis;\n pointer-events: none;\n padding: 0 6px;\n line-height: 30px;\n }\n\n\n input {\n position: absolute;\n width: 0;\n border: 0;\n left: 6px;\n top: 0;\n height: 30px;\n padding: 6px 6px;\n }\n}\n<\/style>\n<\/pre>
<\/p>我们写好multipleChoice.vue组件后,要继续创建multipleChoiceItem.vue组件<\/h2>
multipleChoiceItem.vue组件是为了递归显示每条数据的,里面包括了多选,上级下级联动的逻辑。<\/blockquote>下方是template代码,重复使用组件multipleChoiceItem(自己)来实现递归。<\/blockquote><template>\n <div>\n <div class=\"list\" :class=\"{ 'child-list': is_child }\" v-for=\"(item, index) in dataList\">\n <div class=\"main-body\">\n <span class=\"more-icon\" :class=\"{ 'body-icon': item[selectKey.child], 'active': item.show }\"\n @click=\"changeShow(item, index)\"><\/span>\n <span class=\"check-box\" @click=\"changeCheckStatus(item, index)\">\n <span class=\"chech-item\" :class=\"isCheckClass(item.check)\"><\/span>\n {{ item[selectKey.lable] }}\n <\/span>\n <\/div>\n <multipleChoiceItem v-if=\"item[selectKey.child] && item.show\" v-model=\"item[selectKey.child]\" :is_child=\"true\" \/>\n <\/div>\n <\/div>\n<\/template>\n<\/pre>
<\/p>下方是script代码,内部实现了选中后递归所有子级选中,父级递归检测是全选还是半选状态。<\/blockquote><script>\n\/\/ 多选级联选择器\nexport default {\n name: 'multipleChoiceItem',\n data() {\n return {\n dataList: []\n }\n },\n props: {\n value: {\n type: Array,\n default: () => []\n },\n is_child: {\n type: Boolean,\n default: false\n },\n \/**\n * 用到的key\n * 默认 id name child\n *\/\n selectKey: {\n type: Object,\n default: () => {\n return { value: 'id', lable: 'name', child: 'child' }\n }\n },\n },\n created() {\n this.dataList = this.value;\n },\n watch: {\n\n\n },\n methods: {\n isCheckClass(val) {\n if(val == 0) { \/\/ 未选中\n return ''\n } else if(val == 1) {\n return 'pitch-on'; \/\/ 选中\n } else if (val == 2) {\n return 'half-select'; \/\/ 半选中\n }\n },\n \/\/ 修改选中状态\n changeCheckStatus(item, i) {\n if(item.check == 0 || item.check == 2) {\n item.check = 1;\n } else {\n item.check = 0;\n }\n try {\n this.changeStatus(item, i)\n } catch(err) {\n console.log('err=', err)\n }\n },\n \/\/ 父级自己联动\n changeStatus(item, i) {\n \/\/ console.log(item)\n \/\/ 子级全部修改相同\n this.diffChangeStatus(item)\n \/\/ 父级联动\n if (item.parent) {\n const diffParnet = (child_item) => {\n child_item.check = this.findParent(child_item);\n if(child_item.parent) {\n diffParnet(child_item.parent);\n }\n }\n diffParnet(item.parent)\n }\n },\n diffChangeStatus(item, i) {\n item[this.selectKey.child] && item[this.selectKey.child].forEach(v => {\n v.check = item.check;\n if (v[this.selectKey.child] && v[this.selectKey.child].length) {\n this.diffChangeStatus(v)\n }\n })\n },\n \/\/ 查找父级的选中的状态\n findParent(item) {\n let flag = 0;\n let flag_arr = [];\n let THIS = this;\n function filterData(arr) {\n arr.forEach(v => {\n if (v.check == 1) {\n flag = 1;\n }\n if(!flag_arr.includes(v.check)) {\n flag_arr.push(v.check)\n }\n \/\/ if (v[THIS.selectKey.child]) {\n \/\/ filterData(v[THIS.selectKey.child])\n \/\/ }\n })\n }\n filterData(item[THIS.selectKey.child]);\n if(flag_arr.length > 1) {\n return 2\n } else {\n return flag\n }\n },\n \/\/ 修改当前显示\n changeShow(item, i) {\n item.show = !item.show;\n },\n },\n}\n<\/script>\n<\/pre>
<\/p>下方是style代码<\/blockquote><style lang=\"less\" scoped>\n.check-box {\n display: flex;\n align-items: center;\n}\n.chech-item {\n display: inline-block;\n width: 14px;\n height: 14px;\n border: 1px solid #D7D7D7;\n position: relative;\n margin-right: 4px;\n transition: all .3s;\n \/\/ 半选中\n &.half-select {\n border-color: #2d8cf0;\n background-color: #2d8cf0;\n color: #2d8cf0;\n &::after {\n content: \"\";\n width: 8px;\n height: 2px;\n background-color: #fff;\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-4px, -1px);\n }\n }\n \/\/ 全选\n &.pitch-on {\n border-color: #2d8cf0;\n background-color: #2d8cf0;\n color: #2d8cf0;\n &::after {\n content: \"\";\n width: 4px;\n height: 8px;\n position: absolute;\n top: 1px;\n left: 4px;\n border: 2px solid #fff;\n border-top: 0;\n border-left: 0;\n transition: all 0.2s ease-in-out;\n transform: rotate(45deg) scale(1);\n }\n }\n}\n.list {\n width: 100%;\n\n\n &.child-list {\n padding-left: 20px;\n }\n\n\n .more-icon {\n display: inline-block;\n position: relative;\n width: 30px;\n height: 100%;\n\n\n &.body-icon::after {\n content: \"\";\n position: absolute;\n left: 50%;\n top: 50%;\n margin-top: -4px;\n margin-left: 0px;\n width: 0;\n height: 0;\n border-left: 4px solid #787878;\n border-right: 4px solid transparent;\n border-top: 4px solid transparent;\n border-bottom: 4px solid transparent;\n }\n\n\n &.active::after {\n transform: rotate(90deg);\n }\n }\n\n\n .main-body {\n display: flex;\n align-items: center;\n height: 30px;\n }\n}\n<\/style>\n<\/pre>
<\/p>
<\/h2>
