| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583 |
- <template>
- <view class="cl-tree" :class="[pt.className]">
- <cl-tree-item
- v-for="item in data"
- :key="item.id"
- :item="item"
- :level="0"
- :pt="props.pt"
- ></cl-tree-item>
- </view>
- </template>
- <script lang="ts" setup>
- import { computed, watch, ref, type PropType } from "vue";
- import type { ClTreeItem, ClTreeNodeInfo } from "../../types";
- import { first, isEmpty, isEqual, parsePt } from "@/.cool";
- defineOptions({
- name: "cl-tree"
- });
- const props = defineProps({
- pt: {
- type: Object as PropType<any>,
- default: () => ({})
- },
- // 绑定值
- modelValue: {
- type: [Array, String, Number] as PropType<any | null>,
- default: null
- },
- // 树形结构数据
- list: {
- type: Array as PropType<ClTreeItem[]>,
- default: () => []
- },
- // 节点图标
- icon: {
- type: String,
- default: "arrow-right-s-fill"
- },
- // 展开图标
- expandIcon: {
- type: String,
- default: "arrow-down-s-fill"
- },
- // 是否严格的遵循父子不互相关联
- checkStrictly: {
- type: Boolean,
- default: false
- },
- // 是否可以选择节点
- checkable: {
- type: Boolean,
- default: true
- },
- // 是否允许多选
- multiple: {
- type: Boolean,
- default: false
- }
- });
- const emit = defineEmits(["update:modelValue", "change"]);
- // 定义透传类型
- type PassThrough = {
- className?: string;
- };
- // 计算样式穿透对象
- const pt = computed(() => parsePt<PassThrough>(props.pt));
- // 树数据
- 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; // 节点不存在则返回
- // 非多选模式下,清空所有选中状态
- if (!props.multiple) {
- clearChecked();
- }
- // 设置当前节点选中状态
- nodeInfo.node.isChecked = flag;
- // 多选模式下处理
- if (props.multiple) {
- // 非严格模式下处理父子联动
- 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 {
- // 设置指定节点为展开状态
- 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() {
- // 遍历所有节点,将isExpand设为false
- nodeMap.value.forEach((info: ClTreeNodeInfo) => {
- info.node.isExpand = false;
- });
- }
- /**
- * 同步绑定值
- */
- /**
- * 同步绑定值到外部
- * 当内部选中状态变化时,更新外部的modelValue,并触发change事件
- */
- function syncModelValue() {
- // 如果树数据为空,则不更新绑定值
- if (isEmpty(data.value)) {
- return;
- }
- // 获取当前所有选中的key
- const checkedKeys = getCheckedKeys();
- // 如果外部modelValue为null,或当前选中key与外部modelValue不一致,则更新
- if (props.modelValue == null || !isEqual(checkedKeys, props.modelValue!)) {
- // 如果多选,直接传递数组;否则只传第一个选中的key
- const value = props.multiple ? checkedKeys : first(checkedKeys);
- emit("update:modelValue", value); // 通知外部更新modelValue
- emit("change", value); // 触发change事件
- }
- }
- /**
- * 同步外部modelValue到内部选中状态
- * 当外部modelValue变化时,更新内部选中状态,并保持与外部一致
- */
- function syncCheckedState() {
- // 如果外部modelValue为null,则不处理
- if (props.modelValue == null) {
- return;
- }
- // 获取当前所有选中的key
- const checkedKeys = getCheckedKeys();
- // 如果当前选中key与外部modelValue不一致,则进行同步
- if (!isEqual(checkedKeys, props.modelValue!)) {
- if (Array.isArray(props.modelValue)) {
- setCheckedKeys(props.modelValue!); // 多选时,设置所有选中key
- } else {
- setChecked(props.modelValue!, true); // 单选时,设置单个选中key
- }
- }
- syncModelValue(); // 同步绑定值到外部
- }
- // 监听props.list变化,同步到内部数据
- watch(
- computed(() => props.list),
- (val: ClTreeItem[]) => {
- data.value = val;
- // 检查选中状态
- syncCheckedState();
- },
- { immediate: true }
- );
- // 监听modelValue变化
- watch(
- computed(() => [props.modelValue ?? 0]),
- () => {
- syncCheckedState();
- },
- { immediate: true, deep: true }
- );
- // 监听树数据变化
- watch(
- data,
- () => {
- // 自动更新选中状态
- if (!props.checkStrictly && props.multiple) {
- updateAllCheckStates();
- }
- // 更新绑定值
- syncModelValue();
- },
- { deep: true }
- );
- defineExpose({
- icon: computed(() => props.icon),
- expandIcon: computed(() => props.expandIcon),
- checkStrictly: computed(() => props.checkStrictly),
- checkable: computed(() => props.checkable),
- multiple: computed(() => props.multiple),
- clearChecked,
- setChecked,
- setCheckedKeys,
- getCheckedKeys,
- getHalfCheckedKeys,
- setExpanded,
- setExpandedKeys,
- getExpandedKeys,
- expandAll,
- collapseAll
- });
- </script>
|