Просмотр исходного кода

cl-tree 添加 modelValue、multiple 参数

icssoa 7 месяцев назад
Родитель
Сommit
f6bcf5755a

+ 190 - 172
pages/demo/data/tree.uvue

@@ -3,11 +3,13 @@
 		<view class="p-3">
 			<demo-item :label="t('树形结构')">
 				<cl-tree
+					v-model="checkedKeys"
 					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"
+					:checkable="true"
+					:multiple="true"
 					:check-strictly="checkStrictly"
 				></cl-tree>
 
@@ -22,6 +24,10 @@
 				</cl-list>
 			</demo-item>
 
+			<demo-item :label="t('选中值')">
+				<cl-text>{{ checkedKeys.join("、") }}</cl-text>
+			</demo-item>
+
 			<demo-item :label="t('选中操作')">
 				<view class="mb-2">
 					<cl-button @tap="setChecked">{{ t("选中部分节点") }}</cl-button>
@@ -29,14 +35,6 @@
 
 				<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">
@@ -47,7 +45,7 @@
 						:pt="{
 							className: '!text-sm p-2'
 						}"
-						>{{ halfCheckedKeys.join(", ") }}</cl-text
+						>{{ halfCheckedKeys.join("") }}</cl-text
 					>
 				</view>
 
@@ -69,7 +67,7 @@
 						:pt="{
 							className: '!text-sm p-2'
 						}"
-						>{{ expandedKeys.join(", ") }}</cl-text
+						>{{ expandedKeys.join("") }}</cl-text
 					>
 				</view>
 
@@ -89,232 +87,247 @@
 import DemoItem from "../components/item.uvue";
 import { t } from "@/locale";
 import { ref } from "vue";
-import { useTree } from "@/uni_modules/cool-ui";
+import { useTree, useUi, type ClTreeItem } from "@/uni_modules/cool-ui";
+
+const ui = useUi();
 
