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

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

View File

@@ -1,11 +1,11 @@
<template>
<view class="cl-tree">
<view class="cl-tree" :class="[pt.className]">
<cl-tree-item
v-for="item in data"
:key="item.id"
:item="item"
:level="0"
:pt="pt"
:pt="props.pt"
></cl-tree-item>
</view>
</template>
@@ -13,6 +13,7 @@
<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"
@@ -23,6 +24,11 @@ const props = defineProps({
type: Object as PropType<any>,
default: () => ({})
},
// 绑定值
modelValue: {
type: [Array, String, Number] as PropType<any | null>,
default: null
},
// 树形结构数据
list: {
type: Array as PropType<ClTreeItem[]>,
@@ -38,18 +44,33 @@ const props = defineProps({
type: String,
default: "arrow-down-s-fill"
},
// 是否显示复选框
showCheckbox: {
type: Boolean,
default: false
},
// 是否严格的遵循父子不互相关联
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);
@@ -266,19 +287,27 @@ 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.checkStrictly) {
// 设置所有子孙节点的选中状态
const descendants = getDescendants(key);
for (let i = 0; i < descendants.length; i++) {
descendants[i].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);
// 更新所有祖先节点的状态
updateAncestorsCheckState(key);
}
}
}
@@ -386,11 +415,6 @@ function setExpanded(key: string | number, flag: boolean): void {
* @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]);
@@ -445,29 +469,96 @@ function expandAll(): void {
/**
* 收起所有节点
*/
function collapseAll(): void {
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) {
// 自动更新选中状态
if (!props.checkStrictly && props.multiple) {
updateAllCheckStates();
}
// 更新绑定值
syncModelValue();
},
{ deep: true }
);
@@ -475,8 +566,9 @@ watch(
defineExpose({
icon: computed(() => props.icon),
expandIcon: computed(() => props.expandIcon),
showCheckbox: computed(() => props.showCheckbox),
checkStrictly: computed(() => props.checkStrictly),
checkable: computed(() => props.checkable),
multiple: computed(() => props.multiple),
clearChecked,
setChecked,
setCheckedKeys,

View File

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