添加 cl-tree 树形组件

This commit is contained in:
icssoa
2025-09-01 01:23:29 +08:00
parent e356244639
commit 73a472053f
12 changed files with 1137 additions and 3 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "cool-unix", "name": "cool-unix",
"version": "8.0.19", "version": "8.0.20",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build-ui": "node ./uni_modules/cool-ui/scripts/generate-types.js", "build-ui": "node ./uni_modules/cool-ui/scripts/generate-types.js",

View File

@@ -316,6 +316,12 @@
"navigationBarTitleText": "FilterBar 筛选栏" "navigationBarTitleText": "FilterBar 筛选栏"
} }
}, },
{
"path": "data/tree",
"style": {
"navigationBarTitleText": "Tree 树形结构"
}
},
{ {
"path": "status/badge", "path": "status/badge",
"style": { "style": {

373
pages/demo/data/tree.uvue Normal file
View File

@@ -0,0 +1,373 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('树形结构')">
<cl-tree
ref="treeRef"
:list="list"
:icon="isCustomIcon ? 'add-circle-line' : 'arrow-right-s-fill'"
:expand-icon="isCustomIcon ? 'indeterminate-circle-line' : 'arrow-down-s-fill'"
:show-checkbox="true"
:check-strictly="checkStrictly"
></cl-tree>
<cl-list border :pt="{ className: 'mt-5' }">
<cl-list-item :label="t('父子关联')">
<cl-switch v-model="checkStrictly"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('换个图标')">
<cl-switch v-model="isCustomIcon"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
<demo-item :label="t('选中操作')">
<view class="mb-2">
<cl-button @tap="setChecked">{{ t("选中部分节点") }}</cl-button>
</view>
<view class="mb-2">
<cl-button @tap="getChecked">{{ t("获取选中节点") }}</cl-button>
<cl-text
v-if="checkedKeys.length > 0"
:pt="{
className: '!text-sm p-2'
}"
>{{ checkedKeys.join(", ") }}</cl-text
>
</view>
<view class="mb-2">
<cl-button @tap="getHalfChecked">{{ t("获取半选节点") }}</cl-button>
<cl-text
v-if="halfCheckedKeys.length > 0"
:pt="{
className: '!text-sm p-2'
}"
>{{ halfCheckedKeys.join(", ") }}</cl-text
>
</view>
<view class="mb-2">
<cl-button @tap="clearChecked">{{ t("清空选中") }}</cl-button>
</view>
</demo-item>
<demo-item :label="t('展开操作')">
<view class="mb-2">
<cl-button @tap="expand">{{ t("展开部分节点") }}</cl-button>
</view>
<view class="mb-2">
<cl-button @tap="getExpanded">{{ t("获取展开节点") }}</cl-button>
<cl-text
v-if="expandedKeys.length > 0"
:pt="{
className: '!text-sm p-2'
}"
>{{ expandedKeys.join(", ") }}</cl-text
>
</view>
<view class="mb-2">
<cl-button @tap="expandAll">{{ t("展开所有") }}</cl-button>
</view>
<view class="mb-2">
<cl-button @tap="collapseAll">{{ t("收起所有") }}</cl-button>
</view>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import DemoItem from "../components/item.uvue";
import { t } from "@/locale";
import { ref } from "vue";
import { useTree } from "@/uni_modules/cool-ui";
const list = useTree([
{
id: "1",
label: "华为",
children: [
{
id: "1-1",
label: "手机",
children: [
{
id: "1-1-1",
label: "Mate系列",
children: [
{
id: "1-1-1-1",
label: "Mate 50"
},
{
id: "1-1-1-2",
label: "Mate 40"
},
{
id: "1-1-1-3",
label: "Mate 30"
}
]
},
{
id: "1-1-2",
label: "P系列",
children: [
{
id: "1-1-2-1",
label: "P60"
},
{
id: "1-1-2-2",
label: "P50"
},
{
id: "1-1-2-3",
label: "P40"
}
]
}
]
},
{
id: "1-2",
label: "笔记本",
children: [
{
id: "1-2-1",
label: "MateBook X",
children: [
{
id: "1-2-1-1",
label: "MateBook X Pro"
},
{
id: "1-2-1-2",
label: "MateBook X 2022"
}
]
},
{
id: "1-2-2",
label: "MateBook D",
children: [
{
id: "1-2-2-1",
label: "MateBook D 14"
},
{
id: "1-2-2-2",
label: "MateBook D 15"
}
]
},
{
id: "1-2-3",
label: "MateBook 13"
}
]
}
]
},
{
id: "2",
label: "小米",
children: [
{
id: "2-1",
label: "手机",
children: [
{
id: "2-1-1",
label: "小米数字系列"
},
{
id: "2-1-2",
label: "Redmi系列"
}
]
},
{
id: "2-2",
label: "家电",
children: [
{
id: "2-2-1",
label: "电视"
},
{
id: "2-2-2",
label: "空调"
}
]
}
]
},
{
id: "3",
label: "苹果",
children: [
{
id: "3-1",
label: "手机",
children: [
{
id: "3-1-1",
label: "iPhone 14"
},
{
id: "3-1-2",
label: "iPhone 13"
}
]
},
{
id: "3-2",
label: "平板",
children: [
{
id: "3-2-1",
label: "iPad Pro"
},
{
id: "3-2-2",
label: "iPad Air"
}
]
}
]
},
{
id: "4",
label: "OPPO",
children: [
{
id: "4-1",
label: "手机",
children: [
{
id: "4-1-1",
label: "Find系列"
},
{
id: "4-1-2",
label: "Reno系列"
}
]
},
{
id: "4-2",
label: "配件",
children: [
{
id: "4-2-1",
label: "耳机"
},
{
id: "4-2-2",
label: "手环"
}
]
}
]
},
{
id: "5",
label: "vivo",
children: [
{
id: "5-1",
label: "手机",
children: [
{
id: "5-1-1",
label: "X系列"
},
{
id: "5-1-2",
label: "S系列"
}
]
},
{
id: "5-2",
label: "智能设备",
children: [
{
id: "5-2-1",
label: "手表"
},
{
id: "5-2-2",
label: "耳机"
}
]
}
]
}
]);
// 是否严格的遵循父子不互相关联
const checkStrictly = ref(false);
// 是否自定义图标
const isCustomIcon = ref(false);
// 树形组件引用
const treeRef = ref<ClTreeComponentPublicInstance | null>(null);
// 选中节点的keys
const checkedKeys = ref<(string | number)[]>([]);
// 半选节点的keys
const halfCheckedKeys = ref<(string | number)[]>([]);
// 展开节点的keys
const expandedKeys = ref<(string | number)[]>([]);
// 演示方法
function setChecked() {
treeRef.value!.setCheckedKeys(["1-1", "2"]);
}
function getChecked() {
checkedKeys.value = treeRef.value!.getCheckedKeys();
}
function clearChecked() {
treeRef.value!.clearChecked();
checkedKeys.value = [];
halfCheckedKeys.value = [];
}
function getHalfChecked() {
halfCheckedKeys.value = treeRef.value!.getHalfCheckedKeys();
}
function expand() {
treeRef.value!.setExpandedKeys(["1", "1-1", "2"]);
}
function getExpanded() {
expandedKeys.value = treeRef.value!.getExpandedKeys();
}
function expandAll() {
treeRef.value!.expandAll();
expandedKeys.value = treeRef.value!.getExpandedKeys();
}
function collapseAll() {
treeRef.value!.collapseAll();
expandedKeys.value = [];
}
</script>

View File

@@ -322,6 +322,11 @@ const data = computed<Item[]>(() => {
label: t("筛选栏"), label: t("筛选栏"),
icon: "filter-line", icon: "filter-line",
path: "/pages/demo/data/filter-bar" path: "/pages/demo/data/filter-bar"
},
{
label: t("树形结构"),
icon: "node-tree",
path: "/pages/demo/data/tree"
} }
] ]
}, },

