|
|
@@ -0,0 +1,491 @@
|
|
|
+<template>
|
|
|
+ <view class="cl-tree">
|
|
|
+ <cl-tree-item
|
|
|
+ v-for="item in data"
|
|
|
+ :key="item.id"
|
|
|
+ :item="item"
|
|
|
+ :level="0"
|
|
|
+ :pt="pt"
|
|
|
+ ></cl-tree-item>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup>
|
|
|
+import { computed, watch, ref, type PropType } from "vue";
|
|
|
+import type { ClTreeItem, ClTreeNodeInfo } from "../../types";
|
|
|
+
|
|
|
+defineOptions({
|
|
|
+ name: "cl-tree"
|
|
|
+});
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ pt: {
|
|
|
+ type: Object as PropType<any>,
|
|
|
+ default: () => ({})
|
|
|
+ },
|
|
|
+ // 树形结构数据
|
|
|
+ list: {
|
|
|
+ type: Array as PropType<ClTreeItem[]>,
|
|
|
+ default: () => []
|
|
|
+ },
|
|
|
+ // 节点图标
|
|
|
+ icon: {
|
|
|
+ type: String,
|
|
|
+ default: "arrow-right-s-fill"
|
|
|
+ },
|
|
|
+ // 展开图标
|
|
|
+ expandIcon: {
|
|
|
+ type: String,
|
|
|
+ default: "arrow-down-s-fill"
|
|
|
+ },
|
|
|
+ // 是否显示复选框
|
|
|
+ showCheckbox: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ // 是否严格的遵循父子不互相关联
|
|
|
+ checkStrictly: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 树数据
|
|
|
+const data = ref<ClTreeItem[]>(props.list);
|
|
|
+
|
|
|
+/**
|
|
|
+ * 优化的节点搜索算法,使用Map缓存提升查找性能
|
|
|
+ * 创建节点索引映射,O(1)时间复杂度查找节点
|
|
|
+ */
|
|
|
+const nodeMap = computed(() => {
|
|
|
+ // 创建一个Map用于存储节点信息,key为节点id,value为节点信息对象
|
|
|
+ const map = new Map<string | number, ClTreeNodeInfo>();
|
|
|
+
|
|
|
+ // 递归遍历所有节点,构建节点与父节点的映射关系
|
|
|
+ function buildMap(nodes: ClTreeItem[], parent: ClTreeItem | null = null): void {
|
|
|
+ for (let i: number = 0; i < nodes.length; i++) {
|
|
|
+ const node = nodes[i]; // 当前节点
|
|
|
+
|
|
|
+ // 将当前节点的信息存入map,包含节点本身、父节点、在父节点中的索引
|
|
|
+ map.set(node.id, { node, parent, index: i } as ClTreeNodeInfo);
|
|
|
+
|
|
|
+ // 如果当前节点有子节点,则递归处理子节点
|
|
|
+ if (node.children != null && node.children.length > 0) {
|
|
|
+ buildMap(node.children, node);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从根节点开始构建映射
|
|
|
+ buildMap(data.value);
|
|
|
+ return map; // 返回构建好的Map
|
|
|
+});
|
|
|
+
|
|
|
+/**
|
|
|
+ * 根据key查找节点信息
|
|
|
+ * @param key 节点id
|
|
|
+ * @returns 节点信息对象或null
|
|
|
+ */
|
|
|
+function findNodeInfo(key: string | number): ClTreeNodeInfo | null {
|
|
|
+ const result = nodeMap.value.get(key); // 从Map中查找节点信息
|
|
|
+ return result != null ? result : null; // 找到则返回,否则返回null
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取指定节点的所有祖先节点
|
|
|
+ * @param key 节点id
|
|
|
+ * @returns 祖先节点数组(从根到最近父节点顺序)
|
|
|
+ */
|
|
|
+function getAncestors(key: string | number): ClTreeItem[] {
|
|
|
+ const result: ClTreeItem[] = []; // 用于存储祖先节点
|
|
|
+ let nodeInfo = findNodeInfo(key); // 当前节点信息
|
|
|
+
|
|
|
+ // 循环查找父节点,直到根节点
|
|
|
+ while (nodeInfo != null && nodeInfo.parent != null) {
|
|
|
+ result.unshift(nodeInfo.parent); // 将父节点插入到数组前面
|
|
|
+ nodeInfo = findNodeInfo(nodeInfo.parent.id); // 查找父节点信息
|
|
|
+ }
|
|
|
+
|
|
|
+ return result; // 返回祖先节点数组
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 更新所有节点的选中状态(用于批量操作后的状态同步)
|
|
|
+ */
|
|
|
+function updateAllCheckStates(): void {
|
|
|
+ // 递归更新每个节点的选中和半选状态
|
|
|
+ function updateNodeStates(nodes: ClTreeItem[]): void {
|
|
|
+ for (let i: number = 0; i < nodes.length; i++) {
|
|
|
+ const node = nodes[i]; // 当前节点
|
|
|
+ const children = node.children != null ? node.children : []; // 子节点数组
|
|
|
+
|
|
|
+ if (children.length == 0) {
|
|
|
+ // 叶子节点,重置半选状态
|
|
|
+ node.isHalfChecked = false;
|
|
|
+ continue; // 跳过后续处理
|
|
|
+ }
|
|
|
+
|
|
|
+ // 先递归处理子节点
|
|
|
+ updateNodeStates(children);
|
|
|
+
|
|
|
+ // 统计子节点的选中和半选数量
|
|
|
+ let checkedCount = 0; // 选中数量
|
|
|
+ let halfCheckedCount = 0; // 半选数量
|
|
|
+
|
|
|
+ for (let j = 0; j < children.length; j++) {
|
|
|
+ if (children[j].isChecked == true) {
|
|
|
+ checkedCount++;
|
|
|
+ } else if (children[j].isHalfChecked == true) {
|
|
|
+ halfCheckedCount++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据子节点状态更新当前节点状态
|
|
|
+ if (checkedCount == children.length) {
|
|
|
+ // 全部选中
|
|
|
+ node.isChecked = true;
|
|
|
+ node.isHalfChecked = false;
|
|
|
+ } else if (checkedCount > 0 || halfCheckedCount > 0) {
|
|
|
+ // 部分选中或有半选
|
|
|
+ node.isChecked = false;
|
|
|
+ node.isHalfChecked = true;
|
|
|
+ } else {
|
|
|
+ // 全部未选中
|
|
|
+ node.isChecked = false;
|
|
|
+ node.isHalfChecked = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从根节点开始递归更新
|
|
|
+ updateNodeStates(data.value);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 更新指定节点的所有祖先节点的选中状态
|
|
|
+ * @param key 节点id
|
|
|
+ */
|
|
|
+function updateAncestorsCheckState(key: string | number): void {
|
|
|
+ const ancestors = getAncestors(key); // 获取所有祖先节点
|
|
|
+
|
|
|
+ // 从最近的父节点开始向上更新
|
|
|
+ for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
|
+ const ancestor = ancestors[i]; // 当前祖先节点
|
|
|
+ const children = ancestor.children != null ? ancestor.children : ([] as ClTreeItem[]); // 子节点数组
|
|
|
+
|
|
|
+ if (children.length == 0) continue; // 没有子节点则跳过
|
|
|
+
|
|
|
+ let checkedCount = 0; // 选中数量
|
|
|
+ let halfCheckedCount = 0; // 半选数量
|
|
|
+
|
|
|
+ // 统计子节点的选中和半选数量
|
|
|
+ for (let j = 0; j < children.length; j++) {
|
|
|
+ if (children[j].isChecked == true) {
|
|
|
+ checkedCount++;
|
|
|
+ } else if (children[j].isHalfChecked == true) {
|
|
|
+ halfCheckedCount++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据子节点状态更新当前祖先节点状态
|
|
|
+ if (checkedCount == children.length) {
|
|
|
+ // 全部选中
|
|
|
+ ancestor.isChecked = true;
|
|
|
+ ancestor.isHalfChecked = false;
|
|
|
+ } else if (checkedCount > 0 || halfCheckedCount > 0) {
|
|
|
+ // 部分选中或有半选
|
|
|
+ ancestor.isChecked = false;
|
|
|
+ ancestor.isHalfChecked = true;
|
|
|
+ } else {
|
|
|
+ // 全部未选中
|
|
|
+ ancestor.isChecked = false;
|
|
|
+ ancestor.isHalfChecked = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取指定节点的所有子孙节点
|
|
|
+ * 优化:使用队列实现广度优先遍历,避免递归栈溢出
|
|
|
+ * @param key 节点id
|
|
|
+ * @returns 子孙节点数组
|
|
|
+ */
|
|
|
+function getDescendants(key: string | number): ClTreeItem[] {
|
|
|
+ const nodeInfo = findNodeInfo(key); // 查找节点信息
|
|
|
+ if (nodeInfo == null || nodeInfo.node.children == null) {
|
|
|
+ return []; // 节点不存在或无子节点,返回空数组
|
|
|
+ }
|
|
|
+
|
|
|
+ // 存储所有子孙节点
|
|
|
+ const result: ClTreeItem[] = [];
|
|
|
+
|
|
|
+ // 队列用于广度优先遍历
|
|
|
+ const queue: ClTreeItem[] = [];
|
|
|
+
|
|
|
+ // 将子节点添加到队列中
|
|
|
+ for (let i = 0; i < nodeInfo.node.children.length; i++) {
|
|
|
+ queue.push(nodeInfo.node.children[i]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 广度优先遍历所有子孙节点
|
|
|
+ while (queue.length > 0) {
|
|
|
+ const node = queue.shift(); // 取出队首节点
|
|
|
+
|
|
|
+ if (node == null) break; // 队列为空则结束
|
|
|
+
|
|
|
+ result.push(node); // 将当前节点加入结果数组
|
|
|
+
|
|
|
+ // 如果有子节点,继续加入队列
|
|
|
+ if (node.children != null && node.children.length > 0) {
|
|
|
+ for (let i = 0; i < node.children.length; i++) {
|
|
|
+ queue.push(node.children[i]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result; // 返回所有子孙节点
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 清空所有节点的选中状态
|
|
|
+ */
|
|
|
+function clearChecked(): void {
|
|
|
+ // 遍历所有节点,将 isChecked 和 isHalfChecked 设为 false
|
|
|
+ nodeMap.value.forEach((info: ClTreeNodeInfo) => {
|
|
|
+ info.node.isChecked = false;
|
|
|
+ info.node.isHalfChecked = false;
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 设置指定节点的选中状态
|
|
|
+ * @param key 节点id
|
|
|
+ * @param flag 是否选中
|
|
|
+ */
|
|
|
+function setChecked(key: string | number, flag: boolean): void {
|
|
|
+ const nodeInfo = findNodeInfo(key); // 查找节点信息
|
|
|
+ if (nodeInfo == null) return; // 节点不存在则返回
|
|
|
+
|
|
|
+ // 设置当前节点选中状态
|
|
|
+ nodeInfo.node.isChecked = flag;
|
|
|
+
|
|
|
+ // 非严格模式下处理父子联动
|
|
|
+ if (!props.checkStrictly) {
|
|
|
+ // 设置所有子孙节点的选中状态
|
|
|
+ const descendants = getDescendants(key);
|
|
|
+ for (let i = 0; i < descendants.length; i++) {
|
|
|
+ descendants[i].isChecked = flag;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新所有祖先节点的状态
|
|
|
+ updateAncestorsCheckState(key);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 批量设置节点选中状态
|
|
|
+ * @param keys 需要设置为选中的节点id数组
|
|
|
+ */
|
|
|
+function setCheckedKeys(keys: (string | number)[]): void {
|
|
|
+ // 遍历所有需要选中的节点
|
|
|
+ for (let i = 0; i < keys.length; i++) {
|
|
|
+ const key: string | number = keys[i];
|
|
|
+ const nodeInfo = findNodeInfo(key); // 查找节点信息
|
|
|
+
|
|
|
+ if (nodeInfo != null) {
|
|
|
+ nodeInfo.node.isChecked = true; // 设置为选中
|
|
|
+
|
|
|
+ // 非严格模式下同时设置所有子孙节点为选中状态
|
|
|
+ if (!props.checkStrictly) {
|
|
|
+ const descendants = getDescendants(key);
|
|
|
+ for (let j = 0; j < descendants.length; j++) {
|
|
|
+ descendants[j].isChecked = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 非严格模式下更新所有相关节点的状态
|
|
|
+ if (!props.checkStrictly) {
|
|
|
+ updateAllCheckStates();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取所有选中节点的keys
|
|
|
+ * @returns 选中节点id数组
|
|
|
+ */
|
|
|
+function getCheckedKeys(): (string | number)[] {
|
|
|
+ const result: (string | number)[] = []; // 存储选中节点id
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 递归收集所有选中节点的id
|
|
|
+ * @param nodes 当前遍历的节点数组
|
|
|
+ */
|
|
|
+ function collectCheckedKeys(nodes: ClTreeItem[]): void {
|
|
|
+ for (let i = 0; i < nodes.length; i++) {
|
|
|
+ const node = nodes[i];
|
|
|
+
|
|
|
+ if (node.isChecked == true) {
|
|
|
+ result.push(node.id); // 收集选中节点id
|
|
|
+ }
|
|
|
+
|
|
|
+ if (node.children != null) {
|
|
|
+ collectCheckedKeys(node.children); // 递归处理子节点
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ collectCheckedKeys(data.value); // 从根节点开始收集
|
|
|
+ return result; // 返回所有选中节点id
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取所有半选中节点的keys
|
|
|
+ * @returns 半选节点id数组
|
|
|
+ */
|
|
|
+function getHalfCheckedKeys(): (string | number)[] {
|
|
|
+ const result: (string | number)[] = []; // 存储半选节点id
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 递归收集所有半选节点的id
|
|
|
+ * @param nodes 当前遍历的节点数组
|
|
|
+ */
|
|
|
+ function collectHalfCheckedKeys(nodes: ClTreeItem[]): void {
|
|
|
+ for (let i = 0; i < nodes.length; i++) {
|
|
|
+ const node = nodes[i];
|
|
|
+
|
|
|
+ if (node.isHalfChecked == true) {
|
|
|
+ result.push(node.id); // 收集半选节点id
|
|
|
+ }
|
|
|
+
|
|
|
+ if (node.children != null) {
|
|
|
+ collectHalfCheckedKeys(node.children); // 递归处理子节点
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ collectHalfCheckedKeys(data.value); // 从根节点开始收集
|
|
|
+ return result; // 返回所有半选节点id
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 设置指定节点的展开状态
|
|
|
+ * @param key 节点id
|
|
|
+ * @param flag 是否展开
|
|
|
+ */
|
|
|
+function setExpanded(key: string | number, flag: boolean): void {
|
|
|
+ const nodeInfo = findNodeInfo(key); // 查找节点信息
|
|
|
+ if (nodeInfo == null) return; // 节点不存在则返回
|
|
|
+
|
|
|
+ nodeInfo.node.isExpand = flag; // 设置节点的展开状态
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 批量设置节点展开状态
|
|
|
+ * @param keys 需要展开的节点id数组
|
|
|
+ */
|
|
|
+function setExpandedKeys(keys: (string | number)[]): void {
|
|
|
+ // 先重置所有节点为收起状态
|
|
|
+ nodeMap.value.forEach((info: ClTreeNodeInfo) => {
|
|
|
+ info.node.isExpand = false;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 设置指定节点为展开状态
|
|
|
+ for (let i = 0; i < keys.length; i++) {
|
|
|
+ const nodeInfo = findNodeInfo(keys[i]);
|
|
|
+
|
|
|
+ if (nodeInfo != null) {
|
|
|
+ nodeInfo.node.isExpand = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取所有展开节点的keys
|
|
|
+ * @returns 展开节点id数组
|
|
|
+ */
|
|
|
+function getExpandedKeys(): (string | number)[] {
|
|
|
+ const result: (string | number)[] = []; // 存储展开节点id
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 递归收集所有展开节点的id
|
|
|
+ * @param nodes 当前遍历的节点数组
|
|
|
+ */
|
|
|
+ function collectExpandedKeys(nodes: ClTreeItem[]): void {
|
|
|
+ for (let i = 0; i < nodes.length; i++) {
|
|
|
+ const node = nodes[i];
|
|
|
+
|
|
|
+ if (node.isExpand == true) {
|
|
|
+ result.push(node.id); // 收集展开节点id
|
|
|
+ }
|
|
|
+
|
|
|
+ if (node.children != null) {
|
|
|
+ collectExpandedKeys(node.children); // 递归处理子节点
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ collectExpandedKeys(data.value); // 从根节点开始收集
|
|
|
+ return result; // 返回所有展开节点id
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 展开所有节点
|
|
|
+ */
|
|
|
+function expandAll(): void {
|
|
|
+ // 遍历所有节点,如果有子节点则设置为展开
|
|
|
+ nodeMap.value.forEach((info: ClTreeNodeInfo) => {
|
|
|
+ if (info.node.children != null && info.node.children.length > 0) {
|
|
|
+ info.node.isExpand = true;
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 收起所有节点
|
|
|
+ */
|
|
|
+function collapseAll(): void {
|
|
|
+ // 遍历所有节点,将isExpand设为false
|
|
|
+ nodeMap.value.forEach((info: ClTreeNodeInfo) => {
|
|
|
+ info.node.isExpand = false;
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// 监听props.list变化,同步到内部数据
|
|
|
+watch(
|
|
|
+ computed(() => props.list),
|
|
|
+ (val: ClTreeItem[]) => {
|
|
|
+ data.value = val;
|
|
|
+ },
|
|
|
+ { immediate: true, deep: true }
|
|
|
+);
|
|
|
+
|
|
|
+// 监听树数据变化,自动更新选中状态
|
|
|
+watch(
|
|
|
+ data,
|
|
|
+ () => {
|
|
|
+ if (!props.checkStrictly) {
|
|
|
+ updateAllCheckStates();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+);
|
|
|
+
|
|
|
+defineExpose({
|
|
|
+ icon: computed(() => props.icon),
|
|
|
+ expandIcon: computed(() => props.expandIcon),
|
|
|
+ showCheckbox: computed(() => props.showCheckbox),
|
|
|
+ checkStrictly: computed(() => props.checkStrictly),
|
|
|
+ clearChecked,
|
|
|
+ setChecked,
|
|
|
+ setCheckedKeys,
|
|
|
+ getCheckedKeys,
|
|
|
+ getHalfCheckedKeys,
|
|
|
+ setExpanded,
|
|
|
+ setExpandedKeys,
|
|
|
+ getExpandedKeys,
|
|
|
+ expandAll,
|
|
|
+ collapseAll
|
|
|
+});
|
|
|
+</script>
|