Open zptime opened 3 years ago
<template> <!-- 依托于ant design vue完成,必须先安装该组件库 --> <div class="tree-transfer ant-transfer ant-transfer-customize-list"> <!-- 源数据展示 --> <div class="ant-transfer-list"> <!-- 头部 --> <div class="ant-transfer-list-header"> <a-checkbox :indeterminate="from_is_indeterminate" v-model="from_check_all" @change="fromAllBoxChange" /> <span class="ant-transfer-list-header-selected"> <span >{{ from_check_keys.length || 0 }}/{{ from_all_keys.length }} {{ locale.itemUnit }}</span > <span class="ant-transfer-list-header-title">{{ fromTitle }}</span> </span> </div> <!-- 主体内容 --> <div class="ant-transfer-list-body ant-transfer-list-body-with-search"> <!-- 搜索框 --> <div v-if="filter" class="ant-transfer-list-body-search-wrapper"> <div> <a-input v-model="filterFrom" :placeholder="locale.searchPlaceholder" class="ant-transfer-list-search" /> <a class="ant-transfer-list-search-action"> <a-icon type="close-circle" theme="filled" v-if="filterFrom && filterFrom.length > 0" @click="filterFrom = ''" /> <a-icon type="search" v-else /> </a> </div> </div> <!-- 树列表 --> <div v-if="self_from_data && self_from_data.length > 0" class="ant-transfer-list-body-customize-wrapper" > <a-tree ref="from-tree" class="tt-tree from-tree" blockNode checkable :checked-keys="from_check_keys" :expanded-keys="from_expand_keys" :tree-data="self_from_data" @check="fromTreeChecked" @expand="fromTreeExpanded" :style="{ height: treeHeight + 'px' }" /> </div> <!-- 无数据显示 --> <div v-else class="ant-transfer-list-body-not-found"> {{ locale.notFoundContent }} </div> </div> </div> <!-- 中间操作栏 --> <div class="ant-transfer-operation"> <a-button type="primary" @click="addToAims(true)" shape="circle" :disabled="from_disabled" icon="right" ></a-button> <a-button type="primary" @click="removeToSource" shape="circle" :disabled="to_disabled" icon="left" ></a-button> </div> <!-- 目标数据展示 --> <div class="ant-transfer-list"> <!-- 头部 --> <div class="ant-transfer-list-header"> <a-checkbox :indeterminate="to_is_indeterminate" v-model="to_check_all" @change="toAllBoxChange" /> <span class="ant-transfer-list-header-selected"> <span >{{ to_check_keys.length || 0 }}/{{ to_all_keys.length }} {{ locale.itemsUnit }}</span > <span class="ant-transfer-list-header-title">{{ toTitle }}</span> </span> </div> <!-- 主体内容 --> <div class="ant-transfer-list-body ant-transfer-list-body-with-search"> <!-- 搜索框 --> <div v-if="filter" class="ant-transfer-list-body-search-wrapper"> <div> <a-input v-model="filterTo" :placeholder="locale.searchPlaceholder" class="ant-transfer-list-search" /> <a class="ant-transfer-list-search-action"> <a-icon type="close-circle" theme="filled" v-if="filterTo && filterTo.length > 0" @click="filterTo = ''" /> <a-icon type="search" v-else /> </a> </div> </div> <!-- 树列表 --> <div v-if="self_to_data && self_to_data.length > 0" class="ant-transfer-list-body-customize-wrapper" > <a-tree class="tt-tree to-tree" ref="to-tree" blockNode checkable :checked-keys="to_check_keys" :expanded-keys="to_expand_keys" :tree-data="self_to_data" @check="toTreeChecked" @expand="toTreeExpanded" :style="{ height: treeHeight + 'px' }" /> </div> <!-- 无数据显示 --> <div v-else class="ant-transfer-list-body-not-found"> {{ locale.notFoundContent }} </div> </div> </div> </div> </template> <script> // 关键词过滤 const filterKeywordTreeFn = (tree = [], keyword = "") => { if (!(tree && tree.length)) { return []; } if (!keyword) { return tree; } return R.filter((o) => { // 1. 父节点满足条件,直接返回 if (o.title.includes(keyword)) { return true; } if (o.children?.length) { // 2. 否则,存在子节点时,递归处理 o.children = filterKeywordTreeFn(o.children, keyword); } // 3. 子节点满足条件时,返回 return o.children && o.children.length; // 避免修改原数据,此处用R.clone()处理一下 }, R.clone(tree)); }; // 选中节点过滤:源数据 const filterSourceTreeFn = (tree = [], targetKeys = [], result = []) => { // 父节点和子节点都存在才返回 R.forEach((o) => { // 1. 判断当前节点是否含符合条件的数据:是-继续;否-过滤 if (targetKeys.indexOf(o.id) < 0) { // 2. 判断是否含有子节点:是-继续;否-直接返回 if (o.children?.length) { // 3. 子节点递归处理 o.children = filterSourceTreeFn(o.children, targetKeys); // 4. 存在子节点,且子节点也有符合条件的子节点,直接返回 if (o.children.length) result.push(o); } else { result.push(o); } } }, R.clone(tree)); return result; }; // 选中节点保留:目标数据 const filterTargetTreeFn = (tree = [], targetKeys = []) => { if (!(tree && tree.length)) { return []; } if (!targetKeys?.length) { return tree; } return R.filter((o) => { // 存在子节点时,递归处理 if (o.children?.length) { o.children = filterTargetTreeFn(o.children, targetKeys); } return ( R.indexOf(o.id, targetKeys) > -1 || (o.children && o.children.length) ); }, R.clone(tree)); }; export default { name: "treeTransfer", props: { dataSource: { // 数据源 type: Array, default: () => [], }, targetKeys: { // 右侧框数据的 key 集合 type: Array, default: () => [], }, // 标题 titles: { type: Array, default: () => ["源列表", "目标列表"], }, // 各种语言 locale: { type: Object, default: () => { return { itemUnit: "项", itemsUnit: "项", notFoundContent: "暂无数据", searchPlaceholder: "请输入城市名称", }; }, }, // 是否启用筛选 filter: { type: Boolean, default: true, }, // 替换 treeData 中对应的字段 replaceFields: { type: Object, default: () => { return { children: "children", title: "title", key: "key", }; }, }, }, data() { return { data_source: [...this.dataSource], // 数据源 target_keys: [], // 右侧框数据的 key 集合 from_is_indeterminate: false, // 源数据是否半选 from_check_all: false, // 源数据是否全选 to_is_indeterminate: false, // 目标数据是否半选 to_check_all: false, // 目标数据是否全选 from_disabled: true, // 添加按钮是否禁用 to_disabled: true, // 移除按钮是否禁用 from_check_keys: [], // 源数据选中key数组 以此属性关联穿梭按钮,总全选、半选状态 to_check_keys: [], // 目标数据选中key数组 以此属性关联穿梭按钮,总全选、半选状态 from_expand_keys: [], // 源数据展开key数组 to_expand_keys: [], // 目标数据展开key数组 from_all_keys: [], // 源数据所有key to_all_keys: [], // 目标数据所有key filterFrom: "", // 源数据筛选 filterTo: "", // 目标数据筛选 fullHeight: document.documentElement.clientHeight, // 网页可见区域高度 }; }, mounted() { this.getBodyHeight(); }, computed: { // 源数据 self_from_data() { let from_array = filterSourceTreeFn(this.data_source, this.target_keys); if (this.filterFrom) { from_array = filterKeywordTreeFn(from_array, this.filterFrom); } this.from_all_keys = this.getAllKeys(from_array); this.from_check_keys = this.from_check_keys.filter((key) => this.from_all_keys.includes(key) ); return from_array; }, // 目标数据 self_to_data() { let to_array = filterTargetTreeFn(this.data_source, this.target_keys); if (this.filterTo) { to_array = filterKeywordTreeFn(to_array, this.filterTo); } this.to_all_keys = this.getAllKeys(to_array); this.to_check_keys = this.to_check_keys.filter((key) => this.to_all_keys.includes(key) ); return to_array; }, // 源数据菜单名 fromTitle() { let [text] = this.titles; return text; }, // 目标数据菜单名 toTitle() { let [, text] = this.titles; return text; }, treeHeight() { return this.fullHeight - 350; }, }, watch: { targetKeys: { handler: function (val) { this.target_keys = val; // 过滤数据清除 this.filterFrom = ""; this.filterTo = ""; // 滚动条回到顶部 this.$nextTick(() => { if (this.$el.querySelector(".from-tree")) { this.$el.querySelector(".from-tree").scrollTop = 0; } if (this.$el.querySelector(".to-tree")) { this.$el.querySelector(".to-tree").scrollTop = 0; } }); // tree数据清除 this.clearChecked(); }, deep: true, immediate: true, }, /* 左侧 状态监测 */ from_check_keys(val) { if (val.length > 0) { // 穿梭按钮是否禁用 this.from_disabled = false; // 总半选是否开启 this.from_is_indeterminate = true; // 总全选是否开启 - 根据选中节点中为根节点的数量是否和源数据长度相等 let allParentKeys = this.self_from_data.map( (item) => item[this.replaceFields.key] ); let allCheck = val.filter((item) => allParentKeys.includes(item)); if (allCheck.length == this.self_from_data.length) { // 关闭半选 开启全选 this.from_is_indeterminate = false; this.from_check_all = true; } else { this.from_is_indeterminate = true; this.from_check_all = false; } } else { this.from_disabled = true; this.from_is_indeterminate = false; this.from_check_all = false; } }, /* 右侧 状态监测 */ to_check_keys(val) { if (val.length > 0) { // 穿梭按钮是否禁用 this.to_disabled = false; // 总半选是否开启 this.to_is_indeterminate = true; // 总全选是否开启 - 根据选中节点中为根节点的数量是否和源数据长度相等 let allParentKeys = this.self_to_data.map( (item) => item[this.replaceFields.key] ); let allCheck = val.filter((item) => allParentKeys.includes(item)); if (allCheck.length == this.self_to_data.length) { // 关闭半选 开启全选 this.to_is_indeterminate = false; this.to_check_all = true; } else { this.to_is_indeterminate = true; this.to_check_all = false; } } else { this.to_disabled = true; this.to_is_indeterminate = false; this.to_check_all = false; } }, }, methods: { /** * 清空选中节点 * type:string left左边 right右边 all全部 默认all */ clearChecked(type = "all") { if (type === "left") { this.from_check_keys = []; this.from_expand_keys = []; this.from_is_indeterminate = false; this.from_check_all = false; } else if (type === "right") { this.to_check_keys = []; this.to_expand_keys = []; this.to_is_indeterminate = false; this.to_check_all = false; } else { this.from_check_keys = []; this.to_check_keys = []; this.from_expand_keys = []; this.to_expand_keys = []; this.from_is_indeterminate = false; this.from_check_all = false; this.to_is_indeterminate = false; this.to_check_all = false; } }, /* 添加到右边 */ addToAims(emit) { // 右侧添加选中数据 let targetKeys = [...this.target_keys, ...this.from_check_keys]; this.target_keys = [...new Set(targetKeys)]; // 左侧删掉选中数据 this.from_check_keys = []; // 传递信息给父组件 emit && this.$emit( "handleChange", this.self_from_data, this.self_to_data, this.target_keys ); }, /* 移除到左边 */ removeToSource() { // 右侧删除选中数据 let targetKeys = this.target_keys.filter( (item) => !this.to_check_keys.includes(item) ); this.target_keys = [...new Set(targetKeys)]; // 左侧添加选中数据 this.to_check_keys = []; // 解决右侧数据筛选后提交,数据为空的问题 this.filterTo = ""; // 传递信息给父组件 this.$emit( "handleChange", this.self_from_data, this.self_to_data, this.target_keys ); }, /* 源树选中事件 - 是否禁用穿梭按钮 */ fromTreeExpanded(expandedKeys) { this.from_expand_keys = expandedKeys; }, /* 目标树选中事件 - 是否禁用穿梭按钮 */ toTreeExpanded(expandedKeys) { this.to_expand_keys = expandedKeys; }, /* 源树选中事件 - 是否禁用穿梭按钮 */ fromTreeChecked(checkedKeys, e) { this.from_check_keys = checkedKeys; }, /* 目标树选中事件 - 是否禁用穿梭按钮 */ toTreeChecked(checkedKeys, e) { this.to_check_keys = checkedKeys; }, /* 源数据 总全选checkbox */ fromAllBoxChange(val) { if (this.self_from_data.length == 0) { return; } if (val.target.checked) { this.from_check_keys = this.getAllKeys(this.self_from_data); } else { this.from_check_keys = []; } this.$emit("left-check-change", this.from_check_all); }, /* 目标数据 总全选checkbox */ toAllBoxChange(val) { if (this.self_to_data.length == 0) { return; } if (val.target.checked) { this.to_check_keys = this.getAllKeys(this.self_to_data); } else { this.to_check_keys = []; } this.$emit("right-check-change", this.to_check_all); }, /* 获取数据所有key */ getAllKeys(data) { let result = []; data.forEach((item) => { result.push(item[this.replaceFields.key]); if (item.children && item.children.length) { item.children.forEach((o) => { result.push(o[this.replaceFields.key]); }); } }); return result; }, // 动态获取浏览器高度 getBodyHeight() { const _this = this; window.onresize = () => { return (() => { window.fullHeight = document.documentElement.clientHeight; _this.fullHeight = window.fullHeight; })(); }; }, }, }; </script> <style lang="scss" scoped> $px: 1px; .tree-transfer { .ant-transfer-list { flex: 1; } .tt-tree { max-height: 400 * $px; overflow-y: auto; overflow-x: hidden; height: 100%; } } </style>