View File

@@ -0,0 +1,179 @@
<template>
<view class="cl-tree-item__wrapper" :class="[`cl-tree-item--level-${level}`, pt.className]">
<view
class="cl-tree-item"
:class="[
{
'is-expand': hover,
'is-dark': isDark
},
pt.className
]"
@touchstart="onTouchStart"
@touchend="onTouchEnd"
@touchcancel="onTouchEnd"
>
<view class="cl-tree-item__expand" :class="[pt.expand?.className]">
<cl-icon
:name="icon"
:size="pt.expandIcon?.size ?? 36"
:color="pt.expandIcon?.color"
:pt="{
className: pt.expandIcon?.className
}"
v-if="hasChildren"
></cl-icon>
</view>
<cl-text
:pt="{
className: parseClass(['flex-1 mx-1', pt.label?.className])
}"
>{{ item.label }}</cl-text
>
<template v-if="ClTree?.showCheckbox">
<view
class="cl-tree-item__checkbox"
:class="[pt.checkbox?.className]"
@touchstart.stop="toChecked"
>
<cl-icon
v-if="item.isChecked"
:name="pt.checkedIcon?.name ?? 'checkbox-circle-fill'"
:size="pt.checkedIcon?.size ?? 38"
:color="pt.checkedIcon?.color ?? 'primary'"
></cl-icon>
<cl-icon
v-else-if="item.isHalfChecked"
:name="pt.halfCheckedIcon?.name ?? 'indeterminate-circle-line'"
:size="pt.halfCheckedIcon?.size ?? 38"
:color="pt.halfCheckedIcon?.color ?? 'primary'"
></cl-icon>
<cl-icon
v-else
:name="pt.uncheckedIcon?.name ?? 'checkbox-blank-circle-line'"
:size="pt.uncheckedIcon?.size ?? 38"
:color="pt.uncheckedIcon?.color ?? 'info'"
></cl-icon>
</view>
</template>
</view>
<template v-if="hasChildren && item.isExpand == true">
<cl-tree-item
v-for="item in item.children"
:key="item.id"
:item="item"
:level="level + 1"
:pt="props.pt"
></cl-tree-item>
</template>
</view>
</template>
<script lang="ts" setup>
import { computed, ref, type PropType } from "vue";
import { isDark, parseClass, parsePt, useParent } from "@/cool";
import type { ClTreeItem, PassThroughProps } from "../../types";
import type { ClIconProps } from "../cl-icon/props";
defineOptions({
name: "cl-tree-item"
});
const props = defineProps({
pt: {
type: Object,
default: () => ({})
},
item: {
type: Object as PropType<ClTreeItem>,
default: () => ({})
},
level: {
type: Number,
default: 0
}
});
// 透传属性类型定义,支持自定义各部分样式和图标
type PassThrough = {
className?: string; // 外层自定义类名
wrapper?: PassThroughProps; // 外层包裹属性
expand?: PassThroughProps; // 展开区域属性
expandIcon?: ClIconProps; // 展开图标属性
checkbox?: PassThroughProps; // 复选框区域属性
checkedIcon?: ClIconProps; // 选中图标属性
halfCheckedIcon?: ClIconProps; // 半选图标属性
uncheckedIcon?: ClIconProps; // 未选中图标属性
label?: PassThroughProps; // 标签属性
};
// 解析pt透传属性便于自定义样式和图标
const pt = computed(() => parsePt<PassThrough>(props.pt));
// 获取父级cl-tree组件实例用于调用树的相关方法
const ClTree = useParent<ClTreeComponentPublicInstance>("cl-tree");
// 判断当前节点是否有子节点
const hasChildren = computed(() => props.item.children != null && props.item.children.length > 0);
// 计算当前节点应显示的图标(展开/收起)
const icon = computed(() => {
if (ClTree == null) {
return "";
}
return props.item.isExpand == true ? ClTree.expandIcon : ClTree.icon;
});
// 切换当前节点的展开状态
function toExpand() {
ClTree!.setExpanded(props.item.id, !(props.item.isExpand ?? false));
}
// 切换当前节点的选中状态
function toChecked() {
ClTree!.setChecked(props.item.id, !(props.item.isChecked ?? false));
}
// 控制节点按下时的hover状态
const hover = ref(false);
// 触摸开始时触发设置hover并展开/收起
function onTouchStart() {
hover.value = true;
toExpand();
}
// 触摸结束时触发取消hover
function onTouchEnd() {
hover.value = false;
}
</script>
<style lang="scss" scoped>
.cl-tree-item__wrapper {
@apply pl-8;
}
.cl-tree-item {
@apply flex flex-row items-center w-full p-2 rounded-lg;
&__expand {
@apply w-6 flex items-center justify-center;
}
&.is-expand {
@apply bg-surface-50;
&.is-dark {
@apply bg-surface-700;
}
}
&--level-0 {
@apply pl-0;
}
}
</style>

