cl-tree 添加 modelValue、multiple 参数

This commit is contained in:
icssoa
2025-09-01 12:03:07 +08:00
parent b18bbd007b
commit f6bcf5755a
4 changed files with 360 additions and 219 deletions

View File

@@ -3,11 +3,13 @@
<view class="p-3"> <view class="p-3">
<demo-item :label="t('树形结构')"> <demo-item :label="t('树形结构')">
<cl-tree <cl-tree
v-model="checkedKeys"
ref="treeRef" ref="treeRef"
:list="list" :list="list"
:icon="isCustomIcon ? 'add-circle-line' : 'arrow-right-s-fill'" :icon="isCustomIcon ? 'add-circle-line' : 'arrow-right-s-fill'"
:expand-icon="isCustomIcon ? 'indeterminate-circle-line' : 'arrow-down-s-fill'" :expand-icon="isCustomIcon ? 'indeterminate-circle-line' : 'arrow-down-s-fill'"
:show-checkbox="true" :checkable="true"
:multiple="true"
:check-strictly="checkStrictly" :check-strictly="checkStrictly"
></cl-tree> ></cl-tree>
@@ -22,6 +24,10 @@
</cl-list> </cl-list>
</demo-item> </demo-item>
<demo-item :label="t('选中值')">
<cl-text>{{ checkedKeys.join("、") }}</cl-text>
</demo-item>
<demo-item :label="t('选中操作')"> <demo-item :label="t('选中操作')">
<view class="mb-2"> <view class="mb-2">
<cl-button @tap="setChecked">{{ t("选中部分节点") }}</cl-button> <cl-button @tap="setChecked">{{ t("选中部分节点") }}</cl-button>
@@ -29,14 +35,6 @@
<view class="mb-2"> <view class="mb-2">
<cl-button @tap="getChecked">{{ t("获取选中节点") }}</cl-button> <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>
<view class="mb-2"> <view class="mb-2">
@@ -47,7 +45,7 @@
:pt="{ :pt="{
className: '!text-sm p-2' className: '!text-sm p-2'
}" }"
>{{ halfCheckedKeys.join(", ") }}</cl-text >{{ halfCheckedKeys.join("") }}</cl-text
> >
</view> </view>
@@ -69,7 +67,7 @@
:pt="{ :pt="{
className: '!text-sm p-2' className: '!text-sm p-2'
}" }"
>{{ expandedKeys.join(", ") }}</cl-text >{{ expandedKeys.join("") }}</cl-text
> >
</view> </view>
@@ -89,9 +87,17 @@
import DemoItem from "../components/item.uvue"; import DemoItem from "../components/item.uvue";
import { t } from "@/locale"; import { t } from "@/locale";
import { ref } from "vue"; import { ref } from "vue";
import { useTree } from "@/uni_modules/cool-ui"; import { useTree, useUi, type ClTreeItem } from "@/uni_modules/cool-ui";
const list = useTree([ const ui = useUi();
const list = ref<ClTreeItem[]>([]);
function refresh() {
ui.showLoading();
setTimeout(() => {
list.value = useTree([
{ {
id: "1", id: "1",
label: "华为", label: "华为",
@@ -110,6 +116,7 @@ const list = useTree([
}, },
{ {
id: "1-1-1-2", id: "1-1-1-2",
disabled: true,
label: "Mate 40" label: "Mate 40"
}, },
{ {
@@ -124,6 +131,7 @@ const list = useTree([
children: [ children: [
{ {
id: "1-1-2-1", id: "1-1-2-1",
disabled: true,
label: "P60" label: "P60"
}, },
{ {
@@ -181,6 +189,7 @@ const list = useTree([
{ {
id: "2", id: "2",
label: "小米", label: "小米",
isExpand: true,
children: [ children: [
{ {
id: "2-1", id: "2-1",
@@ -314,7 +323,11 @@ const list = useTree([
} }
] ]
} }
]); ]);
ui.hideLoading();
}, 500);
}
// 是否严格的遵循父子不互相关联 // 是否严格的遵循父子不互相关联
const checkStrictly = ref(false); const checkStrictly = ref(false);
@@ -326,7 +339,8 @@ const isCustomIcon = ref(false);
const treeRef = ref<ClTreeComponentPublicInstance | null>(null); const treeRef = ref<ClTreeComponentPublicInstance | null>(null);
// 选中节点的keys // 选中节点的keys
const checkedKeys = ref<(string | number)[]>([]); const checkedKeys = ref<(string | number)[]>(["1-1-1-1", "2-1-1", "2-1-2"]);
const checkedKeys2 = ref<string | null>("1-1-1");
// 半选节点的keys // 半选节点的keys
const halfCheckedKeys = ref<(string | number)[]>([]); const halfCheckedKeys = ref<(string | number)[]>([]);
@@ -354,7 +368,7 @@ function getHalfChecked() {
} }
function expand() { function expand() {
treeRef.value!.setExpandedKeys(["1", "1-1", "2"]); treeRef.value!.setExpandedKeys(["4", "5"]);
} }
function getExpanded() { function getExpanded() {
@@ -370,4 +384,8 @@ function collapseAll() {
treeRef.value!.collapseAll(); treeRef.value!.collapseAll();
expandedKeys.value = []; expandedKeys.value = [];
} }
onReady(() => {
refresh();
});
</script> </script>

View File

@@ -1,14 +1,22 @@
<template> <template>
<view class="cl-tree-item__wrapper" :class="[`cl-tree-item--level-${level}`, pt.className]"> <view class="cl-tree-item-wrapper" :class="[pt.itemWrapper?.className]">
<view <view
class="cl-tree-item" class="cl-tree-item"
:class="[ :class="[
{ {
'is-expand': hover, 'is-expand': hover,
'is-dark': isDark 'is-dark': isDark,
'is-checked': item.isChecked == true && ClTree?.checkable == true,
'is-half-checked': item.isHalfChecked,
'is-disabled': item.disabled,
'is-multiple': ClTree?.multiple
}, },
pt.className pt.item?.className,
item.isChecked == true ? pt.itemChecked?.className : ''
]" ]"
:style="{
paddingLeft: `${level * 50 + 16}rpx`
}"
@touchstart="onTouchStart" @touchstart="onTouchStart"
@touchend="onTouchEnd" @touchend="onTouchEnd"
@touchcancel="onTouchEnd" @touchcancel="onTouchEnd"
@@ -16,7 +24,7 @@
<view class="cl-tree-item__expand" :class="[pt.expand?.className]"> <view class="cl-tree-item__expand" :class="[pt.expand?.className]">
<cl-icon <cl-icon
:name="icon" :name="icon"
:size="pt.expandIcon?.size ?? 36" :size="pt.expandIcon?.size ?? 34"
:color="pt.expandIcon?.color" :color="pt.expandIcon?.color"
:pt="{ :pt="{
className: pt.expandIcon?.className className: pt.expandIcon?.className
@@ -32,7 +40,7 @@
>{{ item.label }}</cl-text >{{ item.label }}</cl-text
> >
<template v-if="ClTree?.showCheckbox"> <template v-if="showCheckbox">
<view <view
class="cl-tree-item__checkbox" class="cl-tree-item__checkbox"
:class="[pt.checkbox?.className]" :class="[pt.checkbox?.className]"
@@ -99,8 +107,9 @@ const props = defineProps({
// 透传属性类型定义,支持自定义各部分样式和图标 // 透传属性类型定义,支持自定义各部分样式和图标
type PassThrough = { type PassThrough = {
className?: string; // 外层自定义类名 item?: PassThroughProps; // 自定义类名
wrapper?: PassThroughProps; // 外层包裹属性 itemChecked?: PassThroughProps; // 选中状态属性
itemWrapper?: PassThroughProps; // 外层包裹属性
expand?: PassThroughProps; // 展开区域属性 expand?: PassThroughProps; // 展开区域属性
expandIcon?: ClIconProps; // 展开图标属性 expandIcon?: ClIconProps; // 展开图标属性
checkbox?: PassThroughProps; // 复选框区域属性 checkbox?: PassThroughProps; // 复选框区域属性
@@ -119,6 +128,11 @@ const ClTree = useParent<ClTreeComponentPublicInstance>("cl-tree");
// 判断当前节点是否有子节点 // 判断当前节点是否有子节点
const hasChildren = computed(() => props.item.children != null && props.item.children.length > 0); const hasChildren = computed(() => props.item.children != null && props.item.children.length > 0);
// 判断当前节点是否显示复选框
const showCheckbox = computed(() => {
return ClTree?.checkable == true && ClTree?.multiple == true;
});
// 计算当前节点应显示的图标(展开/收起) // 计算当前节点应显示的图标(展开/收起)
const icon = computed(() => { const icon = computed(() => {
if (ClTree == null) { if (ClTree == null) {
@@ -134,6 +148,10 @@ function toExpand() {
// 切换当前节点的选中状态 // 切换当前节点的选中状态
function toChecked() { function toChecked() {
if (props.item.disabled == true) {
return;
}
ClTree!.setChecked(props.item.id, !(props.item.isChecked ?? false)); ClTree!.setChecked(props.item.id, !(props.item.isChecked ?? false));
} }
@@ -144,6 +162,10 @@ const hover = ref(false);
function onTouchStart() { function onTouchStart() {
hover.value = true; hover.value = true;
toExpand(); toExpand();
if (ClTree?.checkable == true && ClTree?.multiple != true && props.item.disabled != true) {
toChecked();
}
} }
// 触摸结束时触发取消hover // 触摸结束时触发取消hover
@@ -153,12 +175,9 @@ function onTouchEnd() {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.cl-tree-item__wrapper {
@apply pl-8;
}
.cl-tree-item { .cl-tree-item {
@apply flex flex-row items-center w-full p-2 rounded-lg; @apply flex flex-row items-center w-full rounded-lg;
padding: 16rpx;
&__expand { &__expand {
@apply w-6 flex items-center justify-center; @apply w-6 flex items-center justify-center;
@@ -172,8 +191,20 @@ function onTouchEnd() {
} }
} }
&--level-0 { &.is-disabled {
@apply pl-0; @apply opacity-50;
}
&.is-checked {
@apply bg-primary-100;
&.is-multiple {
@apply bg-transparent;
}
&.is-dark {
@apply bg-primary-500;
}
} }
} }
</style> </style>

View File

@@ -1,11 +1,11 @@
<template> <template>
<view class="cl-tree"> <view class="cl-tree" :class="[pt.className]">
<cl-tree-item <cl-tree-item
v-for="item in data" v-for="item in data"
:key="item.id" :key="item.id"
:item="item" :item="item"
:level="0" :level="0"
:pt="pt" :pt="props.pt"
></cl-tree-item> ></cl-tree-item>
</view> </view>
</template> </template>
@@ -13,6 +13,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch, ref, type PropType } from "vue"; import { computed, watch, ref, type PropType } from "vue";
import type { ClTreeItem, ClTreeNodeInfo } from "../../types"; import type { ClTreeItem, ClTreeNodeInfo } from "../../types";
import { first, isEmpty, isEqual, parsePt } from "@/cool";
defineOptions({ defineOptions({
name: "cl-tree" name: "cl-tree"
@@ -23,6 +24,11 @@ const props = defineProps({
type: Object as PropType<any>, type: Object as PropType<any>,
default: () => ({}) default: () => ({})
}, },
// 绑定值
modelValue: {
type: [Array, String, Number] as PropType<any | null>,
default: null
},
// 树形结构数据 // 树形结构数据
list: { list: {
type: Array as PropType<ClTreeItem[]>, type: Array as PropType<ClTreeItem[]>,
@@ -38,18 +44,33 @@ const props = defineProps({
type: String, type: String,
default: "arrow-down-s-fill" default: "arrow-down-s-fill"
}, },
// 是否显示复选框
showCheckbox: {
type: Boolean,
default: false
},
// 是否严格的遵循父子不互相关联 // 是否严格的遵循父子不互相关联
checkStrictly: { checkStrictly: {
type: Boolean, type: Boolean,
default: false 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); const data = ref<ClTreeItem[]>(props.list);
@@ -266,9 +287,16 @@ function setChecked(key: string | number, flag: boolean): void {
const nodeInfo = findNodeInfo(key); // 查找节点信息 const nodeInfo = findNodeInfo(key); // 查找节点信息
if (nodeInfo == null) return; // 节点不存在则返回 if (nodeInfo == null) return; // 节点不存在则返回
// 非多选模式下,清空所有选中状态
if (!props.multiple) {
clearChecked();
}
// 设置当前节点选中状态 // 设置当前节点选中状态
nodeInfo.node.isChecked = flag; nodeInfo.node.isChecked = flag;
// 多选模式下处理
if (props.multiple) {
// 非严格模式下处理父子联动 // 非严格模式下处理父子联动
if (!props.checkStrictly) { if (!props.checkStrictly) {
// 设置所有子孙节点的选中状态 // 设置所有子孙节点的选中状态
@@ -280,6 +308,7 @@ function setChecked(key: string | number, flag: boolean): void {
// 更新所有祖先节点的状态 // 更新所有祖先节点的状态
updateAncestorsCheckState(key); updateAncestorsCheckState(key);
} }
}
} }
/** /**
@@ -386,11 +415,6 @@ function setExpanded(key: string | number, flag: boolean): void {
* @param keys 需要展开的节点id数组 * @param keys 需要展开的节点id数组
*/ */
function setExpandedKeys(keys: (string | number)[]): void { function setExpandedKeys(keys: (string | number)[]): void {
// 先重置所有节点为收起状态
nodeMap.value.forEach((info: ClTreeNodeInfo) => {
info.node.isExpand = false;
});
// 设置指定节点为展开状态 // 设置指定节点为展开状态
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const nodeInfo = findNodeInfo(keys[i]); const nodeInfo = findNodeInfo(keys[i]);
@@ -445,29 +469,96 @@ function expandAll(): void {
/** /**
* 收起所有节点 * 收起所有节点
*/ */
function collapseAll(): void { function collapseAll() {
// 遍历所有节点将isExpand设为false // 遍历所有节点将isExpand设为false
nodeMap.value.forEach((info: ClTreeNodeInfo) => { nodeMap.value.forEach((info: ClTreeNodeInfo) => {
info.node.isExpand = false; 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变化同步到内部数据 // 监听props.list变化同步到内部数据
watch( watch(
computed(() => props.list), computed(() => props.list),
(val: ClTreeItem[]) => { (val: ClTreeItem[]) => {
data.value = val; data.value = val;
// 检查选中状态
syncCheckedState();
},
{ immediate: true }
);
// 监听modelValue变化
watch(
computed(() => [props.modelValue ?? 0]),
() => {
syncCheckedState();
}, },
{ immediate: true, deep: true } { immediate: true, deep: true }
); );
// 监听树数据变化,自动更新选中状态 // 监听树数据变化
watch( watch(
data, data,
() => { () => {
if (!props.checkStrictly) { // 自动更新选中状态
if (!props.checkStrictly && props.multiple) {
updateAllCheckStates(); updateAllCheckStates();
} }
// 更新绑定值
syncModelValue();
}, },
{ deep: true } { deep: true }
); );
@@ -475,8 +566,9 @@ watch(
defineExpose({ defineExpose({
icon: computed(() => props.icon), icon: computed(() => props.icon),
expandIcon: computed(() => props.expandIcon), expandIcon: computed(() => props.expandIcon),
showCheckbox: computed(() => props.showCheckbox),
checkStrictly: computed(() => props.checkStrictly), checkStrictly: computed(() => props.checkStrictly),
checkable: computed(() => props.checkable),
multiple: computed(() => props.multiple),
clearChecked, clearChecked,
setChecked, setChecked,
setCheckedKeys, setCheckedKeys,

View File

@@ -210,9 +210,9 @@ declare type ClSlideVerifyComponentPublicInstance = {
declare type ClTreeComponentPublicInstance = { declare type ClTreeComponentPublicInstance = {
icon: string; icon: string;
expandIcon: string; expandIcon: string;
showCheckbox: boolean; checkable: boolean;
multiple: boolean;
checkStrictly: boolean; checkStrictly: boolean;
accordion: boolean;
clearChecked: () => void; clearChecked: () => void;
setChecked: (key: string | number, flag: boolean) => void; setChecked: (key: string | number, flag: boolean) => void;
setCheckedKeys: (keys: (string | number)[]) => void; setCheckedKeys: (keys: (string | number)[]) => void;