-const list = useTree([
-	{
-		id: "1",
-		label: "华为",
-		children: [
+const list = ref<ClTreeItem[]>([]);
+
+function refresh() {
+	ui.showLoading();
+
+	setTimeout(() => {
+		list.value = useTree([
 			{
-				id: "1-1",
-				label: "手机",
+				id: "1",
+				label: "华为",
 				children: [
 					{
-						id: "1-1-1",
-						label: "Mate系列",
+						id: "1-1",
+						label: "手机",
 						children: [
 							{
-								id: "1-1-1-1",
-								label: "Mate 50"
-							},
-							{
-								id: "1-1-1-2",
-								label: "Mate 40"
+								id: "1-1-1",
+								label: "Mate系列",
+								children: [
+									{
+										id: "1-1-1-1",
+										label: "Mate 50"
+									},
+									{
+										id: "1-1-1-2",
+										disabled: true,
+										label: "Mate 40"
+									},
+									{
+										id: "1-1-1-3",
+										label: "Mate 30"
+									}
+								]
 							},
 							{
-								id: "1-1-1-3",
-								label: "Mate 30"
+								id: "1-1-2",
+								label: "P系列",
+								children: [
+									{
+										id: "1-1-2-1",
+										disabled: true,
+										label: "P60"
+									},
+									{
+										id: "1-1-2-2",
+										label: "P50"
+									},
+									{
+										id: "1-1-2-3",
+										label: "P40"
+									}
+								]
 							}
 						]
 					},
 					{
-						id: "1-1-2",
-						label: "P系列",
+						id: "1-2",
+						label: "笔记本",
 						children: [
 							{
-								id: "1-1-2-1",
-								label: "P60"
+								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-1-2-2",
-								label: "P50"
+								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-1-2-3",
-								label: "P40"
+								id: "1-2-3",
+								label: "MateBook 13"
 							}
 						]
 					}
 				]
 			},
 			{
-				id: "1-2",
-				label: "笔记本",
+				id: "2",
+				label: "小米",
+				isExpand: true,
 				children: [
 					{
-						id: "1-2-1",
-						label: "MateBook X",
+						id: "2-1",
+						label: "手机",
 						children: [
 							{
-								id: "1-2-1-1",
-								label: "MateBook X Pro"
+								id: "2-1-1",
+								label: "小米数字系列"
 							},
 							{
-								id: "1-2-1-2",
-								label: "MateBook X 2022"
+								id: "2-1-2",
+								label: "Redmi系列"
 							}
 						]
 					},
 					{
-						id: "1-2-2",
-						label: "MateBook D",
+						id: "2-2",
+						label: "家电",
 						children: [
 							{
-								id: "1-2-2-1",
-								label: "MateBook D 14"
+								id: "2-2-1",
+								label: "电视"
 							},
 							{
-								id: "1-2-2-2",
-								label: "MateBook D 15"
+								id: "2-2-2",
+								label: "空调"
 							}
 						]
-					},
-					{
-						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: "手机",
+				id: "3",
+				label: "苹果",
 				children: [
 					{
-						id: "4-1-1",
-						label: "Find系列"
+						id: "3-1",
+						label: "手机",
+						children: [
+							{
+								id: "3-1-1",
+								label: "iPhone 14"
+							},
+							{
+								id: "3-1-2",
+								label: "iPhone 13"
+							}
+						]
 					},
 					{
-						id: "4-1-2",
-						label: "Reno系列"
+						id: "3-2",
+						label: "平板",
+						children: [
+							{
+								id: "3-2-1",
+								label: "iPad Pro"
+							},
+							{
+								id: "3-2-2",
+								label: "iPad Air"
+							}
+						]
 					}
 				]
 			},
 			{
-				id: "4-2",
-				label: "配件",
+				id: "4",
+				label: "OPPO",
 				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: "4-1",
+						label: "手机",
+						children: [
+							{
+								id: "4-1-1",
+								label: "Find系列"
+							},
+							{
+								id: "4-1-2",
+								label: "Reno系列"
+							}
+						]
 					},
 					{
-						id: "5-1-2",
-						label: "S系列"
+						id: "4-2",
+						label: "配件",
+						children: [
+							{
+								id: "4-2-1",
+								label: "耳机"
+							},
+							{
+								id: "4-2-2",
+								label: "手环"
+							}
+						]
 					}
 				]
 			},
 			{
-				id: "5-2",
-				label: "智能设备",
+				id: "5",
+				label: "vivo",
 				children: [
 					{
-						id: "5-2-1",
-						label: "手表"
+						id: "5-1",
+						label: "手机",
+						children: [
+							{
+								id: "5-1-1",
+								label: "X系列"
+							},
+							{
+								id: "5-1-2",
+								label: "S系列"
+							}
+						]
 					},
 					{
-						id: "5-2-2",
-						label: "耳机"
+						id: "5-2",
+						label: "智能设备",
+						children: [
+							{
+								id: "5-2-1",
+								label: "手表"
+							},
+							{
+								id: "5-2-2",
+								label: "耳机"
+							}
+						]
 					}
 				]
 			}
-		]
-	}
-]);
+		]);
+
+		ui.hideLoading();
+	}, 500);
+}
 
 // 是否严格的遵循父子不互相关联
 const checkStrictly = ref(false);
@@ -326,7 +339,8 @@ const isCustomIcon = ref(false);
 const treeRef = ref<ClTreeComponentPublicInstance | null>(null);
 
 // 选中节点的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
 const halfCheckedKeys = ref<(string | number)[]>([]);
@@ -354,7 +368,7 @@ function getHalfChecked() {
 }
 
 function expand() {
-	treeRef.value!.setExpandedKeys(["1", "1-1", "2"]);
+	treeRef.value!.setExpandedKeys(["4", "5"]);
 }
 
 function getExpanded() {
@@ -370,4 +384,8 @@ function collapseAll() {
 	treeRef.value!.collapseAll();
 	expandedKeys.value = [];
 }
+
+onReady(() => {
+	refresh();
+});
 </script>

+ 45 - 14
uni_modules/cool-ui/components/cl-tree-item/cl-tree-item.uvue

@@ -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>

+ 116 - 24
uni_modules/cool-ui/components/cl-tree/cl-tree.uvue

@@ -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: {
+	// 是否严格的遵循父子不互相关联
+	checkStrictly: {
 		type: Boolean,
 		default: false
 	},
-	// 是否严格的遵循父子不互相关联
-	checkStrictly: {
+	// 是否可以选择节点
+	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,

+ 2 - 2
uni_modules/cool-ui/types/component.d.ts

@@ -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;