View File

@@ -0,0 +1,21 @@
import type { ClTreeItem, PassThroughProps } from "../../types";
import type { ClIconProps } from "../cl-icon/props";
export type ClTreeItemPassThrough = {
className?: string;
wrapper?: PassThroughProps;
expand?: PassThroughProps;
expandIcon?: ClIconProps;
checkbox?: PassThroughProps;
checkedIcon?: ClIconProps;
halfCheckedIcon?: ClIconProps;
uncheckedIcon?: ClIconProps;
label?: PassThroughProps;
};
export type ClTreeItemProps = {
className?: string;
pt?: ClTreeItemPassThrough;
item?: ClTreeItem;
level?: number;
};

View File

@@ -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为节点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; // 节点不存在则返回
// 设置当前节点选中状态
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>

View File

@@ -0,0 +1,11 @@
import type { ClTreeItem, ClTreeNodeInfo } from "../../types";
export type ClTreeProps = {
className?: string;
pt?: any;
list?: ClTreeItem[];
icon?: string;
expandIcon?: string;
showCheckbox?: boolean;
checkStrictly?: boolean;
};

View File

@@ -1,5 +1,5 @@
import { parse } from "@/cool"; import { parse } from "@/cool";
import type { ClCascaderOption, ClListViewItem } from "../types"; import type { ClCascaderOption, ClListViewItem, ClTreeItem } from "../types";
export function useListView(data: UTSJSONObject[]) { export function useListView(data: UTSJSONObject[]) {
return data.map((e) => { return data.map((e) => {
@@ -13,3 +13,12 @@ export function useListView(data: UTSJSONObject[]) {
export function useCascader(data: UTSJSONObject[]) { export function useCascader(data: UTSJSONObject[]) {
return data.map((e) => parse<ClCascaderOption>(e)!); return data.map((e) => parse<ClCascaderOption>(e)!);
} }
export function useTree(data: UTSJSONObject[]) {
return data.map((e) => {
return parse<ClTreeItem>({
...e,
value: e
})!;
});
}

View File

@@ -1,4 +1,4 @@
import type { ClActionSheetItem, ClActionSheetOptions, PassThroughProps, Type, ClButtonType, Size, ClListViewItem, ClFilterItemType, ClSelectOption, ClFormLabelPosition, ClFormRule, ClFormValidateError, ClInputType, ClListItem, Justify, ClListViewGroup, ClListViewVirtualItem, ClListViewRefresherStatus, ClConfirmAction, ClConfirmOptions, ClToastOptions, ClPopupDirection, ClQrcodeMode, ClSelectDateShortcut, ClTabsItem, ClTextType, ClUploadItem } from "./types"; import type { ClActionSheetItem, ClActionSheetOptions, PassThroughProps, Type, ClButtonType, Size, ClListViewItem, ClFilterItemType, ClSelectOption, ClFormLabelPosition, ClFormRule, ClFormValidateError, ClInputType, ClListItem, Justify, ClListViewGroup, ClListViewVirtualItem, ClListViewRefresherStatus, ClConfirmAction, ClConfirmOptions, ClToastOptions, ClPopupDirection, ClQrcodeMode, ClSelectDateShortcut, ClTabsItem, ClTextType, ClTreeItem, ClTreeNodeInfo, ClUploadItem } from "./types";
import { type UiInstance } from "./hooks"; import { type UiInstance } from "./hooks";
import { type QrcodeOptions } from "./draw"; import { type QrcodeOptions } from "./draw";
@@ -67,6 +67,8 @@ import type { ClTimelineProps, ClTimelinePassThrough } from "./components/cl-tim
import type { ClTimelineItemProps, ClTimelineItemPassThrough } from "./components/cl-timeline-item/props"; import type { ClTimelineItemProps, ClTimelineItemPassThrough } from "./components/cl-timeline-item/props";
import type { ClToastProps } from "./components/cl-toast/props"; import type { ClToastProps } from "./components/cl-toast/props";
import type { ClTopbarProps, ClTopbarPassThrough } from "./components/cl-topbar/props"; import type { ClTopbarProps, ClTopbarPassThrough } from "./components/cl-topbar/props";
import type { ClTreeProps } from "./components/cl-tree/props";
import type { ClTreeItemProps, ClTreeItemPassThrough } from "./components/cl-tree-item/props";
import type { ClUploadProps, ClUploadPassThrough } from "./components/cl-upload/props"; import type { ClUploadProps, ClUploadPassThrough } from "./components/cl-upload/props";
import type { ClWaterfallProps, ClWaterfallPassThrough } from "./components/cl-waterfall/props"; import type { ClWaterfallProps, ClWaterfallPassThrough } from "./components/cl-waterfall/props";
@@ -140,6 +142,8 @@ declare module "vue" {
"cl-timeline-item": (typeof import('./components/cl-timeline-item/cl-timeline-item.uvue')['default']) & import('vue').DefineComponent<ClTimelineItemProps>; "cl-timeline-item": (typeof import('./components/cl-timeline-item/cl-timeline-item.uvue')['default']) & import('vue').DefineComponent<ClTimelineItemProps>;
"cl-toast": (typeof import('./components/cl-toast/cl-toast.uvue')['default']) & import('vue').DefineComponent<ClToastProps>; "cl-toast": (typeof import('./components/cl-toast/cl-toast.uvue')['default']) & import('vue').DefineComponent<ClToastProps>;
"cl-topbar": (typeof import('./components/cl-topbar/cl-topbar.uvue')['default']) & import('vue').DefineComponent<ClTopbarProps>; "cl-topbar": (typeof import('./components/cl-topbar/cl-topbar.uvue')['default']) & import('vue').DefineComponent<ClTopbarProps>;
"cl-tree": (typeof import('./components/cl-tree/cl-tree.uvue')['default']) & import('vue').DefineComponent<ClTreeProps>;
"cl-tree-item": (typeof import('./components/cl-tree-item/cl-tree-item.uvue')['default']) & import('vue').DefineComponent<ClTreeItemProps>;
"cl-upload": (typeof import('./components/cl-upload/cl-upload.uvue')['default']) & import('vue').DefineComponent<ClUploadProps>; "cl-upload": (typeof import('./components/cl-upload/cl-upload.uvue')['default']) & import('vue').DefineComponent<ClUploadProps>;
"cl-waterfall": (typeof import('./components/cl-waterfall/cl-waterfall.uvue')['default']) & import('vue').DefineComponent<ClWaterfallProps>; "cl-waterfall": (typeof import('./components/cl-waterfall/cl-waterfall.uvue')['default']) & import('vue').DefineComponent<ClWaterfallProps>;
} }

View File

@@ -206,3 +206,21 @@ declare type ClSlideVerifyComponentPublicInstance = {
init: () => void; init: () => void;
reset: () => void; reset: () => void;
}; };
declare type ClTreeComponentPublicInstance = {
icon: string;
expandIcon: string;
showCheckbox: boolean;
checkStrictly: boolean;
accordion: boolean;
clearChecked: () => void;
setChecked: (key: string | number, flag: boolean) => void;
setCheckedKeys: (keys: (string | number)[]) => void;
getCheckedKeys: () => (string | number)[];
getHalfCheckedKeys: () => (string | number)[];
setExpanded: (key: string | number, flag: boolean) => void;
setExpandedKeys: (keys: (string | number)[]) => void;
getExpandedKeys: () => (string | number)[];
expandAll: () => void;
collapseAll: () => void;
};

View File

@@ -181,3 +181,20 @@ export type ClFilterItem = {
type: ClFilterItemType; type: ClFilterItemType;
options?: ClSelectOption[]; options?: ClSelectOption[];
}; };
export type ClTreeItem = {
id: string | number;
label: string;
disabled?: boolean;
children?: ClTreeItem[];
value?: UTSJSONObject;
isExpand?: boolean;
isChecked?: boolean;
isHalfChecked?: boolean;
};
export type ClTreeNodeInfo = {
node: ClTreeItem;
parent?: ClTreeItem;
index: number;
};