Files
jindengchen-ai-report/cool-unix/uni_modules/cool-ui/components/cl-tree/cl-tree.uvue
2025-11-13 10:36:23 +08:00

584 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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为节点idvalue为节点信息对象
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>