Browse Source

材料采集

liurongli 15 hours ago
parent
commit
3ad7093ec5
45 changed files with 4501 additions and 2689 deletions
  1. 281 0
      common/common.js
  2. 9 2
      components/navBar.vue
  3. 1 1
      pages.json
  4. 1 1
      pages/login/index.vue
  5. 79 9
      pages/materialCollection/taskOverview/addEdit.vue
  6. 0 1012
      pages/materialCollection/taskOverview/components/index.vue
  7. 0 178
      pages/materialCollection/taskOverview/components/personList.vue
  8. 0 674
      pages/materialCollection/taskOverview/components/selectObjectNoDedup.vue
  9. 3 1
      pages/materialCollection/taskOverview/index.vue
  10. 135 160
      pages/materialCollection/taskOverview/personList.vue
  11. 196 643
      pages/materialCollection/taskOverview/selectObjectNoDedup.vue
  12. 98 2
      pages/materialCollection/taskOverview/taskDetail.vue
  13. 7 0
      pages/teacherHonor/honorOverview/honorTypeAuditDetail.vue
  14. 1 0
      pages/teacherHonor/honorOverview/index.vue
  15. 29 3
      pages/teacherHonor/myHonor/uploadEditMyHonor.vue
  16. 2 3
      pages/teacherStudy/learningMonitor/index.vue
  17. 8 0
      reqApi/notification.js
  18. 10 0
      style/common.scss
  19. 156 0
      uni_modules/KieranYin9527-tree/changelog.md
  20. 1 0
      uni_modules/KieranYin9527-tree/components/index.js
  21. 17 0
      uni_modules/KieranYin9527-tree/components/uni-tree-view/constants/index.ts
  22. 1 0
      uni_modules/KieranYin9527-tree/components/uni-tree-view/types.js
  23. 232 0
      uni_modules/KieranYin9527-tree/components/uni-tree-view/types.ts
  24. 555 0
      uni_modules/KieranYin9527-tree/components/uni-tree-view/uni-tree-view.vue
  25. 47 0
      uni_modules/KieranYin9527-tree/components/uni-tree-view/uni-tree-view.vue.d.ts
  26. 1079 0
      uni_modules/KieranYin9527-tree/components/uni-tree-view/useTreeViewState.ts
  27. 120 0
      uni_modules/KieranYin9527-tree/components/uni-tree-view/useVirtualTreeList.ts
  28. 7 0
      uni_modules/KieranYin9527-tree/global.d.ts
  29. 365 0
      uni_modules/KieranYin9527-tree/index.d.ts
  30. 7 0
      uni_modules/KieranYin9527-tree/index.js
  31. 21 0
      uni_modules/KieranYin9527-tree/license.md
  32. 106 0
      uni_modules/KieranYin9527-tree/package.json
  33. 187 0
      uni_modules/KieranYin9527-tree/readme.md
  34. 271 0
      uni_modules/KieranYin9527-tree/style/index.scss
  35. 11 0
      uni_modules/KieranYin9527-tree/types.d.ts
  36. 1 0
      uni_modules/KieranYin9527-tree/utils/device.d.ts
  37. 16 0
      uni_modules/KieranYin9527-tree/utils/device.js
  38. 72 0
      uni_modules/KieranYin9527-tree/utils/env.d.ts
  39. 137 0
      uni_modules/KieranYin9527-tree/utils/env.js
  40. 9 0
      uni_modules/KieranYin9527-tree/utils/helpers.d.ts
  41. 42 0
      uni_modules/KieranYin9527-tree/utils/helpers.js
  42. 41 0
      uni_modules/KieranYin9527-tree/utils/mitt.d.ts
  43. 49 0
      uni_modules/KieranYin9527-tree/utils/mitt.js
  44. 17 0
      uni_modules/KieranYin9527-tree/utils/uni.d.ts
  45. 74 0
      uni_modules/KieranYin9527-tree/utils/uni.js

+ 281 - 0
common/common.js

@@ -142,6 +142,7 @@ export function remainingDays(endTime) {
     if (diffDays < 0) return 0;   // 已过期:不显示剩余时间
     return diffDays + 1;             // 18号到22号:22-18=4,加1后=5天
 }
+//时间
 export function unioformDateTransform(date, param = "time"){
     if(date){
         let strDate = new Date(date).toLocaleString('zh-CN')
@@ -197,4 +198,284 @@ export const getFileSize = (fileSize) => {
         fileSize = Math.round(fileSize / 1024)
         return `${fileSize} KB`
     }
+}
+//部门及部门下的人
+export const getDeptTreeData = (list) => {
+    if (!list || list.length === 0) return []
+
+    const arr = []
+
+    list.forEach(one => {
+        const { name, id, groupVOS } = one
+        const objectOne = {
+            label: name,
+            id: id,
+            children: []
+        }
+        arr.push(objectOne)
+
+        if (groupVOS && groupVOS.length) {
+            groupVOS.forEach(two => {
+                const { groupId, groupName, groupUserVOS } = two
+                const objectTwo = {
+                    label: groupName,
+                    id: groupId,
+                    children: []
+                }
+                objectOne.children.push(objectTwo)
+
+                if (groupUserVOS && groupUserVOS.length) {
+                    groupUserVOS.forEach((three, threeIndex) => {
+                        const { teacherName, teacherId, userAccount, canDel, selected } = three
+                        objectTwo.children.push({
+                            id: `${one.id}_${two.groupId}_${teacherId}_${threeIndex}`,
+                            label: `${teacherName}(${userAccount})`,
+                            targetType: 1,
+                            targetName: teacherName,
+                            targetId: teacherId,
+                            studentCode: null,
+                            roleCode: null,
+                            roleName: null,
+                            gradeCode: null,
+                            gradeName: null,
+                            subjectCode: null,
+                            subjectName: null,
+                            classIdCode: null,
+                            className: null,
+                            classType: null,
+                            classTypeName: null,
+                            departmentId: one.id,
+                            departmentName: one.name,
+                            departmentGroupId: two.groupId,
+                            departmentGroupName: two.groupName,
+                            disabled: !canDel,
+                            selected,
+							checked:selected==1?'checked':''
+                        })
+                    })
+                }
+            })
+        }
+    })
+
+    return arr
+}
+export const subjectTreeData = (list) => {
+    if (!list || list.length === 0) return []
+
+    const arr = []
+
+    list.forEach(one => {
+        const { subjectName, subjectCode, gradePersonVos } = one
+        const objectOne = {
+            label: subjectName,
+            id: subjectCode,
+            children: []
+        }
+        arr.push(objectOne)
+
+        if (gradePersonVos && gradePersonVos.length) {
+            gradePersonVos.forEach(two => {
+                const { gradeName, gradeCode, personVoList } = two
+                const objectTwo = {
+                    label: gradeName,
+                    id: `${subjectCode}_${gradeCode}`,
+                    children: []
+                }
+                objectOne.children.push(objectTwo)
+
+                if (personVoList && personVoList.length) {
+                    personVoList.forEach((three, threeIndex) => {
+                        const {
+                            showName,
+                            teacherName,
+                            teacherId,
+                            userAccount,
+                            roleCode,
+                            roleName,
+                            gradeCode,
+                            gradeName,
+                            classIdCode,
+                            className,
+                            canDel,
+                            selected
+                        } = three
+
+                        objectTwo.children.push({
+                            id: `${one.subjectCode}_${two.gradeCode}_${teacherId}_${threeIndex}`,
+                            label: `${showName}(${userAccount})`,
+                            targetType: 1,
+                            targetName: teacherName,
+                            targetId: teacherId,
+                            studentCode: null,
+                            roleCode,
+                            roleName,
+                            gradeCode,
+                            gradeName,
+                            subjectCode: one.subjectCode,
+                            subjectName: one.subjectName,
+                            classIdCode,
+                            className,
+                            classType: null,
+                            classTypeName: null,
+                            departmentId: null,
+                            departmentName: null,
+                            departmentGroupId: null,
+                            departmentGroupName: null,
+                            disabled: !canDel,
+                            selected
+                        })
+                    })
+                }
+            })
+        }
+    })
+
+    return arr
+}
+
+export const gradTreeData = (list) => {
+    if (!list || list.length === 0) return []
+
+    const arr = []
+
+    list.forEach(one => {
+        const { gradeName, gradeCode, classTypePersonVoList } = one
+        const objectOne = {
+            label: gradeName,
+            id: gradeCode,
+            children: []
+        }
+        arr.push(objectOne)
+
+        if (classTypePersonVoList && classTypePersonVoList.length) {
+            classTypePersonVoList.forEach(two => {
+                const { classTypeName, classType, classInfoPersonVoList } = two
+                const objectTwo = {
+                    label: classTypeName,
+                    id: `${gradeCode}_${classType}`,
+                    children: []
+                }
+                objectOne.children.push(objectTwo)
+
+                if (classInfoPersonVoList && classInfoPersonVoList.length) {
+                    classInfoPersonVoList.forEach(three => {
+                        const { className, classIdCode, personVoList } = three
+                        const objectThree = {
+                            label: className,
+                            id: `${gradeCode}_${classType}_${classIdCode}`,
+                            children: []
+                        }
+                        objectTwo.children.push(objectThree)
+
+                        if (personVoList && personVoList.length) {
+                            personVoList.forEach((four, fourIndex) => {
+                                const {
+                                    showName,
+                                    teacherName,
+                                    teacherId,
+                                    userAccount,
+                                    roleCode,
+                                    roleName,
+                                    courseCode,
+                                    courseName,
+                                    canDel,
+                                    selected
+                                } = four
+
+                                objectThree.children.push({
+                                    id: `${one.gradeCode}_${two.classType}_${three.classIdCode}_${teacherId}_${fourIndex}`,
+                                    label: `${showName}(${userAccount})`,
+                                    targetType: 1,
+                                    targetName: teacherName,
+                                    targetId: teacherId,
+                                    studentCode: null,
+                                    roleCode,
+                                    roleName,
+                                    gradeCode,
+                                    gradeName,
+                                    subjectCode: courseCode,
+                                    subjectName: courseName,
+                                    classIdCode: three.classIdCode,
+                                    className: three.className,
+                                    classType: two.classType,
+                                    classTypeName: two.classTypeName,
+                                    departmentId: null,
+                                    departmentName: null,
+                                    departmentGroupId: null,
+                                    departmentGroupName: null,
+                                    disabled: !canDel,
+                                    selected
+                                })
+                            })
+                        }
+                    })
+                }
+            })
+        }
+    })
+
+    return arr
+}
+
+export const permTreeData = (list) => {
+    if (!list || list.length === 0) return []
+
+    const arr = []
+
+    list.forEach(one => {
+        const { roleId, roleName, personVoList } = one
+        const objectOne = {
+            label: roleName,
+            id: roleId,
+            children: []
+        }
+        arr.push(objectOne)
+
+        if (personVoList && personVoList.length) {
+            personVoList.forEach((two, twoIndex) => {
+                const {
+                    showName,
+                    teacherName,
+                    teacherId,
+                    userAccount,
+                    roleCode,
+                    roleName,
+                    gradeCode,
+                    gradeName,
+                    courseCode,
+                    courseName,
+                    canDel,
+                    selected
+                } = two
+
+                objectOne.children.push({
+                    id: `${one.roleId}_${teacherId}_${twoIndex}`,
+                    label: `${showName}(${userAccount})`,
+                    targetType: 1,
+                    targetName: teacherName,
+                    targetId: teacherId,
+                    studentCode: null,
+                    roleCode,
+                    roleName,
+                    gradeCode,
+                    gradeName,
+                    subjectCode: courseCode,
+                    subjectName: courseName,
+                    classIdCode: null,
+                    className: null,
+                    classType: null,
+                    classTypeName: null,
+                    departmentId: null,
+                    departmentName: null,
+                    departmentGroupId: null,
+                    departmentGroupName: null,
+                    disabled: !canDel,
+                    selected
+                })
+            })
+        }
+    })
+
+    return arr
 }

+ 9 - 2
components/navBar.vue

@@ -47,6 +47,7 @@
 				{{item.label}}
 			</view>
 		</scroll-view>
+		<slot name="tabs_bottom_content" />
 	</uni-nav-bar>
 </template>
 
@@ -105,6 +106,10 @@
 			type: [String,Number],
 			default: () => ''
 		},//默认值
+		defaultTabBtnValue:{
+			type: [String,Number],
+			default: () => ''
+		},//默认值
 		tabBtns: { //tabBtns
 			type: Array,
 			default: () => []
@@ -153,7 +158,8 @@
 	})
 	watch(()=>props.tabBtns,(newVal, oldVal)=>{
 		if(newVal){
-			state.tabBtnSelected = props?.tabBtns?.[0]?.value ?? ''; //tab 默认值
+			const firstValue = props?.tabBtns?.[0]?.value ?? '';
+			state.tabBtnSelected = props.defaultTabBtnValue!==''?props.defaultTabBtnValue : firstValue; //tab 默认值
 		}
 	})
 	//关键字搜索
@@ -211,8 +217,9 @@
 	}
 	onMounted(() => {
 		const firstValue = props?.tabList?.[0]?.value ?? '';
+		const firstBtnValue = props?.tabBtns?.[0]?.value || '';
 		state.tabsSelected = props.defaultTabValue!==''?props.defaultTabValue : firstValue; //tab 默认值
-		state.tabBtnSelected = props?.tabBtns?.[0]?.value || ''; //tabBtns 默认值
+		state.tabBtnSelected = props.defaultTabBtnValue!==''?props.defaultTabBtnValue : firstBtnValue; //tabBtns 默认值
 		state.filterOpts.filterPickerValue = props.filterPickerData.map(item => item?.defaultValue ?? '');
 	})
 </script>

+ 1 - 1
pages.json

@@ -249,7 +249,7 @@
 		{
 			"path": "pages/materialCollection/taskOverview/selectObjectNoDedup",
 			"style": {
-				"navigationBarTitleText": "选择通知对象",
+				"navigationBarTitleText": "选择采集对象",
 				"navigationStyle": "custom"
 			}
 		},

+ 1 - 1
pages/login/index.vue

@@ -48,7 +48,7 @@
 		</view>
 	</popupDialog>
 	<!-- 同意协议并登录 -->
-	<popupDialog class="agreement_dialog" ref="agreementPopupDialog" popType="custom" title="用户协议及隐私改策" :showDialogClose="false" :dialogWidth="550" @DialogConfirm="DialogAgreementConfirm">
+	<popupDialog class="agreement_dialog" ref="agreementPopupDialog" popType="custom" title="用户协议及隐私改策" :showDialogClose="false" dialogWidth="550" @DialogConfirm="DialogAgreementConfirm">
 		<view class="agreement_popup_content">
 			<text class="color_999">我已阅读并同意</text>
 			<text class="color_blue" @click="AgreementDetails">用户协议</text>

+ 79 - 9
pages/materialCollection/taskOverview/addEdit.vue

@@ -21,7 +21,7 @@
 				<uni-forms-item label="任务名称:" required name="taskName">
 					<uni-easyinput v-model="state.formData.taskName" :maxlength="100" placeholder="请输入任务名称" />
 				</uni-forms-item>
-				<uni-forms-item label="采集要求:" required name="collectionRequire">
+				<uni-forms-item class="item_textarea" label="采集要求:" required name="collectionRequire">
 					<uni-easyinput type="textarea" v-model="state.formData.collectionRequire"
 						:style="state.easyinputStyle" placeholder="请输入采集要求"
 						placeholderStyle="font-weight: 400;font-size: 28rpx;color: #999999;"></uni-easyinput>
@@ -39,13 +39,13 @@
 						filterable multiple dataKey="teacherName" dataValue="teacherId" :clear="false"
 						:localdata="state.auditorsData"></zxz-uni-data-select>
 				</uni-forms-item>
-				<uni-forms-item label="采集对象:" required name="targetType">
+				<uni-forms-item label="采集对象:" required name="taskTargets">
 					<view class="forms_target">
-						<view class="target_left">
+						<view class="target_left" @click="GoPersonList">
 							<text class="left_text">{{targetTypeObj[state.formData.targetType]}}</text>
-							<text class="left_text">已选32人</text>
+							<text class="left_text">已选{{state?.formData?.taskTargets?.length || 0}}人</text>
 						</view>
-						<view class="target_right">
+						<view class="target_right" @click="SelectCollectionObject">
 							<text>选择采集对象</text>
 							<uni-icons class="right_icon" type="right"></uni-icons>
 						</view>
@@ -75,13 +75,14 @@
 		insertCollectionTask,
 		UpdateCollectionTask
 	} from '@/reqApi/notification.js';
+	import {getDeptTreeData,subjectTreeData,gradTreeData,permTreeData} from '@/common/common.js'
 	import {
 		reactive,
 		ref,
-		onMounted,
-		nextTick
+		onMounted
 	} from 'vue';
 	import {
+		onShow,
 		onLoad
 	} from '@dcloudio/uni-app';
 	const state = reactive({
@@ -100,7 +101,9 @@
 			auditMechanism: 1, //审核机制 0-无需审核 1-需要审核	
 			taskAuditors: [], //审核人
 			targetType:1,//采集对象 1-按部门,2-按学科,3-按年级,4-按权限,5-按学生
+			taskTargets:[],//选择的采集对象明细
 		},
+		taskTargetsTree:[],//采集对象Tree
 		schoolYearData:[],//学年学期
 		departmentData:[],//归属分类
 		materialCategoryData:[],//材料类目
@@ -133,8 +136,15 @@
 		collectionRequire: {rules: [{required: true,errorMessage: '请输入采集要求'}]},
 		taskTime: {rules: [{required: true,errorMessage: '请输入采集周期'}]},
 		auditMechanism: {rules: [{required: true,errorMessage: '请选择审核机制'}]},
-		targetType: {rules: [{required: true,errorMessage: '请选择采集对象'}]},
+		taskTargets: {rules: [{required: true,errorMessage: '请选择采集对象'}]},
 	});
+	onShow(()=>{
+		const taskTargetsTreeData = uni.getStorageSync('taskTargetsTreeData');//采集对象
+		if(taskTargetsTreeData){
+			state.formData.targetType = taskTargetsTreeData.targetType;
+			state.formData.taskTargets = taskTargetsTreeData.taskTargets;//采集对象
+		}
+	})
 	onLoad(async(option) => {
 		state.taskId = option.taskId;
 		state.title = state.taskId ? '编辑采集任务' : '新增采集任务';
@@ -221,6 +231,23 @@
 					}else if(key == 'taskAuditors'){//审核人
 						const taskAuditorsList = resData?.taskAuditors || [];
 						state.formData.taskAuditors = taskAuditorsList.map(item=>item.auditorId);
+					}else if (key == 'taskTargets') {
+						const personList = resData?.personalInfoMap?.personVoList || [];
+						const targetType = resData.targetType;
+						let taskTargetsTree = [];
+						if (targetType == 1) {
+							taskTargetsTree = getDeptTreeData(personList)
+						}else if (targetType === 2) {
+							taskTargetsTree = subjectTreeData(personList)
+						}else if (targetType === 3) {
+							taskTargetsTree = gradTreeData(personList)
+						}else if (targetType === 4) {
+							taskTargetsTree = permTreeData(personList)
+						}else{
+							taskTargetsTree = [];
+						}
+						state.formData.taskTargets = GetSelectPerson(taskTargetsTree);//已选择
+						state.taskTargetsTree = taskTargetsTree;//对象选择tree
 					}else{
 						state.formData[key] = resData[key];
 						if(key=='departmentId'){
@@ -231,6 +258,21 @@
 			}
 		})
 	}
+	//获取已选的采集对象
+	const GetSelectPerson = (tree) => {
+	    const result = [];
+	    const dfs = (node) => {
+	        if (Array.isArray(node)) {
+	            node.forEach(dfs);
+	        } else if (node.children && node.children.length) {
+	            node.children.forEach(dfs);
+	        } else if (node.selected) {
+	            result.push(node);
+	        }
+	    };
+	    dfs(tree);
+	    return result;
+	};
 	//提交
 	const honorTypeConfirm = () => {
 		let ruleList = {};
@@ -271,7 +313,7 @@
 			targetType: state.formData.targetType,
 			auditMechanism: state.formData.auditMechanism,//审核机制 0-无需审核 1-需要审核
 			taskAuditors: taskAuditors,//审核人
-			taskTargets: []//采集对象明细
+			taskTargets: state.formData.taskTargets//采集对象明细
 		}
 		if (state.taskId) {
 			params.id = state.taskId;
@@ -302,6 +344,31 @@
 			})
 		}
 	}
+	//人员列表
+	const GoPersonList = () => {
+		uni.setStorageSync('taskTargetsTreeData',{
+			targetType:state.formData.targetType,
+			taskTargets:state.formData.taskTargets,//已选中
+		});
+		uni.navigateTo({
+			url: `/pages/materialCollection/taskOverview/personList`
+		});
+	}
+	
+	//选择采集对象
+	const SelectCollectionObject = () => {
+		const schoolYearId = state.formData.schoolYearId;
+		const targetType = state.formData.targetType;
+		const type = state.taskId?'edit':'add';
+		uni.setStorageSync('taskTargetsTreeData',{
+			targetType:targetType,
+			taskTargets:state.formData.taskTargets,//已选中
+			taskTargetsTree:state.taskTargetsTree//tree
+		});
+		uni.navigateTo({
+			url: `/pages/materialCollection/taskOverview/selectObjectNoDedup?schoolYearId=${schoolYearId}&targetType=${targetType}&type=${type}`
+		});
+	}
 	const GoBack = () => {
 		uni.navigateBack({
 			delta: 1
@@ -327,6 +394,9 @@
 				border-top: 0;
 				border-bottom: 2rpx solid #F3F3F3;
 				align-items: center;
+				&.item_textarea{
+					align-items: flex-start;
+				}
 			}
 			.forms_target{
 				display: flex;

+ 0 - 1012
pages/materialCollection/taskOverview/components/index.vue

@@ -1,1012 +0,0 @@
-<template>
-	<view class="page_publish">
-		<commonHeader :title="isEdit ? '编辑任务' : '新建任务'" @back="goBack"></commonHeader>
-
-		<scroll-view scroll-y class="publish_content" :style="{ paddingTop: statusBarHeight + 160 + 'rpx', paddingBottom: safeAreaBottom + 'rpx' }">
-			<view class="form_item">
-				<view class="form_row form_row_type">
-					<text class="form_label">学年学期</text>
-					<view class="form_content">
-						<picker mode="selector" :range="academicYearOptions" :value="academicYearIndex" @change="handleAcademicYearChange">
-							<view class="filter_item">
-								<text class="filter_text">{{ academicYearOptions[academicYearIndex] }}</text>
-								<uni-icons type="down" size="16" color="#999999"></uni-icons>
-							</view>
-						</picker>
-					</view>
-				</view>
-			</view>
-
-			<view class="form_item">
-				<view class="form_row form_row_type">
-					<text class="form_label">归属分类</text>
-					<view class="form_content">
-						<picker mode="selector" :range="categoryOptions" :value="categoryIndex" @change="handleCategoryChange">
-							<view class="filter_item">
-								<text class="filter_text">{{ categoryOptions[categoryIndex] }}</text>
-								<uni-icons type="down" size="16" color="#999999"></uni-icons>
-							</view>
-						</picker>
-					</view>
-				</view>
-			</view>
-
-			<view class="form_item">
-				<view class="form_row form_row_type">
-					<text class="form_label">材料类目</text>
-					<view class="form_content">
-						<picker mode="selector" :range="materialTypeOptions" :value="materialTypeIndex" @change="handleMaterialTypeChange">
-							<view class="filter_item">
-								<text class="filter_text">{{ materialTypeOptions[materialTypeIndex] }}</text>
-								<uni-icons type="down" size="16" color="#999999"></uni-icons>
-							</view>
-						</picker>
-					</view>
-				</view>
-			</view>
-
-			<view class="form_item">
-				<view class="form_row form_row_type">
-					<text class="form_label">任务名称</text>
-					<view class="form_content">
-						<input type="text" class="form_input" v-model="formData.taskName" placeholder="请输入任务名称" />
-					</view>
-				</view>
-			</view>
-
-			<view class="form_item">
-				<view class="form_row">
-					<text class="form_label">采集要求</text>
-					<view class="form_content">
-						<view class="textarea_container">
-							<textarea class="form_textarea" v-model="formData.collectRequirement" placeholder="请输入通知内容"
-								:maxlength="-1"></textarea>
-						</view>
-					</view>
-				</view>
-			</view>
-
-			<view class="form_item">
-				<view class="form_row form_row_type">
-					<text class="form_label">采集周期</text>
-					<view class="form_content date_range">
-						<picker mode="date" :value="formData.startTime" @change="onStartDateChange">
-							<view class="date_range_item">
-								<text class="date_text" :class="{ placeholder: !formData.startTime }">{{ formData.startTime || '开始日期' }}</text>
-								<uni-icons type="calendar" size="16" color="#999999"></uni-icons>
-							</view>
-						</picker>
-						<text class="date_separator">—</text>
-						<picker mode="date" :value="formData.endTime" @change="onEndDateChange">
-							<view class="date_range_item">
-								<text class="date_text" :class="{ placeholder: !formData.endTime }">{{ formData.endTime || '结束日期' }}</text>
-								<uni-icons type="calendar" size="16" color="#999999"></uni-icons>
-							</view>
-						</picker>
-					</view>
-				</view>
-			</view>
-
-			<view class="form_item">
-				<view class="form_row form_row_type">
-					<text class="form_label">审核机制</text>
-					<view class="form_content">
-						<customRadio v-model="formData.auditMechanism" :options="auditOptions" />
-					</view>
-				</view>
-			</view>
-
-			<view class="form_item" v-if="formData.auditMechanism === 0">
-				<view class="form_row form_row_type">
-					<text class="form_label">审核人</text>
-					<view class="form_content">
-						<view class="auditor_row" @click="openAuditorDialog">
-							<text class="auditor_name" :class="{ placeholder: !formData.auditorName }">{{ formData.auditorName || '请选择审核人' }}</text>
-							<uni-icons type="down" size="16" color="#999999"></uni-icons>
-						</view>
-					</view>
-				</view>
-			</view>
-
-			<view class="form_item">
-				<view class="form_row">
-					<text class="form_label">采集对象</text>
-					<view class="form_content">
-						<view class="object_row">
-							<view class="object_info" @click="goPersonList">
-								<text class="object_count">{{ objectTypeLabel || '请选择' }}</text>
-								<text class="object_count">已选{{ selectedCount }}人</text>
-							</view>
-							<view class="object_action" @click="selectObject">
-								<text class="action_text">选择采集对象</text>
-								<text class="action_arrow">></text>
-							</view>
-						</view>
-					</view>
-				</view>
-			</view>
-		</scroll-view>
-
-		<view class="publish_footer" :style="{ paddingBottom: safeAreaBottom + 24 + 'rpx' }">
-			<view class="confirm_btn" @click="handleSave">
-				<text class="btn_text">确定</text>
-			</view>
-		</view>
-
-		<!-- 审核人搜索选择弹窗 -->
-		<customPopupDialog ref="auditorDialogRef" title="选择审核人" :showDialogFooter="false" @DialogConfirm="confirmAuditorSelection">
-			<view class="auditor_search_wrap">
-				<view class="auditor_search_box">
-					<uni-icons type="search" size="20" color="#999999"></uni-icons>
-					<input type="text" class="auditor_search_input" v-model="auditorSearchKey" placeholder="请输入搜索关键字"
-						@confirm="searchAuditors" @input="handleAuditorSearchInput" />
-					<text v-if="auditorSearchKey" class="auditor_search_clear" @click="clearAuditorSearch">
-						<uni-icons type="clear" size="18" color="#999999"></uni-icons>
-					</text>
-				</view>
-				<scroll-view scroll-y class="auditor_list">
-					<view v-if="auditorSearchLoading" class="auditor_loading">
-						<text>加载中...</text>
-					</view>
-					<view v-else-if="auditorListFiltered.length === 0" class="auditor_empty">
-						<noData :tipText="'暂无搜索结果'" />
-					</view>
-					<view v-else class="auditor_list_content">
-						<view v-for="(item, idx) in auditorListFiltered" :key="idx"
-							class="auditor_list_item"
-							:class="{ active: selectedAuditor && selectedAuditor.teacherId === item.teacherId }"
-							@click="selectAuditor(item)">
-							<view class="auditor_item_info">
-								<text class="auditor_item_name">{{ item.teacherName }}</text>
-								<text v-if="item.teacherAccount" class="auditor_item_account">账号:{{ item.teacherAccount
-									}}</text>
-							</view>
-							<view v-if="selectedAuditor && selectedAuditor.teacherId === item.teacherId"
-								class="auditor_item_check">
-								<uni-icons type="checkmarkempty" size="22" color="#2E64FA"></uni-icons>
-							</view>
-						</view>
-					</view>
-				</scroll-view>
-				<view class="auditor_dialog_footer">
-					<view class="auditor_dialog_btn cancel_btn" @click="closeAuditorDialog">
-						<text>取消</text>
-					</view>
-					<view class="auditor_dialog_btn confirm_btn" @click="confirmAuditorSelection">
-						<text>确定</text>
-					</view>
-				</view>
-			</view>
-		</customPopupDialog>
-	</view>
-</template>
-
-<script setup>
-import { ref, reactive, onMounted, computed } from 'vue';
-import { onLoad, onShow } from '@dcloudio/uni-app';
-import uniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue';
-import {getSchoolYearList,getCategoryList,getAuditorList,find_collection_task_target_list,save_collection_task} from '@/reqApi/notification.js';
-import CustomRadio from '@/components/customRadio.vue';
-import CommonHeader from '@/components/commonHeader.vue';
-import CustomPopupDialog from '@/components/customPopupDialog.vue';
-import NoData from '@/components/noData.vue';
-import { useSafeArea } from '@/common/safeArea';
-
-const { statusBarHeight, safeAreaBottom, initSafeArea } = useSafeArea();
-
-const isEdit = ref(false);
-const editId = ref('');
-
-const academicYearIndex = ref(0);
-const academicYearOptions = ref(['']);
-const academicYearIds = ref(['']);
-const academicYearCodes = ref(['']);
-
-const categoryIndex = ref(0);
-const categoryOptions = ref(['全部分类']);
-const categoryData = ref([]);
-
-const materialTypeIndex = ref(0);
-const materialTypeOptions = ref(['全部类目']);
-
-// 审核人弹窗相关
-const auditorDialogRef = ref(null);
-const auditorSearchKey = ref('');
-const auditorSearchLoading = ref(false);
-const auditorListFull = ref([]);
-const selectedAuditor = ref(null);
-let auditorSearchTimer = null;
-
-const auditorListFiltered = computed(() => {
-	return auditorListFull.value;
-});
-
-const auditOptions = [
-	{ label: '需审核', value: 0 },
-	{ label: '无审核', value: 1 }
-];
-
-const formData = reactive({
-	schoolYearName: '',
-	schoolYearId: '',
-	schoolYearCode: '',
-	departmentName: '',
-	departmentId: '',
-	materialCategoryName: '',
-	materialCategoryId: '',
-	taskName: '',
-	collectRequirement: '',
-	startTime: '',
-	endTime: '',
-	auditMechanism: 0,
-	auditorName: '',
-	auditorId: ''
-});
-
-const selectedCount = ref(0);
-const selectedUsers = ref([]);
-const objectTypeLabel = ref('');
-const originalNoticeType = ref(0);
-
-const fetchAcademicYearList = async () => {
-	try {
-		const res = await getSchoolYearList();
-		if (res.data && Array.isArray(res.data)) {
-			const names = res.data.map(item => item.schoolYearName);
-			const ids = res.data.map(item => item.id);
-			const codes = res.data.map(item => item.schoolYearCode);
-			academicYearOptions.value = names;
-			academicYearIds.value = ids;
-			academicYearCodes.value = codes;
-			if (res.data.length > 0) {
-				academicYearIndex.value = 0;
-				formData.schoolYearName = res.data[0].schoolYearName;
-				formData.schoolYearId = res.data[0].id;
-				formData.schoolYearCode = res.data[0].schoolYearCode;
-			}
-		}
-	} catch (error) {
-		console.error('获取学年学期列表失败:', error);
-	}
-};
-
-const fetchCategoryList = async () => {
-	try {
-		const res = await getCategoryList();
-		if (res.data && Array.isArray(res.data)) {
-			categoryData.value = res.data;
-			const categoryNames = res.data.map(item => item.categoryName || item.name);
-			categoryOptions.value = ['全部分类', ...categoryNames];
-			categoryIndex.value = 0;
-			materialTypeOptions.value = ['全部类目'];
-			formData.departmentName = '';
-			formData.departmentId = '';
-			formData.materialCategoryName = '';
-			formData.materialCategoryId = '';
-		}
-	} catch (error) {
-		console.error('获取归属部门列表失败:', error);
-	}
-};
-
-const searchAuditors = async () => {
-	auditorSearchLoading.value = true;
-	try {
-		const res = await getAuditorList(auditorSearchKey.value || '');
-		if (res.data && Array.isArray(res.data)) {
-			auditorListFull.value = res.data.map(item => ({
-				teacherId: item.teacherId || item.id,
-				teacherName: item.teacherName || item.name,
-				teacherAccount: item.teacherAccount || item.accountName || item.userAccount || ''
-			}));
-		} else {
-			auditorListFull.value = [];
-		}
-	} catch (error) {
-		console.error('搜索审核人失败:', error);
-		auditorListFull.value = [];
-	} finally {
-		auditorSearchLoading.value = false;
-	}
-};
-
-const handleAuditorSearchInput = () => {
-	if (auditorSearchTimer) {
-		clearTimeout(auditorSearchTimer);
-	}
-	auditorSearchTimer = setTimeout(() => {
-		searchAuditors();
-	}, 500);
-};
-
-const clearAuditorSearch = () => {
-	auditorSearchKey.value = '';
-	searchAuditors();
-};
-
-const openAuditorDialog = () => {
-	auditorDialogRef.value.OpenDialog('bottom');
-	// 初始化选中状态
-	if (formData.auditorId) {
-		selectedAuditor.value = {
-			teacherId: formData.auditorId,
-			teacherName: formData.auditorName
-		};
-	} else {
-		selectedAuditor.value = null;
-	}
-	auditorSearchKey.value = '';
-	auditorListFull.value = [];
-	searchAuditors();
-};
-
-const closeAuditorDialog = () => {
-	auditorDialogRef.value.CloseDialog();
-};
-
-const selectAuditor = (item) => {
-	selectedAuditor.value = item;
-};
-
-const confirmAuditorSelection = () => {
-	if (!selectedAuditor.value) {
-		uni.showToast({ title: '请选择审核人', icon: 'none' });
-		return;
-	}
-	formData.auditorName = selectedAuditor.value.teacherName;
-	formData.auditorId = selectedAuditor.value.teacherId;
-	closeAuditorDialog();
-};
-
-onMounted(() => {
-	initSafeArea();
-});
-
-onLoad(async (options) => {
-	await Promise.all([
-		fetchAcademicYearList(),
-		fetchCategoryList()
-	]);
-
-	if (options && options.id) {
-		isEdit.value = true;
-		editId.value = options.id;
-		fetchTaskDetail(options.id);
-	}
-});
-
-const fetchTaskDetail = async (id) => {
-	try {
-		const res = await find_collection_task_target_list({ id });
-		if (res.data) {
-			const data = res.data;
-			formData.taskName = data.taskName || '';
-			const content = data.noticeContent || data.content || '';
-			formData.collectRequirement = content.replace(/<[^>]+>/g, '');
-			formData.startTime = data.startTime || '';
-			formData.endTime = data.endTime || '';
-			formData.auditMechanism = data.auditMechanism ? 1 : 0;
-			formData.auditorId = data.auditorId || '';
-			formData.auditorName = data.auditorName || '';
-			formData.departmentName = data.departmentName || '';
-			formData.materialCategoryName = data.materialCategoryName || '';
-			formData.schoolYearName = data.schoolYearName || '';
-
-			if (data.departmentId) {
-				formData.departmentId = data.departmentId;
-				const idx = categoryData.value.findIndex(item => item.id === data.departmentId);
-				if (idx >= 0) {
-					categoryIndex.value = idx + 1;
-					const selectedCategory = categoryData.value[idx];
-					const childList = selectedCategory?.childList || [];
-					materialTypeOptions.value = ['全部类目', ...childList.map(child => child.categoryName || child.name)];
-				}
-			}
-
-			if (data.materialCategory) {
-				formData.materialCategoryId = data.materialCategory;
-				if (categoryIndex.value > 0) {
-					const selectedCategory = categoryData.value[categoryIndex.value - 1];
-					const childList = selectedCategory?.childList || [];
-					const materialIdx = childList.findIndex(item => item.id === data.materialCategory);
-					if (materialIdx >= 0) {
-						materialTypeIndex.value = materialIdx + 1;
-						formData.materialCategoryName = childList[materialIdx].categoryName || childList[materialIdx].name || '';
-					}
-				}
-			}
-
-			if (data.noticeType) {
-				const typeMap = {
-					1: '按部门',
-					2: '按学科',
-					3: '按年级',
-					4: '按权限',
-					5: '按学生'
-				};
-				objectTypeLabel.value = typeMap[data.noticeType] || '';
-				originalNoticeType.value = data.noticeType;
-			}
-
-			if (data.userList && Array.isArray(data.userList)) {
-				selectedUsers.value = data.userList.map(user => ({
-					id: user.userId || user.id,
-					name: user.thirdName || user.name,
-					userId: user.userId,
-					thirdCode: user.thirdCode || user.userAccount || '',
-					userAccount: user.userAccount || user.thirdCode,
-					departmentName: user.departmentName || '',
-					groupName: user.groupName || '',
-					subjectName: user.subjectName || '',
-					gradeName: user.gradeName || '',
-					classTypeName: user.classTypeName || '',
-					className: user.className || '',
-					roleName: user.roleName || ''
-				}));
-				selectedCount.value = selectedUsers.value.length;
-			}
-		}
-	} catch (error) {
-		console.error('获取任务详情失败:', error);
-	}
-};
-
-onShow(() => {
-	const result = uni.getStorageSync('selectObjectResult');
-	if (result) {
-		try {
-			const data = JSON.parse(result);
-			if (data) {
-				objectTypeLabel.value = data.tab;
-				selectedCount.value = data.count;
-				selectedUsers.value = data.users;
-			}
-		} catch (e) {
-			console.error('解析选择结果失败:', e);
-		}
-		uni.removeStorageSync('selectObjectResult');
-	}
-
-	const personResult = uni.getStorageSync('personListResult');
-	if (personResult) {
-		try {
-			const data = JSON.parse(personResult);
-			if (data && data.users) {
-				selectedUsers.value = data.users;
-				selectedCount.value = data.users.length;
-			}
-		} catch (e) {
-			console.error('解析人员列表结果失败:', e);
-		}
-		uni.removeStorageSync('personListResult');
-	}
-});
-
-const handleAcademicYearChange = (e) => {
-	const idx = e.detail.value;
-	academicYearIndex.value = idx;
-	formData.schoolYearName = academicYearOptions.value[idx];
-	formData.schoolYearId = academicYearIds.value[idx];
-	formData.schoolYearCode = academicYearCodes.value[idx];
-};
-
-const handleCategoryChange = (e) => {
-	categoryIndex.value = e.detail.value;
-	materialTypeIndex.value = 0;
-	if (categoryIndex.value === 0) {
-		materialTypeOptions.value = ['全部类目'];
-		formData.departmentName = '';
-		formData.departmentId = '';
-		formData.materialCategoryName = '';
-		formData.materialCategoryId = '';
-	} else {
-		const selectedCategory = categoryData.value[categoryIndex.value - 1];
-		const categoryName = selectedCategory?.categoryName || selectedCategory?.name || '';
-		formData.departmentName = categoryName;
-		formData.departmentId = selectedCategory?.id || '';
-		const childList = selectedCategory?.childList || [];
-		materialTypeOptions.value = ['全部类目', ...childList.map(child => child.categoryName || child.name)];
-		formData.materialCategoryName = '';
-		formData.materialCategoryId = '';
-	}
-};
-
-const handleMaterialTypeChange = (e) => {
-	materialTypeIndex.value = e.detail.value;
-	if (categoryIndex.value === 0) {
-		formData.materialCategoryName = '';
-		formData.materialCategoryId = '';
-	} else {
-		const selectedCategory = categoryData.value[categoryIndex.value - 1];
-		const childList = selectedCategory?.childList || [];
-		if (materialTypeIndex.value === 0) {
-			formData.materialCategoryName = '';
-			formData.materialCategoryId = '';
-		} else {
-			const selectedChild = childList[materialTypeIndex.value - 1];
-			formData.materialCategoryName = selectedChild?.categoryName || selectedChild?.name || '';
-			formData.materialCategoryId = selectedChild?.id || '';
-		}
-	}
-};
-
-const onStartDateChange = (e) => {
-	formData.startTime = e.detail.value;
-};
-
-const onEndDateChange = (e) => {
-	formData.endTime = e.detail.value;
-};
-
-const goBack = () => {
-	uni.navigateBack();
-};
-
-const selectObject = () => {
-	const lastSelection = {
-		tab: objectTypeLabel.value,
-		users: selectedUsers.value,
-		noticeType: originalNoticeType.value || getNoticeTypeValue(objectTypeLabel.value),
-		isEdit: isEdit.value
-	};
-	uni.setStorageSync('selectObjectLastSelection', JSON.stringify(lastSelection));
-	uni.navigateTo({ url: '/pages/materialCollection/taskOverview/components/selectObjectNoDedup' });
-};
-
-const goPersonList = () => {
-	if (selectedUsers.value.length === 0) {
-		return;
-	}
-	const data = {
-		tab: objectTypeLabel.value,
-		users: selectedUsers.value
-	};
-	uni.setStorageSync('personListData', JSON.stringify(data));
-	uni.navigateTo({ url: '/pages/materialCollection/taskOverview/components/personList' });
-};
-
-const getNoticeTypeValue = (tab) => {
-	const map = {
-		'按部门': 1,
-		'按学科': 2,
-		'按年级': 3,
-		'按权限': 4,
-		'按学生': 5
-	};
-	return map[tab] || 1;
-};
-
-const handleSave = async () => {
-	if (!formData.schoolYearId) {
-		uni.showToast({ title: '请选择学年学期', icon: 'none' });
-		return;
-	}
-	if (!formData.departmentId) {
-		uni.showToast({ title: '请选择归属部门', icon: 'none' });
-		return;
-	}
-	if (!formData.materialCategoryId) {
-		uni.showToast({ title: '请选择材料类目', icon: 'none' });
-		return;
-	}
-	if (!formData.taskName) {
-		uni.showToast({ title: '请输入任务名称', icon: 'none' });
-		return;
-	}
-	if (!formData.collectRequirement) {
-		uni.showToast({ title: '请输入采集要求', icon: 'none' });
-		return;
-	}
-	if (!formData.startTime || !formData.endTime) {
-		uni.showToast({ title: '请选择采集周期', icon: 'none' });
-		return;
-	}
-	if (formData.auditMechanism === 1 && !formData.auditorId) {
-		uni.showToast({ title: '请选择审核人', icon: 'none' });
-		return;
-	}
-	if (!objectTypeLabel.value) {
-		uni.showToast({ title: '请选择采集对象', icon: 'none' });
-		return;
-	}
-	if (selectedUsers.value.length === 0) {
-		uni.showToast({ title: '请选择采集人员', icon: 'none' });
-		return;
-	}
-
-	uni.showLoading({ title: '保存中...' });
-
-	try {
-		const objectType = getNoticeTypeValue(objectTypeLabel.value);
-		const params = {
-			schoolYearCode: formData.schoolYearCode,
-			schoolYearId: formData.schoolYearId,
-			departmentId: formData.departmentId,
-			materialCategory: formData.materialCategoryId,
-			taskName: formData.taskName,
-			noticeContent: btoa(unescape(encodeURIComponent(formData.collectRequirement))),
-			startTime: formData.startTime,
-			endTime: formData.endTime,
-			auditMechanism: formData.auditMechanism,
-			auditorId: formData.auditorId,
-			auditorName: formData.auditorName,
-			noticeType: objectType,
-			fileList: []
-		};
-
-		if (isEdit.value && editId.value) {
-			params.id = editId.value;
-		}
-
-		const userList = selectedUsers.value.map(user => {
-			return {
-				userType: objectType === 5 ? 2 : 1,
-				userId: user.userId || user.id,
-				thirdName: user.name,
-				thirdCode: user.thirdCode || '',
-				gradeName: user.gradeName || '',
-				classTypeName: user.classTypeName || '',
-				className: user.className || '',
-				departmentName: user.departmentName || '',
-				groupName: user.groupName || '',
-				subjectName: user.subjectName || '',
-				roleName: user.roleName || ''
-			};
-		});
-
-		if (userList.length > 0) {
-			params.userList = userList;
-		}
-
-		const res = await save_collection_task(params);
-		if (res.code === 200) {
-			uni.showToast({ title: '保存成功', icon: 'success' });
-			setTimeout(() => {
-				uni.navigateBack();
-			}, 1500);
-		} else {
-			uni.showToast({ title: res.message || '保存失败', icon: 'none' });
-		}
-	} catch (error) {
-		console.error('保存失败:', error);
-		uni.showToast({ title: '保存失败', icon: 'none' });
-	} finally {
-		uni.hideLoading();
-	}
-};
-</script>
-
-<style lang="scss" scoped>
-.page_publish {
-	min-height: 100vh;
-	background-color: #FFFFFF;
-	display: flex;
-	flex-direction: column;
-	box-sizing: border-box;
-}
-
-.publish_content {
-	flex: 1;
-	box-sizing: border-box;
-	padding-top: 36rpx;
-	padding-left: 24rpx;
-	padding-right: 24rpx;
-}
-
-.form_item {
-	background-color: #FFFFFF;
-	border-radius: 8rpx;
-	padding-bottom: 24rpx;
-	margin-bottom: 24rpx;
-	border-bottom: 2rpx solid #F3F3F3;
-	&:last-child {
-		border-bottom: none;
-	}
-	.form_label {
-		font-size: 28rpx;
-		color: #666666;
-		font-weight: 400;
-		white-space: nowrap;
-		flex-shrink: 0;
-		margin-right: 16rpx;
-
-		&::after {
-			content: ':';
-			margin-left: 8rpx;
-		}
-	}
-
-	.form_row {
-		display: flex;
-		align-items: flex-start;
-
-		.form_content {
-			flex: 1;
-		}
-	}
-
-	.form_row_type {
-		align-items: center;
-
-		.filter_item {
-			display: flex;
-			align-items: center;
-			justify-content: space-between;
-			padding: 0 16rpx;
-			height: 72rpx;
-			line-height: 72rpx;
-			background-color: #FFFFFF;
-			border-radius: 8rpx;
-			width: 320rpx;
-			border: 2rpx solid #DCDFE6;
-
-			.filter_text {
-				font-size: 28rpx;
-				color: #333333;
-				margin-right: 8rpx;
-				overflow: hidden;
-				text-overflow: ellipsis;
-				white-space: nowrap;
-				&.placeholder {
-					color: #909399;
-				}
-			}
-		}
-	}
-
-	.form_input {
-		font-size: 28rpx;
-		color: #333333;
-		padding: 0 24rpx;
-		background-color: #FFFFFF;
-		border-radius: 8rpx;
-		width: 100%;
-		height: 72rpx;
-		box-sizing: border-box;
-		border: 2rpx solid #E9E9E9;
-	}
-
-	.textarea_container {
-		position: relative;
-
-		.form_textarea {
-			width: 100%;
-			height: 240rpx;
-			font-size: 28rpx;
-			color: #333333;
-			line-height: 1.6;
-			padding: 14rpx 24rpx;
-			background-color: #FFFFFF;
-			border-radius: 8rpx;
-			box-sizing: border-box;
-			border: 2rpx solid #E9E9E9;
-		}
-	}
-
-	.date_range {
-		display: flex;
-		align-items: center;
-		gap: 16rpx;
-
-		.date_range_item {
-			display: flex;
-			align-items: center;
-			gap: 8rpx;
-			padding: 0 16rpx;
-			height: 72rpx;
-			background-color: #FFFFFF;
-			border-radius: 8rpx;
-			border: 2rpx solid #DCDFE6;
-
-			.date_text {
-				font-size: 28rpx;
-				color: #333333;
-				&.placeholder {
-					color: #909399;
-				}
-			}
-		}
-
-		.date_separator {
-			font-size: 28rpx;
-			color: #999999;
-		}
-	}
-
-	.object_row {
-		display: flex;
-		align-items: center;
-		justify-content: space-between;
-
-		.object_info {
-			display: flex;
-			align-items: center;
-			gap: 12rpx;
-
-			.object_count {
-				font-size: 28rpx;
-				color: #2E64FA;
-			}
-		}
-
-		.object_action {
-			display: flex;
-			align-items: center;
-			gap: 8rpx;
-
-			.action_text {
-				font-size: 28rpx;
-				color: #2E64FA;
-			}
-
-			.action_arrow {
-				font-size: 28rpx;
-				color: #2E64FA;
-			}
-		}
-	}
-
-	.auditor_row {
-		display: flex;
-		align-items: center;
-		justify-content: space-between;
-		padding: 0 16rpx;
-		height: 72rpx;
-		background-color: #FFFFFF;
-		border-radius: 8rpx;
-		border: 2rpx solid #E9E9E9;
-		box-sizing: border-box;
-
-		.auditor_name {
-			font-size: 28rpx;
-			color: #333333;
-			flex: 1;
-			overflow: hidden;
-			text-overflow: ellipsis;
-			white-space: nowrap;
-			&.placeholder {
-				color: #909399;
-			}
-		}
-	}
-}
-
-// 审核人搜索弹窗样式
-.auditor_search_wrap {
-	padding: 24rpx;
-	box-sizing: border-box;
-
-	.auditor_search_box {
-		display: flex;
-		align-items: center;
-		height: 72rpx;
-		background: #F5F7FA;
-		border-radius: 36rpx;
-		padding: 0 24rpx;
-		margin-bottom: 24rpx;
-		gap: 16rpx;
-
-		.auditor_search_input {
-			flex: 1;
-			font-size: 28rpx;
-			color: #333333;
-			background: transparent;
-			height: 72rpx;
-			line-height: 72rpx;
-		}
-
-		.auditor_search_clear {
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			width: 40rpx;
-			height: 40rpx;
-		}
-	}
-
-	.auditor_list {
-		height: 600rpx;
-
-		.auditor_loading {
-			text-align: center;
-			padding: 60rpx 0;
-			font-size: 28rpx;
-			color: #999999;
-		}
-
-		.auditor_empty {
-			padding: 60rpx 0;
-		}
-
-		.auditor_list_content {
-			.auditor_list_item {
-				display: flex;
-				align-items: center;
-				justify-content: space-between;
-				padding: 24rpx 16rpx;
-				border-bottom: 2rpx solid #F3F3F3;
-				&:last-child {
-					border-bottom: none;
-				}
-				&.active {
-					background-color: rgba(46, 100, 250, 0.05);
-				}
-
-				.auditor_item_info {
-					flex: 1;
-					display: flex;
-					flex-direction: column;
-					gap: 8rpx;
-
-					.auditor_item_name {
-						font-size: 30rpx;
-						color: #333333;
-						font-weight: 500;
-					}
-
-					.auditor_item_account {
-						font-size: 24rpx;
-						color: #999999;
-					}
-				}
-
-				.auditor_item_check {
-					width: 44rpx;
-					height: 44rpx;
-					display: flex;
-					align-items: center;
-					justify-content: center;
-				}
-			}
-		}
-	}
-
-	.auditor_dialog_footer {
-		display: flex;
-		gap: 24rpx;
-		padding-top: 24rpx;
-		margin-top: 12rpx;
-		border-top: 2rpx solid #F3F3F3;
-
-		.auditor_dialog_btn {
-			flex: 1;
-			height: 88rpx;
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			border-radius: 12rpx;
-			font-size: 30rpx;
-			font-weight: 500;
-		}
-
-		.cancel_btn {
-			background: #F0F2F9;
-			color: #606266;
-		}
-
-		.confirm_btn {
-			background: #2E64FA;
-			color: #FFFFFF;
-		}
-	}
-}
-
-.publish_footer {
-	padding: 24rpx;
-	background-color: #FFFFFF;
-
-	.confirm_btn {
-		width: 702rpx;
-		height: 90rpx;
-		background: #2E64FA;
-		border-radius: 8rpx 8rpx 8rpx 8rpx;
-		text-align: center;
-		line-height: 90rpx;
-		.btn_text {
-			font-size: 32rpx;
-			font-weight: 500;
-			color: #FFFFFF;
-		}
-	}
-}
-</style>

+ 0 - 178
pages/materialCollection/taskOverview/components/personList.vue

@@ -1,178 +0,0 @@
-<template>
-	<view class="page_person_list">
-		<CommonHeader :title="headerTitle" @back="goBack"></CommonHeader>
-
-		<SearchBar v-model="searchWord" @search="handleSearch" :isFixed="true" :top="searchBarTop + 'rpx'" :border="false"></SearchBar>
-
-		<scroll-view scroll-y class="person_list" :style="{ paddingTop: contentPaddingTop + 'rpx' }">
-			<view v-for="(person, index) in filteredList" :key="person.userId || person.id || index" class="person_item">
-				<view class="person_info">
-					<text class="person_name">{{ person.thirdName || person.name }}</text>
-					<text class="person_code">({{ person.thirdCode || person.userAccount || '' }})</text>
-				</view>
-				<view class="person_action" @click="openDeleteModal(index)">
-					<text class="delete_text">删除</text>
-				</view>
-			</view>
-
-			<view v-if="filteredList.length === 0" class="empty_state">
-				<text class="empty_text">暂无人员</text>
-			</view>
-		</scroll-view>
-
-		<ConfirmModal
-			:visible="showDeleteModal"
-			title="提示"
-			:content="`确定要删除【${deletePersonName}】吗?`"
-			cancelText="取消"
-			confirmText="确定"
-			@cancel="closeDeleteModal"
-			@confirm="confirmDelete"
-		></ConfirmModal>
-	</view>
-</template>
-
-<script setup>
-import { ref, computed, onMounted } from 'vue';
-import CommonHeader from '@/components/commonHeader.vue';
-import SearchBar from '@/components/searchBar.vue';
-import ConfirmModal from '@/components/confirmModal.vue';
-import { useSafeArea } from '@/common/safeArea';
-
-const { statusBarHeight, initSafeArea } = useSafeArea();
-
-const HEADER_CONTENT_HEIGHT = 174;
-const SEARCHBAR_HEIGHT = 60;
-const SPACING = 24;
-
-const searchBarTop = computed(() => statusBarHeight.value + HEADER_CONTENT_HEIGHT);
-const contentPaddingTop = computed(() => statusBarHeight.value + HEADER_CONTENT_HEIGHT + SEARCHBAR_HEIGHT + SPACING);
-
-const searchWord = ref('');
-
-const personList = ref([]);
-const objectTypeLabel = ref('');
-
-const showDeleteModal = ref(false);
-const deleteIndex = ref(-1);
-const deletePersonName = ref('');
-
-const filteredList = computed(() => {
-	if (!searchWord.value) return personList.value;
-	const keyword = searchWord.value.toLowerCase();
-	return personList.value.filter(person => {
-		const name = (person.thirdName || person.name || '').toLowerCase();
-		const code = (person.thirdCode || person.userAccount || '').toLowerCase();
-		return name.includes(keyword) || code.includes(keyword);
-	});
-});
-
-const headerTitle = computed(() => {
-	return `${objectTypeLabel.value || ''} 已选${personList.value.length}人`;
-});
-
-const handleSearch = (keyword) => {
-	searchWord.value = keyword;
-};
-
-const openDeleteModal = (index) => {
-	const person = personList.value[index];
-	deletePersonName.value = person.thirdName || person.name || '';
-	deleteIndex.value = index;
-	showDeleteModal.value = true;
-};
-
-const closeDeleteModal = () => {
-	showDeleteModal.value = false;
-	deleteIndex.value = -1;
-	deletePersonName.value = '';
-};
-
-const confirmDelete = () => {
-	if (deleteIndex.value >= 0) {
-		personList.value.splice(deleteIndex.value, 1);
-	}
-	closeDeleteModal();
-};
-
-const goBack = () => {
-	uni.setStorageSync('personListResult', JSON.stringify({
-		users: personList.value
-	}));
-	uni.navigateBack();
-};
-
-onMounted(() => {
-	initSafeArea();
-
-	const data = uni.getStorageSync('personListData');
-	if (data) {
-		try {
-			const parsed = JSON.parse(data);
-			// 不去重,直接使用所有用户
-			personList.value = parsed.users || [];
-			objectTypeLabel.value = parsed.tab || '';
-		} catch (e) {
-			console.error('解析人员列表数据失败:', e);
-		}
-	}
-});
-</script>
-
-<style lang="scss">
-.page_person_list {
-	display: flex;
-	flex-direction: column;
-	min-height: 100vh;
-}
-
-.person_list {
-	flex: 1;
-	padding-left: 24rpx;
-	padding-right: 24rpx;
-	padding-bottom: 24rpx;
-	box-sizing: border-box;
-}
-
-.person_item {
-	display: flex;
-	align-items: center;
-	justify-content: space-between;
-	height: 86rpx;
-	border-bottom: 2rpx solid #F3F3F3;
-	.person_info {
-		display: flex;
-		align-items: center;
-
-		.person_name {
-			font-size: 28rpx;
-			color: #666;
-		}
-
-		.person_code {
-			font-size: 28rpx;
-			color: #666;
-			margin-left: 16rpx;
-		}
-	}
-
-	.person_action {
-		.delete_text {
-			font-size: 28rpx;
-			color: #F56C6C;
-		}
-	}
-}
-
-.empty_state {
-	display: flex;
-	flex-direction: column;
-	align-items: center;
-	padding: 100rpx 0;
-
-	.empty_text {
-		font-size: 28rpx;
-		color: #999999;
-	}
-}
-</style>

+ 0 - 674
pages/materialCollection/taskOverview/components/selectObjectNoDedup.vue

@@ -1,674 +0,0 @@
-<template>
-	<view class="page_select_object">
-		<CommonHeader title="选择通知对象" @back="goBack"></CommonHeader>
-
-		<view class="tabs_container" :style="{ top: tabsTop + 'rpx' }">
-			<view v-for="tab in tabs" :key="tab.key" class="tab_item" :class="{ active: activeTab === tab.key, disabled: editMode }"
-				@click="!editMode && handleTabChange(tab.key)">
-				<text class="tab_text">{{ tab.label }}</text>
-			</view>
-		</view>
-
-		<SearchBar v-model="searchKeyword" @search="searchTree" :border="true" :isFixed="true" :top="searchBarTop + 'rpx'" :zIndex="100"></SearchBar>
-
-		<scroll-view scroll-y class="content_list" :style="{ paddingTop: contentPaddingTop + 'rpx' }">
-			<TreeNodeNoDedup :tree-data="treeData" :expanded-ids="expandedIds" @toggle-expand="toggleExpand"
-				@toggle-select="toggleSelect" />
-		</scroll-view>
-
-		<view class="select_footer">
-			<view class="confirm_btn" @click="handleConfirm">
-				<text class="btn_text">确定(已选{{ selectedCount }}人)</text>
-			</view>
-		</view>
-	</view>
-</template>
-
-<script setup>
-import { ref, computed, onMounted } from 'vue';
-import TreeNodeNoDedup from '@/components/treeNodeNoDedup.vue';
-import SearchBar from '@/components/searchBar.vue';
-import CommonHeader from '@/components/commonHeader.vue';
-import overview from '@/reqApi/overview.js';
-import { useSafeArea } from '@/common/safeArea';
-
-const { statusBarHeight, initSafeArea } = useSafeArea();
-
-const HEADER_CONTENT_HEIGHT = 130;
-const TABS_HEIGHT = 120;
-const SEARCHBAR_HEIGHT = 84;
-const SPACING = 16;
-
-const tabsTop = computed(() => statusBarHeight.value + HEADER_CONTENT_HEIGHT);
-const searchBarTop = computed(() => statusBarHeight.value + HEADER_CONTENT_HEIGHT + TABS_HEIGHT);
-const contentPaddingTop = computed(() => statusBarHeight.value + HEADER_CONTENT_HEIGHT + TABS_HEIGHT + SEARCHBAR_HEIGHT + SPACING);
-
-const activeTab = ref('department');
-const searchKeyword = ref('');
-const schoolYearId = ref('');
-const editMode = ref(false);
-const noticeType = ref(0);
-const selectedUserIds = ref([]);
-
-const tabs = [
-	{ key: 'department', label: '按部门' },
-	{ key: 'subject', label: '按学科' },
-	{ key: 'grade', label: '按年级' },
-	{ key: 'permission', label: '按权限' },
-	{ key: 'student', label: '按学生' }
-];
-
-const treeData = ref([]);
-const fullTreeData = ref([]);
-const expandedIds = ref([]);
-
-// 转换按部门接口数据为树形结构(不去重)
-const transformDepartmentData = (data) => {
-	if (!data || !Array.isArray(data)) return [];
-	
-	return data.map(dept => {
-		const groups = (dept.groupVOS || []).map(group => ({
-			id: `${dept.id}_${group.groupId}`,
-			name: group.groupName,
-			checked: false,
-			halfChecked: false,
-			children: (group.groupUserVOS || []).map(user => ({
-				id: user.userId,
-				name: user.teacherName,
-				userId: user.userId,
-				userAccount: user.userAccount,
-				checked: false,
-				halfChecked: false
-			}))
-		}));
-		
-		return {
-			id: dept.id,
-			name: dept.name,
-			checked: false,
-			halfChecked: false,
-			children: groups
-		};
-	});
-};
-
-// 转换按学科的数据为树形结构(不去重)
-const transformSubjectData = (data) => {
-	if (!data || !Array.isArray(data)) return [];
-	
-	return data.map(subject => {
-		const grades = (subject.gradePersonVos || []).map(grade => ({
-			id: `${subject.subjectCode}_${grade.schoolYearGradeId || grade.gradeCode}`,
-			name: grade.gradeName,
-			checked: false,
-			halfChecked: false,
-			children: (grade.personVoList || []).map(user => ({
-				id: user.userId,
-				name: user.teacherName,
-				userId: user.userId,
-				userAccount: user.userAccount,
-				checked: false,
-				halfChecked: false
-			}))
-		}));
-		
-		return {
-			id: `subject_${subject.subjectCode}`,
-			name: subject.subjectName,
-			checked: false,
-			halfChecked: false,
-			children: grades
-		};
-	});
-};
-
-// 转换按年级的数据为树形结构(不去重)
-const transformGradeData = (data) => {
-	if (!data || !Array.isArray(data)) return [];
-	
-	return data.map(grade => {
-		const classTypes = (grade.classTypePersonVoList || []).map(classType => {
-			const classes = (classType.classInfoPersonVoList || []).map(cls => ({
-				id: `${classType.classType}_${cls.classCode}`,
-				name: cls.className,
-				checked: false,
-				halfChecked: false,
-				children: (cls.personVoList || []).map(user => ({
-					id: user.userId,
-					name: user.teacherName,
-					userId: user.userId,
-					userAccount: user.userAccount,
-					checked: false,
-					halfChecked: false
-				}))
-			}));
-			
-			return {
-				id: `${grade.schoolYearGradeId || grade.gradeCode}_${classType.classType}`,
-				name: classType.classTypeName,
-				checked: false,
-				halfChecked: false,
-				children: classes
-			};
-		});
-		
-		return {
-			id: grade.schoolYearGradeId || grade.gradeCode,
-			name: grade.gradeName,
-			checked: false,
-			halfChecked: false,
-			children: classTypes
-		};
-	});
-};
-
-// 转换按权限的数据为树形结构(不去重)
-const transformPersonalData = (data) => {
-	if (!data || !Array.isArray(data)) return [];
-	
-	return data.map(item => {
-		return {
-			id: item.roleId,
-			name: item.roleName,
-			checked: false,
-			halfChecked: false,
-			children: (item.personVoList || []).map(user => ({
-				id: user.userId,
-				name: user.teacherName,
-				userId: user.userId,
-				userAccount: user.userAccount,
-				checked: false,
-				halfChecked: false
-			}))
-		};
-	});
-};
-
-// 转换按学生的数据为树形结构(不去重)
-const transformStudentData = (data) => {
-	if (!data || !Array.isArray(data)) return [];
-	
-	return data.map(grade => {
-		const classes = (grade.clsVOS || []).map(cls => ({
-			id: `${grade.schoolYearGradeId || grade.gradeCode}_${cls.classId}`,
-			name: cls.className,
-			checked: false,
-			halfChecked: false,
-			children: (cls.studentTblVOS || []).map(student => ({
-				id: student.userId,
-				name: student.studentName,
-				userId: student.userId,
-				userAccount: student.studentCode,
-				checked: false,
-				halfChecked: false
-			}))
-		}));
-		
-		return {
-			id: `student_${grade.schoolYearGradeId || grade.gradeCode}`,
-			name: grade.gradeName,
-			checked: false,
-			halfChecked: false,
-			children: classes
-		};
-	});
-};
-
-// 获取学年
-const getSchoolYear = async () => {
-	try {
-		const res = await overview.findSchoolYear();
-		if (res.data && res.data.length > 0) {
-			schoolYearId.value = res.data[0].id;
-		}
-	} catch (error) {
-		console.error('获取学年失败:', error);
-	}
-};
-
-// 获取按部门数据
-const loadDepartmentData = async () => {
-	if (!schoolYearId.value) return;
-	
-	try {
-		const res = await overview.find_depart_contain_user({ schoolYearId: schoolYearId.value });
-		fullTreeData.value = transformDepartmentData(res.data);
-		treeData.value = fullTreeData.value;
-		expandedIds.value = [];
-	} catch (error) {
-		console.error('获取部门数据失败:', error);
-	}
-};
-
-// 获取按学科/年级/权限数据
-const loadPersonalData = async (type) => {
-	if (!schoolYearId.value) return;
-	
-	try {
-		const res = await overview.find_collect_personal_info({ schoolYearId: schoolYearId.value });
-		
-		let data = [];
-		let transformFn = transformPersonalData;
-		
-		if (type === 'subject') {
-			data = res.data.subjectPersonVoList || [];
-			transformFn = transformSubjectData;
-		} else if (type === 'grade') {
-			data = res.data.collectGradePersonVos || [];
-			transformFn = transformGradeData;
-		} else if (type === 'permission') {
-			data = res.data.collectPermissionPersonVos || [];
-		}
-		
-		fullTreeData.value = transformFn(data);
-		treeData.value = fullTreeData.value;
-		expandedIds.value = [];
-	} catch (error) {
-		console.error(`获取${type}数据失败:`, error);
-	}
-};
-
-// 获取按学生数据
-const loadStudentData = async () => {
-	if (!schoolYearId.value) return;
-	
-	try {
-		const res = await overview.query_notice_student({ id: 0, schoolYearId: schoolYearId.value });
-		fullTreeData.value = transformStudentData(res.data || []);
-		treeData.value = fullTreeData.value;
-		expandedIds.value = [];
-	} catch (error) {
-		console.error('获取学生数据失败:', error);
-	}
-};
-
-// 切换标签
-const handleTabChange = async (key) => {
-	activeTab.value = key;
-	
-	if (key === 'department') {
-		await loadDepartmentData();
-	} else if (key === 'subject' || key === 'grade' || key === 'permission') {
-		await loadPersonalData(key);
-	} else if (key === 'student') {
-		await loadStudentData();
-	}
-};
-
-const searchTree = (keyword) => {
-	if (!keyword.trim()) {
-		treeData.value = fullTreeData.value;
-		return;
-	}
-	
-	const filterNodes = (nodes) => {
-		const filtered = [];
-		nodes.forEach(node => {
-			const hasMatch = node.name && node.name.includes(keyword);
-			let childrenFiltered = [];
-			
-			if (node.children && node.children.length > 0) {
-				childrenFiltered = filterNodes(node.children);
-			}
-			
-			if (hasMatch || childrenFiltered.length > 0) {
-				filtered.push({
-					...node,
-					children: childrenFiltered
-				});
-			}
-		});
-		return filtered;
-	};
-	
-	treeData.value = filterNodes(fullTreeData.value);
-};
-
-const toggleExpand = ({ itemId, siblingIds, isExpanded }) => {
-	if (isExpanded) {
-		expandedIds.value = expandedIds.value.filter(id => id !== itemId);
-	} else {
-		expandedIds.value = expandedIds.value.filter(id => {
-			if (siblingIds.includes(id)) {
-				return false;
-			}
-			return true;
-		});
-		expandedIds.value = [...expandedIds.value, itemId];
-	}
-};
-
-// 向下级联:更新所有子节点的选中状态
-const toggleChildren = (children, checked) => {
-	children.forEach(child => {
-		child.checked = checked;
-		child.halfChecked = false;
-		if (child.children && child.children.length > 0) {
-			toggleChildren(child.children, checked);
-		}
-	});
-};
-
-// 向上级联:更新父节点的选中状态
-const updateParentStatus = () => {
-	const updateNode = (nodes) => {
-		nodes.forEach(node => {
-			if (node.children && node.children.length > 0) {
-				updateNode(node.children);
-
-				const childrenChecked = node.children.filter(c => c.checked);
-				const childrenHalfChecked = node.children.filter(c => c.halfChecked);
-
-				if (childrenChecked.length === node.children.length) {
-					node.checked = true;
-					node.halfChecked = false;
-				} else if (childrenChecked.length > 0 || childrenHalfChecked.length > 0) {
-					node.checked = false;
-					node.halfChecked = true;
-				} else {
-					node.checked = false;
-					node.halfChecked = false;
-				}
-			}
-		});
-	};
-	updateNode(treeData.value);
-};
-
-const toggleSelect = (item) => {
-	item.checked = !item.checked;
-	item.halfChecked = false;
-
-	if (item.children && item.children.length > 0) {
-		// 向下级联
-		toggleChildren(item.children, item.checked);
-	}
-
-	// 向上级联
-	updateParentStatus();
-};
-
-const selectedCount = computed(() => {
-	let count = 0;
-	const collectChecked = (items) => {
-		items.forEach(item => {
-			if (item.children && item.children.length > 0) {
-				collectChecked(item.children);
-			} else if (item.checked && item.userId) {
-				count++;
-			}
-		});
-	};
-	collectChecked(treeData.value);
-	return count;
-});
-
-const goBack = () => {
-	uni.navigateBack();
-};
-
-const collectSelectedUsers = () => {
-	const selectedUsers = [];
-	const currentTab = activeTab.value;
-
-	const collect = (nodes, parentInfo = {}) => {
-		nodes.forEach(node => {
-			if (node.children && node.children.length > 0) {
-				const newParentInfo = { ...parentInfo };
-				if (node.name && !node.userId) {
-					if (currentTab === 'department') {
-						if (!newParentInfo.departmentName) {
-							newParentInfo.departmentName = node.name;
-						} else if (!newParentInfo.groupName) {
-							newParentInfo.groupName = node.name;
-						}
-					} else if (currentTab === 'subject') {
-						if (!newParentInfo.subjectName) {
-							newParentInfo.subjectName = node.name;
-						} else if (!newParentInfo.gradeName) {
-							newParentInfo.gradeName = node.name;
-						}
-					} else if (currentTab === 'grade') {
-						if (!newParentInfo.gradeName) {
-							newParentInfo.gradeName = node.name;
-						} else if (!newParentInfo.classTypeName) {
-							newParentInfo.classTypeName = node.name;
-						} else if (!newParentInfo.className) {
-							newParentInfo.className = node.name;
-						}
-					} else if (currentTab === 'permission') {
-						if (!newParentInfo.roleName) {
-							newParentInfo.roleName = node.name;
-						}
-					} else if (currentTab === 'student') {
-						if (!newParentInfo.gradeName) {
-							newParentInfo.gradeName = node.name;
-						} else if (!newParentInfo.classTypeName) {
-							newParentInfo.classTypeName = node.name;
-						} else if (!newParentInfo.className) {
-							newParentInfo.className = node.name;
-						}
-					}
-				}
-				collect(node.children, newParentInfo);
-			} else if (node.checked && node.userId) {
-				selectedUsers.push({
-					id: node.id,
-					name: node.name,
-					userId: node.userId,
-					thirdCode: node.userAccount || '',
-					departmentName: parentInfo.departmentName || '',
-					groupName: parentInfo.groupName || '',
-					subjectName: parentInfo.subjectName || '',
-					gradeName: parentInfo.gradeName || '',
-					classTypeName: parentInfo.classTypeName || '',
-					className: parentInfo.className || '',
-					roleName: parentInfo.roleName || ''
-				});
-			}
-		});
-	};
-	collect(treeData.value);
-	return selectedUsers;
-};
-
-const handleConfirm = () => {
-	const data = {
-		tab: tabs.find(t => t.key === activeTab.value).label,
-		count: selectedCount.value,
-		users: collectSelectedUsers()
-	};
-	
-	uni.setStorageSync('selectObjectResult', JSON.stringify(data));
-	uni.navigateBack();
-};
-
-const setSelectedUsers = (users) => {
-	if (!users || !Array.isArray(users)) return;
-	
-	const selectedTeacherIds = new Set(users.map(u => u.teacherId));
-	
-	const setNode = (nodes) => {
-		nodes.forEach(node => {
-			if (node.children && node.children.length > 0) {
-				setNode(node.children);
-			} else if (node.teacherId && selectedTeacherIds.has(node.teacherId)) {
-				node.checked = true;
-			}
-		});
-	};
-	
-	setNode(treeData.value);
-	updateParentStatus();
-};
-
-const setSelectedUsersById = (userIds) => {
-	if (!userIds || !Array.isArray(userIds)) return;
-	
-	const selectedIds = new Set(userIds);
-	
-	const setNode = (nodes) => {
-		nodes.forEach(node => {
-			if (node.children && node.children.length > 0) {
-				setNode(node.children);
-			} else if (node.id && selectedIds.has(node.id)) {
-				node.checked = true;
-			}
-		});
-	};
-	
-	setNode(treeData.value);
-	updateParentStatus();
-};
-
-onMounted(async () => {
-	initSafeArea();
-
-	const pages = getCurrentPages();
-	if (pages.length === 1) {
-		uni.navigateBack();
-		return;
-	}
-	
-	const lastSelection = uni.getStorageSync('selectObjectLastSelection');
-	if (lastSelection) {
-		try {
-			const data = JSON.parse(lastSelection);
-			
-			if (data.isEdit) {
-				editMode.value = true;
-			}
-			
-			if (data.noticeType) {
-				noticeType.value = data.noticeType;
-				const typeMap = {
-					1: 'department',
-					2: 'subject',
-					3: 'grade',
-					4: 'permission',
-					5: 'student'
-				};
-				const tabKey = typeMap[data.noticeType];
-				if (tabKey) {
-					activeTab.value = tabKey;
-				}
-			} else if (data.tab) {
-				const tab = tabs.find(t => t.label === data.tab);
-				if (tab) {
-					activeTab.value = tab.key;
-				}
-			}
-			
-			if (data.users) {
-				selectedUserIds.value = data.users.map(u => u.userId || u.id);
-			}
-		} catch (e) {
-			console.error('解析上次选择失败:', e);
-		}
-	}
-	
-	await getSchoolYear();
-	
-	if (activeTab.value === 'department') {
-		await loadDepartmentData();
-	} else if (activeTab.value === 'subject' || activeTab.value === 'grade' || activeTab.value === 'permission') {
-		await loadPersonalData(activeTab.value);
-	} else if (activeTab.value === 'student') {
-		await loadStudentData();
-	}
-	
-	if (selectedUserIds.value.length > 0) {
-		setSelectedUsersById(selectedUserIds.value);
-	} else if (lastSelection) {
-		try {
-			const data = JSON.parse(lastSelection);
-			if (data.users) {
-				setSelectedUsers(data.users);
-			}
-		} catch (e) {
-			console.error('设置选中状态失败:', e);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.page_select_object {
-	display: flex;
-	flex-direction: column;
-	min-height: 100vh;
-}
-
-.tabs_container {
-	position: fixed;
-	left: 0;
-	right: 0;
-	z-index: 99;
-	display: flex;
-	padding: 24rpx ;
-	background-color: #FFFFFF;
-	justify-content: space-between;
-
-	.tab_item {
-		flex-shrink: 0;
-		width: 122rpx;
-		height: 72rpx;
-		background: #FFFFFF;
-		border-radius: 8rpx;
-		border: 2rpx solid #DCDFE6;
-		display: flex;
-		align-items: center;
-		justify-content: center;
-
-		&.active {
-			background: #2E64FA;
-			border-color: #2E64FA;
-
-			.tab_text {
-				color: #FFFFFF;
-			}
-		}
-
-		.tab_text {
-			font-weight: 400;
-			font-size: 28rpx;
-			color: #333333;
-		}
-
-		&.disabled {
-			pointer-events: none;
-			opacity: 0.5;
-		}
-	}
-}
-
-.content_list {
-	flex: 1;
-	padding-bottom: 150rpx;
-}
-
-.select_footer {
-	position: fixed;
-	bottom: 0;
-	left: 0;
-	right: 0;
-	padding: 24rpx;
-	background-color: #FFFFFF;
-	border-top: 2rpx solid #F3F3F3;
-
-	.confirm_btn {
-		width: 100%;
-		height: 90rpx;
-		background: #2E64FA;
-		border-radius: 8rpx;
-		text-align: center;
-		line-height: 90rpx;
-
-		.btn_text {
-			font-size: 32rpx;
-			font-weight: 500;
-			color: #FFFFFF;
-		}
-	}
-}
-</style>

+ 3 - 1
pages/materialCollection/taskOverview/index.vue

@@ -1,4 +1,4 @@
-<template>
+<template>
 	<!-- 头部 -->
 	<nav-bar height="328" backPath="workspace" title="材料采集" :filterPickerData="state.filterPickerData" :tabList="state.tabList" :onlyTitle="false" :showFilterPicker="true" :showTabs="true" rightWidth="0rpx" @CommonFilterData="CommonFilterData">
 		<template #tabs_right_content>
@@ -310,6 +310,7 @@ const DialogDelConfirm = () => {
 }
 //新建、编辑
 const handleAddEdit = (type,item) => {
+	uni.removeStorageSync('taskTargetsTreeData');//移除缓存
 	uni.navigateTo({
 		url: `/pages/materialCollection/taskOverview/addEdit?taskId=${item?.id || ''}`
 	});
@@ -334,6 +335,7 @@ onShow(()=>{
 })
 onUnmounted(()=>{
 	uni.setStorageSync('isLoadCollectTaskOverview', false);
+	uni.removeStorageSync('taskTargetsTreeData');//移除缓存
 })
 </script>
 

+ 135 - 160
pages/materialCollection/taskOverview/personList.vue

@@ -1,178 +1,153 @@
 <template>
-	<view class="page_person_list">
-		<CommonHeader :title="headerTitle" @back="goBack"></CommonHeader>
-
-		<SearchBar v-model="searchWord" @search="handleSearch" :isFixed="true" :top="searchBarTop + 'rpx'" :border="false"></SearchBar>
-
-		<scroll-view scroll-y class="person_list" :style="{ paddingTop: contentPaddingTop + 'rpx' }">
-			<view v-for="(person, index) in filteredList" :key="person.userId || person.id || index" class="person_item">
-				<view class="person_info">
-					<text class="person_name">{{ person.thirdName || person.name }}</text>
-					<text class="person_code">({{ person.thirdCode || person.userAccount || '' }})</text>
-				</view>
-				<view class="person_action" @click="openDeleteModal(index)">
-					<text class="delete_text">删除</text>
+	<view class="page_body">
+		<!-- 头部 -->
+		<nav-bar height="220" :title="pageTitle" :tabBtns="state.tabBtns" :onlyTitle="false" :showHeadSearchInput="false" :showFilterPicker="false" :showTabs="false" :showTabBtns="false" rightWidth="0rpx" @GoBack="GoBack">
+			<template #tabs_bottom_content>
+				<!-- 搜索框单独一行 -->
+				<view class="nav_head row_search_box">
+					<uni-easyinput class="search_box" v-model="state.searchWord" trim="all" :styles="state.searchInputStyles"
+						:placeholderStyle="state.searchInputPlaceholderStyle" placeholder="请输入关键字搜索" @input="HandleSearchInput">
+						<template #left>
+							<image src="@/static/image/overview/search.png" class="search_icon"></image>
+						</template>
+					</uni-easyinput>
 				</view>
+			</template>
+		</nav-bar>
+		<view class="person_list">
+			<view class="list_item" v-for="(item,index) in state.listData" :key="item.id">
+				<view class="item_name">{{item.label}}</view>
+				<view :class="['btn_del',{'disabled':item.disabled}]" @click="handleDel(item.disabled,index,item.id,item.targetName)">删除</view>
 			</view>
-
-			<view v-if="filteredList.length === 0" class="empty_state">
-				<text class="empty_text">暂无人员</text>
-			</view>
-		</scroll-view>
-
-		<ConfirmModal
-			:visible="showDeleteModal"
-			title="提示"
-			:content="`确定要删除【${deletePersonName}】吗?`"
-			cancelText="取消"
-			confirmText="确定"
-			@cancel="closeDeleteModal"
-			@confirm="confirmDelete"
-		></ConfirmModal>
+		</view>
+		<!-- 删除确认框 -->
+		<popupDialog ref="popupDelDialogRef" :showFootBorder="true" :content="state.popupDelDialog.content" @DialogConfirm="DialogDelConfirm" />
 	</view>
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue';
-import CommonHeader from '@/components/commonHeader.vue';
-import SearchBar from '@/components/searchBar.vue';
-import ConfirmModal from '@/components/confirmModal.vue';
-import { useSafeArea } from '@/common/safeArea';
-
-const { statusBarHeight, initSafeArea } = useSafeArea();
-
-const HEADER_CONTENT_HEIGHT = 174;
-const SEARCHBAR_HEIGHT = 60;
-const SPACING = 24;
-
-const searchBarTop = computed(() => statusBarHeight.value + HEADER_CONTENT_HEIGHT);
-const contentPaddingTop = computed(() => statusBarHeight.value + HEADER_CONTENT_HEIGHT + SEARCHBAR_HEIGHT + SPACING);
-
-const searchWord = ref('');
-
-const personList = ref([]);
-const objectTypeLabel = ref('');
-
-const showDeleteModal = ref(false);
-const deleteIndex = ref(-1);
-const deletePersonName = ref('');
-
-const filteredList = computed(() => {
-	if (!searchWord.value) return personList.value;
-	const keyword = searchWord.value.toLowerCase();
-	return personList.value.filter(person => {
-		const name = (person.thirdName || person.name || '').toLowerCase();
-		const code = (person.thirdCode || person.userAccount || '').toLowerCase();
-		return name.includes(keyword) || code.includes(keyword);
+	import navBar from '@/components/navBar.vue';
+	import {onLoad} from '@dcloudio/uni-app';
+	import {reactive,ref,onUnmounted, computed} from 'vue';
+	import popupDialog from '@/components/popupDialog.vue';
+	const state = reactive({
+		listData:[],
+		allListData:[],
+		popupDelDialog:{//删除确认框
+			content:'',
+			index:'',
+			id:'',
+			targetName:''
+		}
 	});
-});
-
-const headerTitle = computed(() => {
-	return `${objectTypeLabel.value || ''} 已选${personList.value.length}人`;
-});
-
-const handleSearch = (keyword) => {
-	searchWord.value = keyword;
-};
-
-const openDeleteModal = (index) => {
-	const person = personList.value[index];
-	deletePersonName.value = person.thirdName || person.name || '';
-	deleteIndex.value = index;
-	showDeleteModal.value = true;
-};
-
-const closeDeleteModal = () => {
-	showDeleteModal.value = false;
-	deleteIndex.value = -1;
-	deletePersonName.value = '';
-};
-
-const confirmDelete = () => {
-	if (deleteIndex.value >= 0) {
-		personList.value.splice(deleteIndex.value, 1);
+	const targetType = ref('');//采集对象 1-按部门,2-按学科,3-按年级,4-按权限,5-按学生
+	const popupDelDialogRef = ref('');
+	onLoad((option) => {
+		const taskTargetsTreeData = uni.getStorageSync('taskTargetsTreeData');
+		targetType.value = taskTargetsTreeData.targetType;
+		const listData = taskTargetsTreeData?.taskTargets || [];
+		state.listData = listData;
+		state.allListData = listData;
+	})
+	const pageTitle = computed(() => {
+		const num = state.allListData?.length || 0;
+		if(targetType.value == 1){
+			return `按部门 已选${num}人`
+		}else if(targetType.value == 2){
+			return `按学科 已选${num}人`
+		}else if(targetType.value == 3){
+			return `按年级 已选${num}人`
+		}else if(targetType.value == 4){
+			return `按权限 已选${num}人`
+		}else{
+			return ''
+		}
+	});
+	onUnmounted(()=>{
+		uni.removeStorageSync('taskTargetsTreeData');//移除缓存
+	})
+	//搜索
+	const HandleSearchInput = (e) => {
+		const searchWord = (e || '').trim();
+		if (!searchWord) {
+			state.listData = [...state.allListData]; // 清空恢复全量
+			return;
+		}
+		state.listData = state.allListData.filter(item=>item.label.indexOf(searchWord) > -1)
 	}
-	closeDeleteModal();
-};
-
-const goBack = () => {
-	uni.setStorageSync('personListResult', JSON.stringify({
-		users: personList.value
-	}));
-	uni.navigateBack();
-};
-
-onMounted(() => {
-	initSafeArea();
-
-	const data = uni.getStorageSync('personListData');
-	if (data) {
-		try {
-			const parsed = JSON.parse(data);
-			// 不去重,直接使用所有用户
-			personList.value = parsed.users || [];
-			objectTypeLabel.value = parsed.tab || '';
-		} catch (e) {
-			console.error('解析人员列表数据失败:', e);
+	//删除
+	const handleDel = (disabled,index,id,targetName) => {
+		if(disabled){
+			return false
 		}
+		state.popupDelDialog.index = id;
+		state.popupDelDialog.id = id;
+		state.popupDelDialog.targetName = targetName;
+		state.popupDelDialog.content = `确定要删除【${targetName}】吗?`;
+		popupDelDialogRef.value.OpenDialog();
+	}
+	const DialogDelConfirm = () => {
+		state.listData.splice(state.popupDelDialog.index,1);
+		const key = state.allListData.findIndex(item=>item.id==state.popupDelDialog.id);
+		if (key !== -1) {
+			state.allListData.splice(key, 1);
+		} 
+		uni.setStorageSync('taskTargetsTreeData',{
+			targetType:Number(targetType.value),
+			taskTargets:state.allListData,//已选中
+		});
+	}
+	//返回
+	const GoBack = () => {
+		uni.navigateBack({
+			delta: 1
+		});
 	}
-});
 </script>
 
-<style lang="scss">
-.page_person_list {
-	display: flex;
-	flex-direction: column;
-	min-height: 100vh;
-}
-
-.person_list {
-	flex: 1;
-	padding-left: 24rpx;
-	padding-right: 24rpx;
-	padding-bottom: 24rpx;
-	box-sizing: border-box;
-}
-
-.person_item {
-	display: flex;
-	align-items: center;
-	justify-content: space-between;
-	height: 86rpx;
-	border-bottom: 2rpx solid #F3F3F3;
-	.person_info {
+<style scoped lang="scss">
+	.page_body{
 		display: flex;
-		align-items: center;
-
-		.person_name {
-			font-size: 28rpx;
-			color: #666;
+		flex-direction: column;
+		height: 100%;
+		min-width: auto;
+		.row_search_box{
+			height: 96rpx;
+			align-items: flex-start;
 		}
-
-		.person_code {
-			font-size: 28rpx;
-			color: #666;
-			margin-left: 16rpx;
+		.person_list{
+			display: flex;
+			flex-direction: column;
+			width: 100%;
+			padding: 0 24rpx;
+			box-sizing: border-box;
+			flex:1;
+			overflow: auto;
+			align-items: flex-start;
+			.list_item{
+				display: flex;
+				width: 100%;
+				justify-content: space-between;
+				padding: 24rpx 0;
+				border-bottom: 2rpx solid #F3F3F3;
+				.item_name{
+					font-weight: 400;
+					font-size: 28rpx;
+					color: #666666;
+					flex:1;
+					white-space: nowrap;
+					overflow: hidden;
+					text-overflow: ellipsis;
+				}
+				.btn_del{
+					font-weight: 400;
+					font-size: 28rpx;
+					color: #F56C6C;
+					&.disabled{
+						color: #C0C4CC;
+					}
+				}
+			}
 		}
 	}
-
-	.person_action {
-		.delete_text {
-			font-size: 28rpx;
-			color: #F56C6C;
-		}
-	}
-}
-
-.empty_state {
-	display: flex;
-	flex-direction: column;
-	align-items: center;
-	padding: 100rpx 0;
-
-	.empty_text {
-		font-size: 28rpx;
-		color: #999999;
-	}
-}
 </style>

+ 196 - 643
pages/materialCollection/taskOverview/selectObjectNoDedup.vue

@@ -1,674 +1,227 @@
 <template>
-	<view class="page_select_object">
-		<CommonHeader title="选择通知对象" @back="goBack"></CommonHeader>
-
-		<view class="tabs_container" :style="{ top: tabsTop + 'rpx' }">
-			<view v-for="tab in tabs" :key="tab.key" class="tab_item" :class="{ active: activeTab === tab.key, disabled: editMode }"
-				@click="!editMode && handleTabChange(tab.key)">
-				<text class="tab_text">{{ tab.label }}</text>
-			</view>
+	<view class="page_body">
+		<!-- 头部 -->
+		<nav-bar :class="{'item_width':pageType=='edit'}" height="320" title="选择采集对象" :defaultTabBtnValue="targetType" :tabBtns="state.tabBtns" :onlyTitle="false" :showHeadSearchInput="false" :showFilterPicker="false" :showTabs="false" :showTabBtns="true" rightWidth="0rpx" @CommonFilterData="CommonFilterData" @GoBack="GoBack">
+			<template #tabs_bottom_content>
+				<!-- 搜索框单独一行 -->
+				<view class="nav_head row_search_box">
+					<uni-easyinput class="search_box" v-model="state.searchWord" trim="all" :styles="state.searchInputStyles"
+						:placeholderStyle="state.searchInputPlaceholderStyle" placeholder="请输入关键字搜索">
+						<template #left>
+							<image src="@/static/image/overview/search.png" class="search_icon"></image>
+						</template>
+					</uni-easyinput>
+				</view>
+			</template>
+		</nav-bar>
+		<!-- https://ofreshman.github.io/uni-tree-view/guide/quick-start -->
+		<!-- expand-on-click-node: 点击整行是否展开/收起-->
+		<view class="tree_node">
+			<uni-tree-view
+			    selectable
+			    multiple
+				ref="treeRef"
+				theme-color="#2E64FA"
+				v-model="state.defaultCheckedKeys"
+				:default-expand-all="true"
+				:expand-on-click-node="true"
+				:checked-disabled="true"
+				:filter-value="state.searchWord"
+				highlight-filter
+			    :data="state.treeData"
+				@check-change="HandleCheckChange"
+			>
+			<template #append="{ data,node }">
+			    <!-- data 是你传入的原始数据项,可读任意业务字段 -->
+			    <text v-if="data?.children?.length" style="color:#87918b; font-size:17rpx;">
+				  <uni-icons :type="node.expanded?'down':'right'" style="font-size: 28rpx;color: #999999;"></uni-icons>
+			    </text>
+			  </template>
+			</uni-tree-view>
 		</view>
-
-		<SearchBar v-model="searchKeyword" @search="searchTree" :border="true" :isFixed="true" :top="searchBarTop + 'rpx'" :zIndex="100"></SearchBar>
-
-		<scroll-view scroll-y class="content_list" :style="{ paddingTop: contentPaddingTop + 'rpx' }">
-			<TreeNodeNoDedup :tree-data="treeData" :expanded-ids="expandedIds" @toggle-expand="toggleExpand"
-				@toggle-select="toggleSelect" />
-		</scroll-view>
-
-		<view class="select_footer">
-			<view class="confirm_btn" @click="handleConfirm">
-				<text class="btn_text">确定(已选{{ selectedCount }}人)</text>
-			</view>
+		<view class="foot_btn">
+			<view class="btn_submit" @click="SaveCheckedTreeData">确定{{state.checkedValue.length?`(已选${state.checkedValue.length}人)`:''}}</view>
 		</view>
 	</view>
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue';
-import TreeNodeNoDedup from '@/components/treeNodeNoDedup.vue';
-import SearchBar from '@/components/searchBar.vue';
-import CommonHeader from '@/components/commonHeader.vue';
-import overview from '@/reqApi/overview.js';
-import { useSafeArea } from '@/common/safeArea';
-
-const { statusBarHeight, initSafeArea } = useSafeArea();
-
-const HEADER_CONTENT_HEIGHT = 130;
-const TABS_HEIGHT = 120;
-const SEARCHBAR_HEIGHT = 84;
-const SPACING = 16;
-
-const tabsTop = computed(() => statusBarHeight.value + HEADER_CONTENT_HEIGHT);
-const searchBarTop = computed(() => statusBarHeight.value + HEADER_CONTENT_HEIGHT + TABS_HEIGHT);
-const contentPaddingTop = computed(() => statusBarHeight.value + HEADER_CONTENT_HEIGHT + TABS_HEIGHT + SEARCHBAR_HEIGHT + SPACING);
-
-const activeTab = ref('department');
-const searchKeyword = ref('');
-const schoolYearId = ref('');
-const editMode = ref(false);
-const noticeType = ref(0);
-const selectedUserIds = ref([]);
-
-const tabs = [
-	{ key: 'department', label: '按部门' },
-	{ key: 'subject', label: '按学科' },
-	{ key: 'grade', label: '按年级' },
-	{ key: 'permission', label: '按权限' },
-	{ key: 'student', label: '按学生' }
-];
-
-const treeData = ref([]);
-const fullTreeData = ref([]);
-const expandedIds = ref([]);
-
-// 转换按部门接口数据为树形结构(不去重)
-const transformDepartmentData = (data) => {
-	if (!data || !Array.isArray(data)) return [];
-	
-	return data.map(dept => {
-		const groups = (dept.groupVOS || []).map(group => ({
-			id: `${dept.id}_${group.groupId}`,
-			name: group.groupName,
-			checked: false,
-			halfChecked: false,
-			children: (group.groupUserVOS || []).map(user => ({
-				id: user.userId,
-				name: user.teacherName,
-				userId: user.userId,
-				userAccount: user.userAccount,
-				checked: false,
-				halfChecked: false
-			}))
-		}));
-		
-		return {
-			id: dept.id,
-			name: dept.name,
-			checked: false,
-			halfChecked: false,
-			children: groups
-		};
+	import navBar from '@/components/navBar.vue';
+	import {findDepartContainUser,findCollectPersonalInfo} from '@/reqApi/notification.js';
+	import {getDeptTreeData,subjectTreeData,gradTreeData,permTreeData} from '@/common/common.js';
+	import {onLoad} from '@dcloudio/uni-app';
+	import {reactive,ref,onUnmounted,nextTick} from 'vue';
+	const state = reactive({
+		tabBtns:[{
+			label:'按部门',
+			value:1,
+		},{
+			label:'按学科',
+			value:2,
+		},{
+			label:'按年级',
+			value:3,
+		},{
+			label:'按权限',
+			value:4,
+		}],
+		defaultCheckedKeys:[],//初始选中key
+		checkedDisabled:[],
+		checkedValue:[],//勾选的项
+		treeData:[],
+		deptTreeData:[],//部门tree
+		gradeTreeList:[],//年级tree
+		permissionTreeList:[],//权限tree
+		subjectTreeList:[]//学科tree
 	});
-};
-
-// 转换按学科的数据为树形结构(不去重)
-const transformSubjectData = (data) => {
-	if (!data || !Array.isArray(data)) return [];
-	
-	return data.map(subject => {
-		const grades = (subject.gradePersonVos || []).map(grade => ({
-			id: `${subject.subjectCode}_${grade.schoolYearGradeId || grade.gradeCode}`,
-			name: grade.gradeName,
-			checked: false,
-			halfChecked: false,
-			children: (grade.personVoList || []).map(user => ({
-				id: user.userId,
-				name: user.teacherName,
-				userId: user.userId,
-				userAccount: user.userAccount,
-				checked: false,
-				halfChecked: false
-			}))
-		}));
-		
-		return {
-			id: `subject_${subject.subjectCode}`,
-			name: subject.subjectName,
-			checked: false,
-			halfChecked: false,
-			children: grades
-		};
-	});
-};
-
-// 转换按年级的数据为树形结构(不去重)
-const transformGradeData = (data) => {
-	if (!data || !Array.isArray(data)) return [];
-	
-	return data.map(grade => {
-		const classTypes = (grade.classTypePersonVoList || []).map(classType => {
-			const classes = (classType.classInfoPersonVoList || []).map(cls => ({
-				id: `${classType.classType}_${cls.classCode}`,
-				name: cls.className,
-				checked: false,
-				halfChecked: false,
-				children: (cls.personVoList || []).map(user => ({
-					id: user.userId,
-					name: user.teacherName,
-					userId: user.userId,
-					userAccount: user.userAccount,
-					checked: false,
-					halfChecked: false
-				}))
-			}));
-			
-			return {
-				id: `${grade.schoolYearGradeId || grade.gradeCode}_${classType.classType}`,
-				name: classType.classTypeName,
-				checked: false,
-				halfChecked: false,
-				children: classes
-			};
-		});
-		
-		return {
-			id: grade.schoolYearGradeId || grade.gradeCode,
-			name: grade.gradeName,
-			checked: false,
-			halfChecked: false,
-			children: classTypes
-		};
-	});
-};
-
-// 转换按权限的数据为树形结构(不去重)
-const transformPersonalData = (data) => {
-	if (!data || !Array.isArray(data)) return [];
-	
-	return data.map(item => {
-		return {
-			id: item.roleId,
-			name: item.roleName,
-			checked: false,
-			halfChecked: false,
-			children: (item.personVoList || []).map(user => ({
-				id: user.userId,
-				name: user.teacherName,
-				userId: user.userId,
-				userAccount: user.userAccount,
-				checked: false,
-				halfChecked: false
-			}))
-		};
-	});
-};
-
-// 转换按学生的数据为树形结构(不去重)
-const transformStudentData = (data) => {
-	if (!data || !Array.isArray(data)) return [];
-	
-	return data.map(grade => {
-		const classes = (grade.clsVOS || []).map(cls => ({
-			id: `${grade.schoolYearGradeId || grade.gradeCode}_${cls.classId}`,
-			name: cls.className,
-			checked: false,
-			halfChecked: false,
-			children: (cls.studentTblVOS || []).map(student => ({
-				id: student.userId,
-				name: student.studentName,
-				userId: student.userId,
-				userAccount: student.studentCode,
-				checked: false,
-				halfChecked: false
-			}))
-		}));
-		
-		return {
-			id: `student_${grade.schoolYearGradeId || grade.gradeCode}`,
-			name: grade.gradeName,
-			checked: false,
-			halfChecked: false,
-			children: classes
-		};
-	});
-};
-
-// 获取学年
-const getSchoolYear = async () => {
-	try {
-		const res = await overview.findSchoolYear();
-		if (res.data && res.data.length > 0) {
-			schoolYearId.value = res.data[0].id;
+	const schoolYearId = ref('');//学年学期id
+	const targetType = ref('');//采集对象 1-按部门,2-按学科,3-按年级,4-按权限,5-按学生
+	const pageType = ref('');//添加或修改
+	const treeRef = ref('');
+	onLoad((option) => {
+		schoolYearId.value = option.schoolYearId;
+		targetType.value = Number(option.targetType);
+		pageType.value = option.type;
+		const taskTargetsTreeData = uni.getStorageSync('taskTargetsTreeData');
+		const taskTargets = taskTargetsTreeData?.taskTargets || [];
+		state.checkedValue = taskTargets;
+		state.defaultCheckedKeys = taskTargets?.map(item=>item.id) || [];
+		state.checkedDisabled = taskTargets.filter(item=>item.disabled && item.checked=='checked');
+		if(pageType.value == 'add'){
+			GetDepartContainUser();
+			GetCollectPersonalInfo();
+		}else{//编辑
+			state.tabBtns = state.tabBtns.filter(item=>item.value == targetType.value);
+			state.treeData = taskTargetsTreeData?.taskTargetsTree || [];
 		}
-	} catch (error) {
-		console.error('获取学年失败:', error);
+	})
+	//查询本学校下的当前学年的所有部门及部门下的人
+	const GetDepartContainUser = () => {
+		findDepartContainUser(schoolYearId.value).then(res=>{
+			if(res.code == 200){
+				const resData = res.data || [];
+				state.deptTreeData = getDeptTreeData(resData);
+				CurrentTreeData();
+			}
+		})
 	}
-};
-
-// 获取按部门数据
-const loadDepartmentData = async () => {
-	if (!schoolYearId.value) return;
-	
-	try {
-		const res = await overview.find_depart_contain_user({ schoolYearId: schoolYearId.value });
-		fullTreeData.value = transformDepartmentData(res.data);
-		treeData.value = fullTreeData.value;
-		expandedIds.value = [];
-	} catch (error) {
-		console.error('获取部门数据失败:', error);
+	//查询本学校下的当前学年的所有学科、年级、权限数据及下的人
+	const GetCollectPersonalInfo = async () => {
+		const res = await findCollectPersonalInfo(schoolYearId.value);
+		const { collectGradePersonVos, collectPermissionPersonVos, subjectPersonVoList } = res.data;
+		state.gradeTreeList = gradTreeData(collectGradePersonVos);
+		state.permissionTreeList = permTreeData(collectPermissionPersonVos);
+		state.subjectTreeList = subjectTreeData(subjectPersonVoList);
+		CurrentTreeData();
 	}
-};
-
-// 获取按学科/年级/权限数据
-const loadPersonalData = async (type) => {
-	if (!schoolYearId.value) return;
-	
-	try {
-		const res = await overview.find_collect_personal_info({ schoolYearId: schoolYearId.value });
-		
-		let data = [];
-		let transformFn = transformPersonalData;
-		
-		if (type === 'subject') {
-			data = res.data.subjectPersonVoList || [];
-			transformFn = transformSubjectData;
-		} else if (type === 'grade') {
-			data = res.data.collectGradePersonVos || [];
-			transformFn = transformGradeData;
-		} else if (type === 'permission') {
-			data = res.data.collectPermissionPersonVos || [];
+	const CommonFilterData = (filterOptions,type) => {
+		if(type=='tabBtns' && pageType.value == 'add'){
+			targetType.value = filterOptions.tabBtnSelected;
+			CurrentTreeData();
 		}
-		
-		fullTreeData.value = transformFn(data);
-		treeData.value = fullTreeData.value;
-		expandedIds.value = [];
-	} catch (error) {
-		console.error(`获取${type}数据失败:`, error);
 	}
-};
-
-// 获取按学生数据
-const loadStudentData = async () => {
-	if (!schoolYearId.value) return;
-	
-	try {
-		const res = await overview.query_notice_student({ id: 0, schoolYearId: schoolYearId.value });
-		fullTreeData.value = transformStudentData(res.data || []);
-		treeData.value = fullTreeData.value;
-		expandedIds.value = [];
-	} catch (error) {
-		console.error('获取学生数据失败:', error);
+	const CurrentTreeData = () => {
+		// 1-按部门,2-按学科,3-按年级,4-按权限,5-按学生
+		if(targetType.value == 1){
+			state.treeData = [...state.deptTreeData];
+		}else if(targetType.value == 2){
+			state.treeData = [...state.subjectTreeList];
+		}else if(targetType.value == 3){
+			state.treeData = state.gradeTreeList;
+		}else if(targetType.value == 4){
+			state.treeData = state.permissionTreeList;
+		}else{
+			state.treeData = [];
+		}
 	}
-};
-
-// 切换标签
-const handleTabChange = async (key) => {
-	activeTab.value = key;
-	
-	if (key === 'department') {
-		await loadDepartmentData();
-	} else if (key === 'subject' || key === 'grade' || key === 'permission') {
-		await loadPersonalData(key);
-	} else if (key === 'student') {
-		await loadStudentData();
+	//选择的值
+	const HandleCheckChange = ({keys,node,nodes,value}) => {
+		// 不可取消的节点 key 列表
+		const lockedKeys = state.checkedDisabled.map(el=>el.id)
+		const merged = [...new Set([...keys, ...lockedKeys])];//去重
+		state.defaultCheckedKeys = merged;
+		const nodesList = nodes.filter(item => item.isLeaf).map(el => ({ ...el.source }));
+		//去重
+		state.checkedValue = [...new Map([...nodesList, ...state.checkedDisabled].map(item => [item.id, item])).values()];
 	}
-};
-
-const searchTree = (keyword) => {
-	if (!keyword.trim()) {
-		treeData.value = fullTreeData.value;
-		return;
+	//保存选择值
+	const SaveCheckedTreeData = () => {
+		if(state.checkedValue.length == 0){
+			uni.showToast({
+				title: '请选择采集对象!',
+				icon: 'none'
+			})
+			return false
+		}
+		uni.setStorageSync('taskTargetsTreeData',{
+			targetType:targetType.value,
+			taskTargets:state.checkedValue//采集对象
+		});//返回上一页并传值
+		GoBack()
 	}
-	
-	const filterNodes = (nodes) => {
-		const filtered = [];
-		nodes.forEach(node => {
-			const hasMatch = node.name && node.name.includes(keyword);
-			let childrenFiltered = [];
-			
-			if (node.children && node.children.length > 0) {
-				childrenFiltered = filterNodes(node.children);
-			}
-			
-			if (hasMatch || childrenFiltered.length > 0) {
-				filtered.push({
-					...node,
-					children: childrenFiltered
-				});
-			}
+	//返回
+	const GoBack = () => {
+		uni.navigateBack({
+			delta: 1
 		});
-		return filtered;
-	};
-	
-	treeData.value = filterNodes(fullTreeData.value);
-};
-
-const toggleExpand = ({ itemId, siblingIds, isExpanded }) => {
-	if (isExpanded) {
-		expandedIds.value = expandedIds.value.filter(id => id !== itemId);
-	} else {
-		expandedIds.value = expandedIds.value.filter(id => {
-			if (siblingIds.includes(id)) {
-				return false;
-			}
-			return true;
-		});
-		expandedIds.value = [...expandedIds.value, itemId];
 	}
-};
+</script>
 
-// 向下级联:更新所有子节点的选中状态
-const toggleChildren = (children, checked) => {
-	children.forEach(child => {
-		child.checked = checked;
-		child.halfChecked = false;
-		if (child.children && child.children.length > 0) {
-			toggleChildren(child.children, checked);
+<style scoped lang="scss">
+	:deep(.item_width){
+		.scroll_tab_btn{
+			.tab_item{
+				width: 122rpx;
+				flex: 0;
+			}	
 		}
-	});
-};
-
-// 向上级联:更新父节点的选中状态
-const updateParentStatus = () => {
-	const updateNode = (nodes) => {
-		nodes.forEach(node => {
-			if (node.children && node.children.length > 0) {
-				updateNode(node.children);
-
-				const childrenChecked = node.children.filter(c => c.checked);
-				const childrenHalfChecked = node.children.filter(c => c.halfChecked);
-
-				if (childrenChecked.length === node.children.length) {
-					node.checked = true;
-					node.halfChecked = false;
-				} else if (childrenChecked.length > 0 || childrenHalfChecked.length > 0) {
-					node.checked = false;
-					node.halfChecked = true;
-				} else {
-					node.checked = false;
-					node.halfChecked = false;
-				}
-			}
-		});
-	};
-	updateNode(treeData.value);
-};
-
-const toggleSelect = (item) => {
-	item.checked = !item.checked;
-	item.halfChecked = false;
-
-	if (item.children && item.children.length > 0) {
-		// 向下级联
-		toggleChildren(item.children, item.checked);
 	}
-
-	// 向上级联
-	updateParentStatus();
-};
-
-const selectedCount = computed(() => {
-	let count = 0;
-	const collectChecked = (items) => {
-		items.forEach(item => {
-			if (item.children && item.children.length > 0) {
-				collectChecked(item.children);
-			} else if (item.checked && item.userId) {
-				count++;
-			}
-		});
-	};
-	collectChecked(treeData.value);
-	return count;
-});
-
-const goBack = () => {
-	uni.navigateBack();
-};
-
-const collectSelectedUsers = () => {
-	const selectedUsers = [];
-	const currentTab = activeTab.value;
-
-	const collect = (nodes, parentInfo = {}) => {
-		nodes.forEach(node => {
-			if (node.children && node.children.length > 0) {
-				const newParentInfo = { ...parentInfo };
-				if (node.name && !node.userId) {
-					if (currentTab === 'department') {
-						if (!newParentInfo.departmentName) {
-							newParentInfo.departmentName = node.name;
-						} else if (!newParentInfo.groupName) {
-							newParentInfo.groupName = node.name;
-						}
-					} else if (currentTab === 'subject') {
-						if (!newParentInfo.subjectName) {
-							newParentInfo.subjectName = node.name;
-						} else if (!newParentInfo.gradeName) {
-							newParentInfo.gradeName = node.name;
-						}
-					} else if (currentTab === 'grade') {
-						if (!newParentInfo.gradeName) {
-							newParentInfo.gradeName = node.name;
-						} else if (!newParentInfo.classTypeName) {
-							newParentInfo.classTypeName = node.name;
-						} else if (!newParentInfo.className) {
-							newParentInfo.className = node.name;
-						}
-					} else if (currentTab === 'permission') {
-						if (!newParentInfo.roleName) {
-							newParentInfo.roleName = node.name;
-						}
-					} else if (currentTab === 'student') {
-						if (!newParentInfo.gradeName) {
-							newParentInfo.gradeName = node.name;
-						} else if (!newParentInfo.classTypeName) {
-							newParentInfo.classTypeName = node.name;
-						} else if (!newParentInfo.className) {
-							newParentInfo.className = node.name;
-						}
-					}
-				}
-				collect(node.children, newParentInfo);
-			} else if (node.checked && node.userId) {
-				selectedUsers.push({
-					id: node.id,
-					name: node.name,
-					userId: node.userId,
-					thirdCode: node.userAccount || '',
-					departmentName: parentInfo.departmentName || '',
-					groupName: parentInfo.groupName || '',
-					subjectName: parentInfo.subjectName || '',
-					gradeName: parentInfo.gradeName || '',
-					classTypeName: parentInfo.classTypeName || '',
-					className: parentInfo.className || '',
-					roleName: parentInfo.roleName || ''
-				});
-			}
-		});
-	};
-	collect(treeData.value);
-	return selectedUsers;
-};
-
-const handleConfirm = () => {
-	const data = {
-		tab: tabs.find(t => t.key === activeTab.value).label,
-		count: selectedCount.value,
-		users: collectSelectedUsers()
-	};
-	
-	uni.setStorageSync('selectObjectResult', JSON.stringify(data));
-	uni.navigateBack();
-};
-
-const setSelectedUsers = (users) => {
-	if (!users || !Array.isArray(users)) return;
-	
-	const selectedTeacherIds = new Set(users.map(u => u.teacherId));
-	
-	const setNode = (nodes) => {
-		nodes.forEach(node => {
-			if (node.children && node.children.length > 0) {
-				setNode(node.children);
-			} else if (node.teacherId && selectedTeacherIds.has(node.teacherId)) {
-				node.checked = true;
-			}
-		});
-	};
-	
-	setNode(treeData.value);
-	updateParentStatus();
-};
-
-const setSelectedUsersById = (userIds) => {
-	if (!userIds || !Array.isArray(userIds)) return;
-	
-	const selectedIds = new Set(userIds);
-	
-	const setNode = (nodes) => {
-		nodes.forEach(node => {
-			if (node.children && node.children.length > 0) {
-				setNode(node.children);
-			} else if (node.id && selectedIds.has(node.id)) {
-				node.checked = true;
-			}
-		});
-	};
-	
-	setNode(treeData.value);
-	updateParentStatus();
-};
-
-onMounted(async () => {
-	initSafeArea();
-
-	const pages = getCurrentPages();
-	if (pages.length === 1) {
-		uni.navigateBack();
-		return;
-	}
-	
-	const lastSelection = uni.getStorageSync('selectObjectLastSelection');
-	if (lastSelection) {
-		try {
-			const data = JSON.parse(lastSelection);
-			
-			if (data.isEdit) {
-				editMode.value = true;
-			}
-			
-			if (data.noticeType) {
-				noticeType.value = data.noticeType;
-				const typeMap = {
-					1: 'department',
-					2: 'subject',
-					3: 'grade',
-					4: 'permission',
-					5: 'student'
-				};
-				const tabKey = typeMap[data.noticeType];
-				if (tabKey) {
-					activeTab.value = tabKey;
-				}
-			} else if (data.tab) {
-				const tab = tabs.find(t => t.label === data.tab);
-				if (tab) {
-					activeTab.value = tab.key;
-				}
-			}
-			
-			if (data.users) {
-				selectedUserIds.value = data.users.map(u => u.userId || u.id);
+	:deep(.scroll_tab_btn){
+		margin-top: 24rpx;
+		.tab_item{
+			&.selected{
+				background-color: #2E64FA;
+				color: #FFFFFF;
 			}
-		} catch (e) {
-			console.error('解析上次选择失败:', e);
 		}
 	}
-	
-	await getSchoolYear();
-	
-	if (activeTab.value === 'department') {
-		await loadDepartmentData();
-	} else if (activeTab.value === 'subject' || activeTab.value === 'grade' || activeTab.value === 'permission') {
-		await loadPersonalData(activeTab.value);
-	} else if (activeTab.value === 'student') {
-		await loadStudentData();
-	}
-	
-	if (selectedUserIds.value.length > 0) {
-		setSelectedUsersById(selectedUserIds.value);
-	} else if (lastSelection) {
-		try {
-			const data = JSON.parse(lastSelection);
-			if (data.users) {
-				setSelectedUsers(data.users);
+	.page_body{
+		display: flex;
+		flex-direction: column;
+		height: 100%;
+		min-width: auto;
+		.row_search_box{
+			height: 96rpx;
+			align-items: flex-start;
+		}
+		.tree_node{
+			display: flex;
+			width: 100%;
+			padding: 0 24rpx;
+			box-sizing: border-box;
+			flex:1;
+			overflow: auto;
+			:deep(.utv-tree-item__arrow-icon){
+				display: none;
 			}
-		} catch (e) {
-			console.error('设置选中状态失败:', e);
 		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.page_select_object {
-	display: flex;
-	flex-direction: column;
-	min-height: 100vh;
-}
-
-.tabs_container {
-	position: fixed;
-	left: 0;
-	right: 0;
-	z-index: 99;
-	display: flex;
-	padding: 24rpx ;
-	background-color: #FFFFFF;
-	justify-content: space-between;
-
-	.tab_item {
-		flex-shrink: 0;
-		width: 122rpx;
-		height: 72rpx;
-		background: #FFFFFF;
-		border-radius: 8rpx;
-		border: 2rpx solid #DCDFE6;
-		display: flex;
-		align-items: center;
-		justify-content: center;
-
-		&.active {
-			background: #2E64FA;
-			border-color: #2E64FA;
-
-			.tab_text {
+		.foot_btn{
+			width: 100%;
+			padding: 40rpx 24rpx;
+			flex-shrink: 0;
+			box-sizing: border-box;
+			.btn_submit{
+				background-color: #2E64FA;
+				border-radius: 8rpx;
+				width: 100%;
+				height: 90rpx;
+				font-weight: 500;
+				font-size: 28rpx;
 				color: #FFFFFF;
+				display: flex;
+				align-items: center;
+				justify-content: center;
 			}
 		}
-
-		.tab_text {
-			font-weight: 400;
-			font-size: 28rpx;
-			color: #333333;
-		}
-
-		&.disabled {
-			pointer-events: none;
-			opacity: 0.5;
-		}
-	}
-}
-
-.content_list {
-	flex: 1;
-	padding-bottom: 150rpx;
-}
-
-.select_footer {
-	position: fixed;
-	bottom: 0;
-	left: 0;
-	right: 0;
-	padding: 24rpx;
-	background-color: #FFFFFF;
-	border-top: 2rpx solid #F3F3F3;
-
-	.confirm_btn {
-		width: 100%;
-		height: 90rpx;
-		background: #2E64FA;
-		border-radius: 8rpx;
-		text-align: center;
-		line-height: 90rpx;
-
-		.btn_text {
-			font-size: 32rpx;
-			font-weight: 500;
-			color: #FFFFFF;
-		}
 	}
-}
 </style>

+ 98 - 2
pages/materialCollection/taskOverview/taskDetail.vue

@@ -98,7 +98,7 @@
 									<text class="name">{{item.targetName}}</text>
 									<text class="file_num" v-if="state?.[`${state.tabsSelected}Header`]?.includes('fileNum')">文件数量:{{item?.fileNum || 0}}</text>
 								</view>
-								<view class="btn">查看</view>
+								<view class="btn" @click="OpenPreviewFileDialog(item.taskTargetFileVos)">查看</view>
 							</view>
 							<view class="item_info">
 								<template v-for="prop in state?.[`${state.tabsSelected}Header`]">
@@ -118,16 +118,34 @@
 				<uni-load-more v-if="state.tabListData.length > 0" :status="state.loadStatus" />
 			</scroll-view>	
 		</view>
+		<customPopupDialog class="file_preview" ref="popupFileDialogRef" title="查看文件" dialogWidth="670" :isMaskClick="false" :showDialogFooter="false">
+			<view class="dialog_content_file">
+				<view class="file_list_item" v-for="item in state.fileDialog.fileList" :Key="item.id">
+					<image class="file_icon" src="@/static/image/icon/file.png"></image>
+					<view class="file_info">
+						<view class="file_name">{{item.fileName}}</view>
+						<view class="file_size">
+							<text>{{getFileSize(item.fileSize)}}</text>
+							<text>{{ item.createTime }}</text>
+						</view>
+					</view>
+					<view class="file_btn">
+						<text class="preview">预览</text>
+					</view>
+				</view>
+			</view>
+		</customPopupDialog>
 	</view>
 </template>
 
 <script setup>
 	import uniEasyinput from '@/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue';
 	import uniDataSelect from '@/uni_modules/uni-data-select/components/uni-data-select/uni-data-select.vue';
+	import customPopupDialog from '@/components/customPopupDialog.vue';
 	import uniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue';
 	import uvSticky from '@/uni_modules/uv-sticky/components/uv-sticky/uv-sticky.vue'; //吸顶
 	import navBar from '@/components/navBar.vue';
-	import { remainingDays } from '@/common/common.js';
+	import { remainingDays,getFileSize } from '@/common/common.js';
 	import {
 		GetCollectionTask,
 		getTaskTargetList,
@@ -202,6 +220,9 @@
 		tabListData: [], //列表数据
 		searchBoxTop:0,//搜索框距离顶部的高度
 		isSticky:false,//是否吸顶
+		fileDialog:{//文件预览
+			fileList:[]
+		}
 	});
 	const statusClassMap = {
 		0: 'status_pending',
@@ -213,6 +234,7 @@
 		1: '进行中',
 		2: '已结束'
 	};
+	const popupFileDialogRef = ref(null)
 	onLoad((option) => {
 		state.taskId = option.taskId;
 		state.taskStatus = option.taskStatus;
@@ -441,6 +463,12 @@
 		}
 		state.oldScrollTop = scrollTop;
 	}
+	//打开预览弹框
+	const OpenPreviewFileDialog = (files) => {
+		console.log(files)
+		state.fileDialog.fileList = files || [];
+		popupFileDialogRef.value.OpenDialog();
+	}
 	//返回到顶部
 	const GoTop = () => {
 		// 解决view层不同步的问题
@@ -757,4 +785,72 @@
 	.no_data{
 		margin-top: 24rpx;
 	}
+	.file_preview{
+		:deep(.dialog_content){
+			padding: 24rpx 0 32rpx;
+		}
+		.dialog_content_file{
+			display: flex;
+			width: 100%;
+			max-height: 440rpx;
+			padding: 0 24rpx;
+			box-sizing: border-box;
+			overflow-y: auto;
+			flex-direction: column;
+			.file_list_item {
+				display: flex;
+				align-items: center;
+				width: 100%;
+				padding: 20rpx 24rpx;
+				background: #FFFFFF;
+				border-radius: 20rpx;
+				border: 2rpx solid #EBEEF5;
+				box-sizing: border-box;
+				margin-top: 40rpx;
+				&:nth-child(1){
+					margin-top: 0;
+				}
+				gap:20rpx;
+				.file_icon{
+					flex-shrink: 0;
+					width: 80rpx;
+					height: 80rpx;
+				}
+				.file_info{
+					flex:1;
+					display: flex;
+					flex-direction: column;
+					overflow: hidden;
+					.file_name{
+						width: 100%;
+						font-weight: 500;
+						font-size: 28rpx;
+						color: #333333;
+						overflow: hidden;
+						white-space: nowrap;
+						text-overflow: ellipsis;
+					}
+					.file_size{
+						display: flex;
+						width: 100%;
+						gap: 20rpx;
+						font-weight: 400;
+						font-size: 24rpx;
+						color: #999999;
+						margin-top: 8rpx;
+					}
+				}
+				.file_btn{
+					flex-shrink: 0;
+					display: flex;
+					gap:24rpx;
+					font-weight: 400;
+					font-size: 28rpx;
+					.preview{
+						color: #2E64FA;
+					}
+				}
+			}
+		}
+	}
 </style>

+ 7 - 0
pages/teacherHonor/honorOverview/honorTypeAuditDetail.vue

@@ -217,6 +217,13 @@
 	.page_body{
 		height: 100%;
 		min-height: auto;
+		.row_search_box{
+			height: 96rpx;
+			align-items: flex-start;
+		}
+		:deep(.scroll_tabs){
+			margin-top: 32rpx;
+		}
 		.page_content{
 			height: calc(100% - 312rpx);
 			&.no_review{

+ 1 - 0
pages/teacherHonor/honorOverview/index.vue

@@ -170,6 +170,7 @@
 					title: '删除成功!',
 					icon: 'none'
 				});
+				popupDialogRef.value.CloseDialog();
 				OnLoadData();
 			}else{
 				uni.showToast({

+ 29 - 3
pages/teacherHonor/myHonor/uploadEditMyHonor.vue

@@ -50,7 +50,7 @@
 							<uni-icons class="upload_file_btn_icon" type="plusempty"></uni-icons>
 							<view class="upload_file_btn_text">上传佐证材料</view>
 						</view>
-						<view class="upload_file_list_item" v-for="item in state.formData.docDatas" :key="item.createdAt" @click="PreviewFile(item)">
+						<view class="upload_file_list_item" v-for="(item,i) in state.formData.docDatas" :key="item.createdAt" @click="PreviewFile(item)">
 							<image class="list_item_icon" src="/static/image/icon/file.png"></image>
 							<view class="list_item_content">
 								<view class="item_content_title">{{item.documentName}}</view>
@@ -390,7 +390,7 @@
 					name: 'file',
 					header: {Authorization: token.value},
 					method: 'POST',
-					withCredentials: true, // iOS 本地测远程且要带 cookie
+					// withCredentials: true, // iOS 本地测远程且要带 cookie
 					success: (res) => {
 						const result = JSON.parse(res.result);
 						state.formData.docDatas.push({
@@ -399,8 +399,34 @@
 							fileSize:fileSizeConvert(files?.[0]?.size || 0),
 							createdAt:unioformDateTransform(new Date())
 						})
-					}
+					},
+					fail: (e) => console.log(e,`${apiBaseUrl.value}/api/v1/teach/oss/file/oss/upload_filesSele`,9999)
 				});
+				// const fileItem = files[0]
+				// // 不使用 fileUploader.value.upload,改用uni原生上传API
+				// uni.uploadFile({
+				// 	url: `${apiBaseUrl.value}/api/v1/teach/oss/file/oss/upload_filesSele`,
+				// 	filePath: fileItem.path,
+				// 	name: 'file', //后端接收文件字段名
+				// 	header: {
+				// 		Authorization: token.value
+				// 	},
+				// 	timeout: 120000, //上传超时时间,大文件2分钟
+				// 	success: (res) => {
+				// 		console.log('上传成功',res, res.statusCode, res.data)
+				// 		const result = JSON.parse(res.data);
+				// 		state.formData.docDatas.push({
+				// 			documentName: result.data.fileName,
+				// 			documentUrl: result.data.url,
+				// 			fileSize: fileSizeConvert(fileItem?.size || 0),
+				// 			createdAt: unioformDateTransform(new Date())
+				// 		})
+				// 	},
+				// 	fail: (err) => {
+				// 		//这里打印iOS原生真实错误,不再是组件包装后的“网络错误”
+				// 		console.error('uni.uploadFile原生失败', err)
+				// 	}
+				// })
 			}
 		})
 	}

+ 2 - 3
pages/teacherStudy/learningMonitor/index.vue

@@ -1,4 +1,4 @@
-<template>
+<template>
 	<!-- 头部 -->
 	<nav-bar :height="state.courseType == '0'?'312':'224'" backPath="workspace" title="教师研修" :filterPickerData="state.filterPickerData" :tabList="state.tabList" :onlyTitle="false" :showFilterPicker="true" :showTabs="true" rightWidth="0rpx" @CommonFilterData="CommonFilterData"></nav-bar>
 	<view :class="['page_content',{'self_content_height':state.courseType == '1'}]">
@@ -25,8 +25,7 @@
 					</view>
 					<view class="item_status">
 						<view v-if="state.courseType == '0'" :class="['status',{going:item.courseStatus === 0,finished:item.courseStatus === 2}]">{{item.courseStatus === 0?'进行中':'已结束'}}</view>
-						<view class="item_field" v-if="state.courseType == '0' && item.courseType">{{item.courseType}}</view>
-						<view class="item_field" v-if="state.courseType == '1' && item.teachCourseTypeName">{{item.teachCourseTypeName}}</view>
+						<view class="item_field" v-if="item.teachCourseTypeName">{{item.teachCourseTypeName}}</view>
 						<view class="item_field" v-if="item.teachCourseTypeClassName">{{item.teachCourseTypeClassName}}</view>
 						<view class="item_field" v-if="item.teacherName">{{item.teacherName}}</view>
 						<view class="item_field" v-if="state.courseType == '0' && item.remainingTime && item.remainingTime!='-'">剩余{{item.remainingTime}}</view>

+ 8 - 0
reqApi/notification.js

@@ -87,4 +87,12 @@ export const updateReviewStatus = (data) => {
 // 已审核的通过和驳回
 export const ChangeReviewStatus = (data) => {
   return request.post('/api/v1/teaching_plan/collect/task_target_review/change_review_status',data)
+}
+// 查询本学校下的当前学年的所有部门及部门下的人
+export const findDepartContainUser = (schoolYearId) => {
+  return request.get(`/api/v1/teaching_plan/collect/target_loading/find_depart_contain_user?schoolYearId=${schoolYearId}`)
+}
+//查询本学校下的当前学年的所有学科、年级、权限数据及下的人
+export const findCollectPersonalInfo = (schoolYearId) => {
+  return request.get(`/api/v1/teaching_plan/collect/target_loading/find_collect_personal_info?schoolYearId=${schoolYearId}`)
 }

+ 10 - 0
style/common.scss

@@ -446,6 +446,10 @@ uni-page-body {
 				font-size: 28rpx;
 				color: #333333;
 				height: 72rpx;
+				.uni-easyinput__placeholder-class {
+					font-size: 28rpx;
+					color: #C0C4CC;
+				}
 			}
 		}
 		.upload_button{
@@ -948,4 +952,10 @@ uni-page-body {
 			}
 		}
 	}
+}
+.uni-calendar-item--multiple .uni-calendar-item--before-checked, .uni-calendar-item--multiple .uni-calendar-item--after-checked {
+	background-color:#2E64FA !important;
+}
+.uni-datetime-picker--btn{
+	background-color:#2E64FA !important;
 }

+ 156 - 0
uni_modules/KieranYin9527-tree/changelog.md

@@ -0,0 +1,156 @@
+## 0.6.2(2026-08-17)
+澄清 `uni_modules` 插件重新安装、更新及安装方式切换的影响范围。
+## 0.6.1(2026-08-16)
+更新插件示例工程
+# Changelog
+
+本项目的显著变更会记录在此文件中,版本号遵循 Semantic Versioning。
+
+## Unreleased
+
+## 0.6.0(2026-08-16)
+### Added
+
+- **dcloud:** 将插件市场发布迁移为 uni_modules。
+- **core:** 完善过滤与选中行为。
+
+### Fixed
+
+- **package:** 收紧公开子路径导出。
+- **build:** 强化文档和 README 构建校验。
+- **docs:** 修复文档开发进程退出码与端口解析。
+## 0.5.1 - 2026-08-14
+
+### Fixed
+
+- 修复 npm 安装使用时具名插槽在 IDE 中无法识别的问题,并保留 `$props` / JSX 事件回调的 payload 类型。
+
+## 0.5.0 - 2026-08-13
+
+### Fixed
+
+- **docs:** 修正文档演示入口。
+
+## 0.4.2 - 2026-08-12
+
+### Fixed
+
+- 修复微信、支付宝小程序中节点选中或反选时,选中态样式触发页面抖动的问题。
+- 修复小程序中点击复选框时事件继续冒泡,导致选中或反选偶发无效的问题;开启 `check-on-click-node` 后,点击节点标签也可稳定切换选中状态。
+
+## 0.4.1 - 2026-08-11
+
+### Fixed
+
+- 兼容带 emoji 前缀的提交信息。
+
+## 0.4.0 - 2026-08-11
+
+### Added
+
+- 为 `check-change` 事件 payload 增加 `halfCheckedKeys` / `halfCheckedNodes` 字段,便于直接读取父子联动模式下的半选状态。
+- 为 `filter-change` 事件 payload 增加 `matchedKeys` / `matchedNodes` 字段,区分直接命中节点与包含祖先后代的最终可见节点。
+- 新增 `check-on-click-leaf` 属性,支持仅点击叶子节点行时切换选中状态,默认 `false`。
+- 新增 `accordion` 属性,支持手风琴模式(展开节点时自动收起同级已展开节点),默认 `false`。
+- 新增 `empty-filter` 插槽,用于筛选无结果时的专用空状态,未提供时自动回退到 `empty` 插槽。
+
+### Changed
+
+- 优化默认、图标、标签和尾部插槽的回退渲染,仅在提供对应插槽时进入插槽分支,避免小程序生成无内容的插槽节点。
+- 基础示例与 playground 不再默认开启 `expand-on-click-node`,默认演示与组件一致的箭头展开交互,同时保留实时切换入口。
+
+### Fixed
+
+- 修复 `default-expanded-keys` 指定后代节点时未自动展开所有祖先的问题。
+- 新增 `default-expand-parent` 属性控制是否自动展开祖先,默认 `true`(保持修复后的行为)。
+- 修复小程序中箭头或选择控件点击继续冒泡到节点行,导致展开、选中和 `node-click` 行为相互冲突的问题。
+
+### Tests
+
+- 补充箭头与选择控件快速连续点击的组件回归测试,确保嵌套控件行为不会误触发节点行交互。
+
+## 0.3.2 - 2026-08-04
+
+### Fixed
+
+- 修复 GitHub Pages 示例预览 404。
+
+## 0.3.1 - 2026-08-01
+
+### Fixed
+
+- 修复在线演示未使用 hash 路由导致页面直达失败的问题,并补充 Netlify 部署回退配置。
+
+## 0.3.0 - 2026-08-01
+
+### Changed
+
+- 新增 `packDisabledKey` / `pack-disabled-key` 规范命名,并暂时保留旧 `packDisabledkey` / `pack-disabledkey` 作为废弃别名。
+- 优化选中事件 payload 构建与受控 `v-model` 等价回流,避免重复遍历和不必要的全量选中态重放。
+- 为组件公开实例方法增加 `UniTreeViewExposed` 编译期一致性校验,并补充懒加载公开方法的行为说明。
+- 明确 `uni-tree-view/shared` 继续提供共享运行时工具,并清理 resolver 中无关的模板注释。
+
+### Fixed
+
+- 修复全选、清空、父子联动和受控值回放可能绕过 `checked-disabled` 改变禁用节点状态的问题。
+- 修复 `mitt` 通配监听器未接收事件类型的问题。
+
+### Tests
+
+- 补充整行点击组合行为、空状态插槽、关键词高亮、普通模式滚动、属性兼容迁移、受控行为切换和共享工具契约测试。
+
+## 0.2.0 - 2026-07-31
+
+### Changed
+
+- 规范组件内部 SCSS 元素类与状态类命名,保留 `nodeClass` 公开样式入口不变。
+
+### Tests
+
+- 按结构与展开、选择、懒加载拆分树状态测试,并补齐组件公开事件 payload 的集成链路验证。
+
+## 0.1.0 - 2026-07-30
+
+### Fixed
+
+- npm 发布迁移到 GitHub Actions Trusted Publishing,并支持手动重试已有发布标签。
+- 规范 npm 包仓库元数据,并升级 Release workflow 的 Node 环境 Action 至 v7,消除 npm 11 发布警告。
+
+## 0.0.9 - 2026-07-29
+
+### Fixed
+
+- 修复 GitHub Actions 发布标签校验脚本被 shell 提前解析导致发布中断的问题。
+- 发布前预检变更日志,并在 bumpp 执行失败且尚未创建提交时自动恢复被修改的跟踪文件。
+
+## 0.0.8 - 2026-07-29
+
+### Added
+
+- 节点整行选择与整行展开开关。
+- 增加 `node-class` 一级属性,作为每个节点行的外部样式入口。
+- 自定义过滤方法、关键词高亮和空状态插槽。
+- scrollToKey、懒加载错误事件和重试方法。
+- 大数据虚拟渲染演示和多平台构建说明。
+
+### Changed
+
+- **Breaking:** 选择入口由 `showCheckbox` 更名为 `selectable`,选择控件位置由 `checkboxPlacement` 更名为 `selectionPlacement`,旧属性不再保留。
+- **Breaking:** 选中与展开事件统一为 `check-change` 和 `expand-change`,选中事件类型更名为 `TreeCheckChangePayload`。
+- `treeProps` 只负责 `id`、`label`、`children`、`disabled`、`leaf`、`append`、`icon` 数据字段映射;节点行样式统一通过一级 `nodeClass` 属性传入。
+- 优化移动端按压反馈、选择区域、选中态和加载状态。
+- 优化文档站示例页实时预览布局:宽屏挂载到右侧栏,窄屏以可折叠卡片展示。
+- 文档预览 playground 改用项目 Logo,并移除 H5 路由固定 base 以适配文档内嵌预览。
+- Playground 迁移到 Wot UI v2;核心组件仍保持零 Wot UI 依赖。
+
+### Removed
+
+- 移除 `field`、`labelField`、`valueField`、`childrenField`、`disabledField`、`leafField`、`appendField`、`iconField` 等旧字段映射属性,统一使用 `treeProps`。
+- 移除 `defaultExpandedIds` 展开别名,统一使用 `defaultExpandedKeys`。
+- 移除 `change`、`checked`、`updated`、`expand`、`goChild` 等历史事件别名。
+- 移除 `treeProps.class` 样式映射,改用 `nodeClass`。
+
+### Fixed
+
+- 禁用且已选节点继续使用禁用样式。
+- 补齐懒加载旋转动画和失败后的重试状态。

+ 1 - 0
uni_modules/KieranYin9527-tree/components/index.js

@@ -0,0 +1 @@
+export * from "./uni-tree-view/types.js";

+ 17 - 0
uni_modules/KieranYin9527-tree/components/uni-tree-view/constants/index.ts

@@ -0,0 +1,17 @@
+import type { TreeProps } from "../types";
+
+export const CHECK_STATUS_MAP = {
+  checked: "checked", // 选中
+  unchecked: "unchecked", // 未选中
+  indeterminate: "indeterminate" // 半选
+} as const;
+
+export const DefaultTreeProps: TreeProps = {
+  id: "id",
+  label: "label",
+  children: "children",
+  disabled: "disabled",
+  leaf: "leaf",
+  append: "append",
+  icon: "icon"
+};

+ 1 - 0
uni_modules/KieranYin9527-tree/components/uni-tree-view/types.js

@@ -0,0 +1 @@
+export {};

+ 232 - 0
uni_modules/KieranYin9527-tree/components/uni-tree-view/types.ts

@@ -0,0 +1,232 @@
+import type { CHECK_STATUS_MAP } from "./constants/index";
+
+export type TreeKey = string | number;
+
+export type CheckStatus = typeof CHECK_STATUS_MAP[keyof typeof CHECK_STATUS_MAP];
+
+export type TreeModelValue = TreeKey | TreeKey[] | null;
+
+export interface TreeDataItem {
+  [key: string]: any;
+}
+
+export interface TreeNode {
+  id: TreeKey;
+  label: string;
+  append: string;
+  icon: string;
+  path: string[];
+  source: TreeDataItem;
+  parentId?: TreeKey;
+  parentIds: TreeKey[];
+  parents: TreeDataItem[];
+  level: number;
+  disabled: boolean;
+  checked: CheckStatus;
+  expanded: boolean;
+  visible: boolean;
+  isLeaf: boolean;
+  loaded: boolean;
+  loading: boolean;
+  loadError: unknown;
+}
+
+export interface TreeProps {
+  id: string;
+  label: string;
+  children: string;
+  disabled?: string;
+  leaf?: string;
+  append?: string;
+  icon?: string;
+}
+
+export interface TreeCheckChangePayload {
+  value: TreeModelValue;
+  keys: TreeKey[];
+  nodes: TreeNode[];
+  /** Current half-checked keys in linked multiple-selection mode. */
+  halfCheckedKeys: TreeKey[];
+  /** Current half-checked nodes in linked multiple-selection mode. */
+  halfCheckedNodes: TreeNode[];
+  node: TreeNode;
+}
+
+export interface TreeLoadPayload {
+  node: TreeNode;
+  children: TreeDataItem[];
+}
+
+export interface TreeLoadErrorPayload {
+  node: TreeNode;
+  error: unknown;
+}
+
+export interface TreeScrollToOptions {
+  /** Expand ancestors before locating the node. */
+  expandParents?: boolean;
+}
+
+export interface TreeExpandPayload {
+  expanded: boolean;
+  node: TreeNode;
+}
+
+export interface TreeNodeClickPayload {
+  id: TreeKey;
+  node: TreeNode;
+  path: TreeNode[];
+}
+
+export interface TreeFilterPayload {
+  value: string;
+  /** Visible keys after filtering, including direct matches, ancestors and descendants. */
+  keys: TreeKey[];
+  /** Visible nodes after filtering, including direct matches, ancestors and descendants. */
+  nodes: TreeNode[];
+  /** Keys directly matched by the built-in or custom filter rule. */
+  matchedKeys: TreeKey[];
+  /** Nodes directly matched by the built-in or custom filter rule. */
+  matchedNodes: TreeNode[];
+}
+
+export interface TreeSlotProps {
+  node: TreeNode;
+  data: TreeDataItem;
+  path: TreeNode[];
+}
+
+export interface TreeEmptySlotProps {
+  filterValue: string;
+}
+
+export interface UniTreeViewSlots {
+  default?: (props: TreeSlotProps) => unknown;
+  label?: (props: TreeSlotProps) => unknown;
+  icon?: (props: TreeSlotProps) => unknown;
+  append?: (props: TreeSlotProps) => unknown;
+  empty?: (props: TreeEmptySlotProps) => unknown;
+  "empty-filter"?: (props: TreeEmptySlotProps) => unknown;
+}
+
+export interface UniTreeViewProps {
+  /** Current selected value. Single select uses one key, multiple select uses an array. */
+  modelValue?: TreeModelValue;
+  /** Tree data. */
+  data?: TreeDataItem[];
+  /** Filter keyword. Matching nodes and their related branch stay visible. */
+  filterValue?: string;
+  /** Custom node matcher used when filterValue is not empty. */
+  filterMethod?: (value: string, node: TreeNode) => boolean;
+  /** Highlight literal filter keyword matches in the built-in label. */
+  highlightFilter?: boolean;
+  /** Default checked keys for uncontrolled initial state. */
+  defaultCheckedKeys?: TreeKey | TreeKey[] | null;
+  /** Field mapping for id, label, children, disabled, leaf, append and icon. */
+  treeProps?: Partial<TreeProps>;
+  /** Theme color for active checkbox/radio. */
+  themeColor?: string;
+  /** Whether to enable and show the selection control. */
+  selectable?: boolean;
+  /** Whether to show radio UI in single-select mode. */
+  showRadioIcon?: boolean;
+  /** Whether to support multiple selection. */
+  multiple?: boolean;
+  /** Whether clicking a node row changes its selection state. */
+  checkOnClickNode?: boolean;
+  /** Whether clicking a leaf node row changes its selection state. */
+  checkOnClickLeaf?: boolean;
+  /** Whether clicking a node row expands or collapses it. */
+  expandOnClickNode?: boolean;
+  /** Whether expanding a node collapses its expanded siblings. */
+  accordion?: boolean;
+  /** Whether parent and child checked states are independent. */
+  checkStrictly?: boolean;
+  /** Single-select mode can only select leaf nodes. Ignored when `multiple` is true. */
+  onlyRadioLeaf?: boolean;
+  /** Whether all nodes are expanded initially. */
+  defaultExpandAll?: boolean;
+  /** Default expanded node keys. */
+  defaultExpandedKeys?: TreeKey[];
+  /** Whether default expanded keys also expand all their ancestors. */
+  defaultExpandParent?: boolean;
+  /** Expand ancestors of checked nodes initially. */
+  expandChecked?: boolean;
+  /** Preserve runtime expanded state when tree data is rebuilt. */
+  cacheExpandedKeys?: boolean;
+  /** Lazy load mode. Nodes can be expanded before children exist. */
+  loadMode?: boolean;
+  /** Lazy load function. */
+  loadApi?: (node: TreeNode) => TreeDataItem[] | Promise<TreeDataItem[]>;
+  /** Custom leaf resolver. */
+  isLeafFn?: (item: TreeDataItem, node: TreeNode) => boolean;
+  /** Load once on first expand even when static children exist. */
+  alwaysFirstLoad?: boolean;
+  /** Whether disabled nodes can participate in selection state changes. */
+  checkedDisabled?: boolean;
+  /** Whether checked disabled nodes are included in returned keys/nodes. */
+  packDisabledKey?: boolean;
+  /** @deprecated Use `packDisabledKey` instead. */
+  packDisabledkey?: boolean;
+  /** Custom class name added to every node row. */
+  nodeClass?: string;
+  /** Tree item indent in rpx. */
+  indent?: number;
+  /** Selection control placement. */
+  selectionPlacement?: "left" | "right";
+  /** Empty text shown when data is empty. */
+  emptyText?: string;
+  /** Show label path under the node label. */
+  showPath?: boolean;
+  /** Separator used by the built-in path display. */
+  pathSeparator?: string;
+  /** Enable fixed-height virtual rendering for very large visible node lists. */
+  virtual?: boolean;
+  /** Row height in px when virtual rendering is enabled. */
+  virtualItemHeight?: number;
+  /** Scroll container height in px when virtual rendering is enabled. */
+  virtualHeight?: number;
+  /** Extra rows rendered before and after the viewport in virtual mode. */
+  virtualOverscan?: number;
+}
+
+export interface UniTreeViewExposed {
+  setCheckedKeys: (keys: TreeKey | TreeKey[], checked?: boolean) => TreeModelValue;
+  getCheckedKeys: () => TreeKey[];
+  getHalfCheckedKeys: () => TreeKey[];
+  getUncheckedKeys: () => TreeKey[];
+  getCheckedNodes: () => TreeNode[];
+  getHalfCheckedNodes: () => TreeNode[];
+  getUncheckedNodes: () => TreeNode[];
+  setExpandedKeys: (keys: TreeKey[] | "all", expanded?: boolean) => void;
+  getExpandedKeys: () => TreeKey[];
+  getUnexpandedKeys: () => TreeKey[];
+  getVisibleKeys: () => TreeKey[];
+  getMatchedKeys: () => TreeKey[];
+  getExpandedNodes: () => TreeNode[];
+  getUnexpandedNodes: () => TreeNode[];
+  getVisibleNodes: () => TreeNode[];
+  getMatchedNodes: () => TreeNode[];
+  getNode: (key: TreeKey) => TreeNode | undefined;
+  getNodePath: (keyOrNode: TreeKey | TreeNode) => TreeNode[];
+  expandAll: () => void;
+  collapseAll: () => void;
+  loadNode: (node: TreeNode) => Promise<TreeDataItem[]>;
+  retryLoad: (keyOrNode: TreeKey | TreeNode) => Promise<TreeDataItem[]>;
+  scrollToKey: (key: TreeKey, options?: TreeScrollToOptions) => Promise<boolean>;
+}
+
+/**
+ * 事件表使用 Vue 3.3+ 的「具名元组」形态,且必须是 type 别名而非 interface:
+ * `defineEmits<T>()` 的约束是 `Record<string, any[]>`,interface 没有隐式索引签名,无法满足。
+ */
+// eslint-disable-next-line ts/consistent-type-definitions -- interface 无隐式索引签名,不满足 defineEmits 约束
+export type UniTreeViewEmits = {
+  "update:modelValue": [value: TreeModelValue];
+  "check-change": [payload: TreeCheckChangePayload];
+  "expand-change": [payload: TreeExpandPayload];
+  "load": [payload: TreeLoadPayload];
+  "load-error": [payload: TreeLoadErrorPayload];
+  "node-click": [payload: TreeNodeClickPayload];
+  "filter-change": [payload: TreeFilterPayload];
+};

File diff suppressed because it is too large
+ 555 - 0
uni_modules/KieranYin9527-tree/components/uni-tree-view/uni-tree-view.vue


+ 47 - 0
uni_modules/KieranYin9527-tree/components/uni-tree-view/uni-tree-view.vue.d.ts

@@ -0,0 +1,47 @@
+/* eslint-disable ts/no-empty-object-type */
+import type { DefineComponent, PublicProps, SlotsType } from "vue";
+import type { AllowedComponentProps } from "../../types";
+import type {
+  UniTreeViewEmits,
+  UniTreeViewExposed,
+  UniTreeViewProps,
+  UniTreeViewSlots
+} from "./types";
+
+export type * from "./types";
+
+/**
+ * `DefineComponent` 的 E 参数要求 `EmitsOptions`(函数值)形态,
+ * 这里从 `UniTreeViewEmits` 的元组形态映射而来。
+ */
+type UniTreeViewEmitsOptions = {
+  [K in keyof UniTreeViewEmits]: (...args: UniTreeViewEmits[K]) => any;
+};
+
+/**
+ * Vue 的 `ResolveProps` 未导出,按同等语义复刻:原始 props 只读化并透传事件属性,
+ * 事件参数直接取自 `UniTreeViewEmits` 的具名元组以保留 payload 类型。
+ */
+type UniTreeViewEmitsToProps = {
+  [K in keyof UniTreeViewEmits as `on${Capitalize<string & K>}`]?: (...args: UniTreeViewEmits[K]) => any;
+};
+
+type UniTreeViewComponent = DefineComponent<
+  AllowedComponentProps & UniTreeViewProps,
+  UniTreeViewExposed,
+  {},
+  {},
+  {},
+  {},
+  {},
+  UniTreeViewEmitsOptions,
+  string,
+  PublicProps,
+  Readonly<AllowedComponentProps & UniTreeViewProps> & UniTreeViewEmitsToProps,
+  {},
+  SlotsType<UniTreeViewSlots>
+>;
+
+declare const _default: UniTreeViewComponent;
+
+export default _default;

+ 1079 - 0
uni_modules/KieranYin9527-tree/components/uni-tree-view/useTreeViewState.ts

@@ -0,0 +1,1079 @@
+import { computed, ref, shallowRef, toRaw, watch } from "vue";
+import { CHECK_STATUS_MAP, DefaultTreeProps } from "./constants";
+import type {
+  CheckStatus,
+  TreeCheckChangePayload,
+  TreeDataItem,
+  TreeKey,
+  TreeModelValue,
+  TreeNode,
+  TreeProps,
+  UniTreeViewProps
+} from "./types";
+
+export type TreeViewStateProps = Pick<
+  UniTreeViewProps,
+  | "data"
+  | "treeProps"
+  | "filterValue"
+  | "filterMethod"
+  | "modelValue"
+  | "defaultCheckedKeys"
+  | "multiple"
+  | "checkStrictly"
+  | "accordion"
+  | "onlyRadioLeaf"
+  | "defaultExpandAll"
+  | "defaultExpandedKeys"
+  | "defaultExpandParent"
+  | "expandChecked"
+  | "cacheExpandedKeys"
+  | "loadMode"
+  | "loadApi"
+  | "isLeafFn"
+  | "alwaysFirstLoad"
+  | "checkedDisabled"
+  | "packDisabledKey"
+  | "packDisabledkey"
+>;
+
+export function useTreeViewState(props: TreeViewStateProps) {
+  const treeList = ref<TreeNode[]>([]);
+  const visibleTreeList = ref<TreeNode[]>([]);
+  const matchedTreeList = ref<TreeNode[]>([]);
+  const treeVersion = ref(0);
+  const reconciledModelValue = shallowRef<{ value: TreeModelValue } | null>(null);
+  const pendingCheckChangePayload = shallowRef<TreeCheckChangePayload | null>(null);
+  const childrenMap = ref<Map<TreeKey, TreeNode[]>>(new Map());
+  const nodeMap = ref<Map<TreeKey, TreeNode>>(new Map());
+  const cachedExpandedKeys = ref<Set<TreeKey>>(new Set());
+  let pendingCheckedKeys = new Set<TreeKey>();
+  let pendingImperativeCheckedKeys = new Set<TreeKey>();
+  let warnedInvalidKeys = new Set<string>();
+  let initialized = false;
+
+  const resolvedTreeProps = computed<TreeProps>(() => {
+    return {
+      id: props.treeProps?.id ?? DefaultTreeProps.id,
+      label: props.treeProps?.label ?? DefaultTreeProps.label,
+      children: props.treeProps?.children ?? DefaultTreeProps.children,
+      disabled: props.treeProps?.disabled ?? DefaultTreeProps.disabled,
+      leaf: props.treeProps?.leaf ?? DefaultTreeProps.leaf,
+      append: props.treeProps?.append ?? DefaultTreeProps.append,
+      icon: props.treeProps?.icon ?? DefaultTreeProps.icon
+    };
+  });
+
+  const isMultiple = computed(() => Boolean(props.multiple));
+  const resolvedPackDisabledKey = computed(() => {
+    return props.packDisabledKey ?? props.packDisabledkey ?? true;
+  });
+
+  watch(
+    () => [
+      props.data,
+      getTreeConfigSignature()
+    ] as const,
+    () => {
+      initializeTree(toRaw(props.data ?? []));
+    },
+    {
+      immediate: true
+    }
+  );
+
+  watch(
+    () => getExpansionConfigSignature(),
+    () => {
+      applyExpandedState();
+    }
+  );
+
+  watch(
+    () => [getCheckedValueSignature(), getCheckedBehaviorSignature()] as const,
+    ([, behaviorSignature], [, previousBehaviorSignature]) => {
+      const checkedKeys = getInitialCheckedKeys();
+      if (behaviorSignature === previousBehaviorSignature && isConfiguredCheckedStateCurrent(checkedKeys)) {
+        return;
+      }
+      applyConfiguredCheckedState(checkedKeys);
+    }
+  );
+
+  watch(
+    () => [props.filterValue, props.filterMethod] as const,
+    () => {
+      updateVisibility();
+    }
+  );
+
+  function initializeTree(treeData: TreeDataItem[] = []) {
+    const wasInitialized = initialized;
+    const preserveRuntimeChecked = initialized && props.modelValue === undefined;
+    const checkedKeys = props.modelValue !== undefined
+      ? getInitialCheckedKeys()
+      : preserveRuntimeChecked
+        ? isMultiple.value
+          ? [...getRawCheckedKeys(), ...pendingCheckedKeys]
+          : [...pendingImperativeCheckedKeys, ...pendingCheckedKeys, ...getRawCheckedKeys()]
+        : getInitialCheckedKeys();
+    const pendingKeys = preserveRuntimeChecked ? [...pendingCheckedKeys] : checkedKeys;
+    const imperativeKeys = [...pendingImperativeCheckedKeys];
+    syncCachedExpandedKeys();
+    childrenMap.value = new Map();
+    nodeMap.value = new Map();
+    warnedInvalidKeys = new Set();
+    treeList.value = flattenTree(treeData);
+    treeVersion.value += 1;
+    initialized = true;
+    applyCheckedState(checkedKeys);
+    pendingCheckedKeys = new Set(pendingKeys.filter((key) => !nodeMap.value.has(key)));
+    const resolvedImperativeKeys = imperativeKeys.filter((key) => nodeMap.value.has(key));
+    pendingImperativeCheckedKeys = new Set(
+      imperativeKeys.filter((key) => pendingCheckedKeys.has(key))
+    );
+    applyExpandedState();
+    publishPendingSelectionChange(resolvedImperativeKeys);
+    reconcileMissingControlledKeys(wasInitialized, checkedKeys);
+  }
+
+  function toggleExpand(node: TreeNode) {
+    if (!isExpandable(node)) {
+      return null;
+    }
+
+    const nextExpanded = !node.expanded;
+    if (nextExpanded && props.accordion) {
+      const siblings = node.parentId === undefined
+        ? treeList.value.filter((item) => item.level === 0)
+        : childrenMap.value.get(node.parentId) ?? [];
+      for (const sibling of siblings) {
+        if (sibling !== node && sibling.expanded) {
+          sibling.expanded = false;
+          syncExpandedCacheForNode(sibling);
+        }
+      }
+    }
+
+    node.expanded = nextExpanded;
+    if (props.cacheExpandedKeys) {
+      if (node.expanded) {
+        cachedExpandedKeys.value.add(node.id);
+      } else {
+        cachedExpandedKeys.value.delete(node.id);
+      }
+    }
+    updateVisibility();
+    return {
+      expanded: node.expanded,
+      node
+    };
+  }
+
+  function checkNode(node: TreeNode) {
+    if (!canSelectNode(node)) {
+      return null;
+    }
+
+    if (isMultiple.value) {
+      const newStatus = node.checked === CHECK_STATUS_MAP.checked
+        ? CHECK_STATUS_MAP.unchecked
+        : CHECK_STATUS_MAP.checked;
+      if (props.checkStrictly) {
+        node.checked = newStatus;
+      } else {
+        updateNodeAndDescendantsStatus(node.id, newStatus);
+        updateParentNodesStatus(node.id);
+      }
+    } else {
+      const newStatus = node.checked === CHECK_STATUS_MAP.checked
+        ? CHECK_STATUS_MAP.unchecked
+        : CHECK_STATUS_MAP.checked;
+      clearCheckedStatus();
+      if (newStatus === CHECK_STATUS_MAP.checked) {
+        node.checked = CHECK_STATUS_MAP.checked;
+      }
+    }
+
+    return buildCheckChangePayload(node);
+  }
+
+  function flattenTree(
+    list: TreeDataItem[] = [],
+    level = 0,
+    parentIds: TreeKey[] = [],
+    parents: TreeDataItem[] = []
+  ) {
+    const nodes: TreeNode[] = [];
+    const {
+      id: idKey,
+      label: labelKey,
+      children: childrenKey,
+      disabled: disabledKey = "disabled",
+      append: appendKey = "append",
+      icon: iconKey = "icon"
+    } = resolvedTreeProps.value;
+    list.forEach((item) => {
+      const id = item[idKey] as TreeKey;
+      const children = item[childrenKey];
+      const label = String(item[labelKey] ?? "");
+      warnAboutInvalidKey(id, label);
+
+      const treeNode: TreeNode = {
+        id,
+        label,
+        append: String(item[appendKey] ?? ""),
+        icon: String(item[iconKey] ?? ""),
+        path: [...parents.map((parent) => String(parent[labelKey] ?? "")), label],
+        source: item,
+        parentId: parentIds[parentIds.length - 1],
+        parentIds,
+        parents,
+        level,
+        expanded: false,
+        visible: level === 0,
+        disabled: Boolean(item[disabledKey]),
+        checked: CHECK_STATUS_MAP.unchecked,
+        isLeaf: false,
+        loaded: false,
+        loading: false,
+        loadError: null
+      };
+      treeNode.isLeaf = resolveIsLeaf(item, treeNode);
+      treeNode.loaded = treeNode.isLeaf || !props.loadMode || (Array.isArray(children) && children.length > 0 && !props.alwaysFirstLoad);
+      nodes.push(treeNode);
+
+      if (id !== undefined && id !== null && nodeMap.value.has(id)) {
+        warnOnce(
+          `duplicate:${String(id)}`,
+          `[uni-tree-view] 检测到重复节点 key:${String(id)}。请确保 tree-props.id 映射的值在整棵树中唯一。`
+        );
+      }
+      nodeMap.value.set(id, treeNode);
+      const parentId = parentIds.slice(-1)[0];
+      if (parentId !== undefined) {
+        if (!childrenMap.value.has(parentId)) {
+          childrenMap.value.set(parentId, []);
+        }
+        childrenMap.value.get(parentId)!.push(treeNode);
+      }
+
+      if (Array.isArray(children) && children.length > 0) {
+        nodes.push(...flattenTree(children, level + 1, [...parentIds, id], [...parents, item]));
+      }
+    });
+
+    return nodes;
+  }
+
+  function applyCheckedState(keys: TreeKey[]) {
+    clearCheckedStatus();
+    if (keys.length === 0) {
+      return;
+    }
+
+    if (isMultiple.value) {
+      if (props.checkStrictly) {
+        for (const key of keys) {
+          const node = nodeMap.value.get(key);
+          if (node && canSelectDisabledNode(node)) {
+            node.checked = CHECK_STATUS_MAP.checked;
+          }
+        }
+        return;
+      }
+
+      updateNodeAndDescendantsStatus(keys, CHECK_STATUS_MAP.checked, Boolean(props.checkedDisabled));
+      updateParentNodesStatus(keys);
+      return;
+    }
+
+    const firstSelectableKey = keys.find((key) => {
+      const node = nodeMap.value.get(key);
+      return node && canSelectDisabledNode(node) && (!props.onlyRadioLeaf || node.isLeaf);
+    });
+    if (firstSelectableKey !== undefined) {
+      const node = nodeMap.value.get(firstSelectableKey);
+      if (node) {
+        node.checked = CHECK_STATUS_MAP.checked;
+      }
+    }
+  }
+
+  function applyConfiguredCheckedState(keys: TreeKey[]) {
+    applyCheckedState(keys);
+    pendingCheckedKeys = new Set(keys.filter((key) => !nodeMap.value.has(key)));
+    pendingImperativeCheckedKeys = new Set();
+  }
+
+  function applyPendingCheckedState() {
+    const resolvedKeys = [...pendingCheckedKeys].filter((key) => nodeMap.value.has(key));
+    if (resolvedKeys.length === 0) {
+      return;
+    }
+
+    const resolvedImperativeKeys = resolvedKeys.filter((key) => pendingImperativeCheckedKeys.has(key));
+    for (const key of resolvedKeys) {
+      pendingCheckedKeys.delete(key);
+      pendingImperativeCheckedKeys.delete(key);
+    }
+
+    if (isMultiple.value) {
+      if (props.checkStrictly) {
+        for (const key of resolvedKeys) {
+          const node = nodeMap.value.get(key);
+          if (node && canSelectDisabledNode(node)) {
+            node.checked = CHECK_STATUS_MAP.checked;
+          }
+        }
+      } else {
+        updateNodeAndDescendantsStatus(resolvedKeys, CHECK_STATUS_MAP.checked, Boolean(props.checkedDisabled));
+        updateParentNodesStatus(resolvedKeys);
+      }
+      publishPendingSelectionChange(resolvedImperativeKeys);
+      return;
+    }
+
+    const node = resolvedKeys
+      .map((key) => nodeMap.value.get(key))
+      .find((item): item is TreeNode => item !== undefined && canSelectNode(item));
+    if (node) {
+      clearCheckedStatus();
+      node.checked = CHECK_STATUS_MAP.checked;
+    }
+    publishPendingSelectionChange(resolvedImperativeKeys);
+  }
+
+  function applyExpandedState() {
+    const defaultExpandedKeys = normalizeKeys(props.defaultExpandedKeys);
+    const expandedKeySet = new Set<TreeKey>(defaultExpandedKeys);
+
+    if (props.defaultExpandParent !== false) {
+      for (const key of defaultExpandedKeys) {
+        const node = nodeMap.value.get(key);
+        if (!node) {
+          continue;
+        }
+
+        for (const parentId of node.parentIds) {
+          expandedKeySet.add(parentId);
+        }
+      }
+    }
+
+    for (const node of treeList.value) {
+      node.expanded = Boolean(props.defaultExpandAll)
+        || expandedKeySet.has(node.id)
+        || (Boolean(props.cacheExpandedKeys) && cachedExpandedKeys.value.has(node.id));
+    }
+
+    applyExpandCheckedState();
+
+    updateVisibility();
+  }
+
+  function applyExpandCheckedState() {
+    if (!props.expandChecked) {
+      return;
+    }
+
+    for (const node of getCheckedNodes()) {
+      for (const parentId of node.parentIds) {
+        const parent = nodeMap.value.get(parentId);
+        if (parent) {
+          parent.expanded = true;
+        }
+      }
+    }
+  }
+
+  function updateNodeAndDescendantsStatus(
+    targetIds: TreeKey | TreeKey[],
+    newStatus: Exclude<CheckStatus, "indeterminate">,
+    includeDisabled = false
+  ) {
+    const pendingIds = [...(Array.isArray(targetIds) ? targetIds : [targetIds])];
+
+    while (pendingIds.length > 0) {
+      const targetId = pendingIds.pop();
+      if (targetId === undefined) {
+        continue;
+      }
+
+      const node = nodeMap.value.get(targetId);
+      if (!node || (node.disabled && !includeDisabled && !props.checkedDisabled)) {
+        continue;
+      }
+
+      node.checked = newStatus;
+      const children = childrenMap.value.get(targetId);
+      if (children && children.length > 0) {
+        for (const child of children) {
+          pendingIds.push(child.id);
+        }
+      }
+    }
+  }
+
+  function hasChildren(nodeId: TreeKey) {
+    const children = childrenMap.value.get(nodeId);
+    return Array.isArray(children) && children.length > 0;
+  }
+
+  function isExpandable(node: TreeNode) {
+    return !node.isLeaf && (hasChildren(node.id) || Boolean(props.loadMode));
+  }
+
+  function updateParentNodesStatus(targetIds?: TreeKey | TreeKey[]) {
+    if (props.checkStrictly || !isMultiple.value) {
+      return;
+    }
+
+    if (targetIds === undefined) {
+      for (let index = treeList.value.length - 1; index >= 0; index -= 1) {
+        updateNodeFromChildren(treeList.value[index]);
+      }
+      return;
+    }
+
+    const affectedNodes = new Map<TreeKey, TreeNode>();
+    const ids = Array.isArray(targetIds) ? targetIds : [targetIds];
+    for (const id of ids) {
+      const node = nodeMap.value.get(id);
+      if (!node) {
+        continue;
+      }
+      if (hasChildren(node.id)) {
+        affectedNodes.set(node.id, node);
+      }
+      for (const parentId of node.parentIds) {
+        const parent = nodeMap.value.get(parentId);
+        if (parent) {
+          affectedNodes.set(parent.id, parent);
+        }
+      }
+    }
+
+    [...affectedNodes.values()]
+      .sort((a, b) => b.level - a.level)
+      .forEach(updateNodeFromChildren);
+  }
+
+  function updateNodeFromChildren(node: TreeNode) {
+    const children = childrenMap.value.get(node.id);
+    if (!children?.length) {
+      return;
+    }
+
+    const allChecked = children.every((child) => child.checked === CHECK_STATUS_MAP.checked);
+    const allUnchecked = children.every((child) => child.checked === CHECK_STATUS_MAP.unchecked);
+
+    if (allChecked) {
+      node.checked = CHECK_STATUS_MAP.checked;
+    } else if (allUnchecked) {
+      node.checked = CHECK_STATUS_MAP.unchecked;
+    } else {
+      node.checked = CHECK_STATUS_MAP.indeterminate;
+    }
+  }
+
+  function updateVisibility() {
+    const rawFilterValue = String(props.filterValue ?? "").trim();
+    const normalizedFilterValue = rawFilterValue.toLowerCase();
+    const visibleNodes: TreeNode[] = [];
+
+    if (rawFilterValue) {
+      const visibleKeySet = new Set<TreeKey>();
+      const matchedNodes: TreeNode[] = [];
+      for (const node of treeList.value) {
+        const matched = props.filterMethod
+          ? props.filterMethod(rawFilterValue, node)
+          : node.label.toLowerCase().includes(normalizedFilterValue);
+        if (!matched) {
+          continue;
+        }
+
+        matchedNodes.push(node);
+        visibleKeySet.add(node.id);
+        for (const parentId of node.parentIds) {
+          visibleKeySet.add(parentId);
+        }
+        addDescendantVisibleKeys(node.id, visibleKeySet);
+      }
+
+      for (const node of treeList.value) {
+        node.visible = visibleKeySet.has(node.id);
+        if (node.visible) {
+          visibleNodes.push(node);
+        }
+      }
+      visibleTreeList.value = visibleNodes;
+      matchedTreeList.value = matchedNodes;
+      return buildFilterPayload(visibleNodes, matchedNodes);
+    }
+
+    matchedTreeList.value = [];
+
+    // treeList is pre-order flattened, so parent visibility is already resolved here.
+    for (const node of treeList.value) {
+      const parent = node.parentId === undefined ? undefined : nodeMap.value.get(node.parentId);
+      node.visible = node.level === 0 || Boolean(parent?.visible && parent.expanded);
+      if (node.visible) {
+        visibleNodes.push(node);
+      }
+    }
+
+    visibleTreeList.value = visibleNodes;
+    return buildFilterPayload(visibleNodes);
+  }
+
+  function buildFilterPayload(
+    nodes = visibleTreeList.value,
+    matchedNodes = matchedTreeList.value
+  ) {
+    return {
+      value: String(props.filterValue ?? ""),
+      keys: nodes.map((node) => node.id),
+      nodes,
+      matchedKeys: matchedNodes.map((node) => node.id),
+      matchedNodes
+    };
+  }
+
+  function syncCachedExpandedKeys() {
+    if (!props.cacheExpandedKeys) {
+      return;
+    }
+
+    for (const node of treeList.value) {
+      if (node.expanded) {
+        cachedExpandedKeys.value.add(node.id);
+      } else {
+        cachedExpandedKeys.value.delete(node.id);
+      }
+    }
+  }
+
+  function canSelectNode(node: TreeNode) {
+    if (!canSelectDisabledNode(node)) {
+      return false;
+    }
+
+    if (!isMultiple.value && props.onlyRadioLeaf && !node.isLeaf) {
+      return false;
+    }
+
+    return true;
+  }
+
+  function canSelectDisabledNode(node: TreeNode) {
+    return !node.disabled || Boolean(props.checkedDisabled);
+  }
+
+  function clearCheckedStatus() {
+    for (const node of treeList.value) {
+      if (!canSelectDisabledNode(node)) {
+        continue;
+      }
+      node.checked = CHECK_STATUS_MAP.unchecked;
+    }
+  }
+
+  function getRawCheckedKeys() {
+    return treeList.value
+      .filter((node) => node.checked === CHECK_STATUS_MAP.checked)
+      .map((node) => node.id);
+  }
+
+  function normalizeKeys(value: TreeKey | TreeKey[] | null | undefined): TreeKey[] {
+    if (value === null || value === undefined) {
+      return [];
+    }
+
+    return Array.isArray(value) ? value : [value];
+  }
+
+  function getTreeConfigSignature() {
+    return JSON.stringify({
+      treeProps: resolvedTreeProps.value,
+      loadMode: props.loadMode,
+      alwaysFirstLoad: props.alwaysFirstLoad
+    });
+  }
+
+  function getExpansionConfigSignature() {
+    return JSON.stringify({
+      defaultExpandAll: props.defaultExpandAll,
+      defaultExpandedKeys: normalizeKeys(props.defaultExpandedKeys),
+      defaultExpandParent: props.defaultExpandParent,
+      expandChecked: props.expandChecked,
+      cacheExpandedKeys: props.cacheExpandedKeys
+    });
+  }
+
+  function getCheckedValueSignature() {
+    return JSON.stringify({
+      controlled: props.modelValue !== undefined,
+      value: props.modelValue !== undefined ? props.modelValue : props.defaultCheckedKeys
+    });
+  }
+
+  function getCheckedBehaviorSignature() {
+    return JSON.stringify({
+      multiple: props.multiple,
+      checkStrictly: props.checkStrictly,
+      onlyRadioLeaf: props.onlyRadioLeaf,
+      checkedDisabled: props.checkedDisabled,
+      packDisabledKey: resolvedPackDisabledKey.value
+    });
+  }
+
+  function isConfiguredCheckedStateCurrent(keys: TreeKey[]) {
+    const expectedKeySet = new Set(keys);
+    const currentKeySet = new Set([...getCheckedKeys(), ...pendingCheckedKeys]);
+    if (expectedKeySet.size !== currentKeySet.size) {
+      return false;
+    }
+    return [...expectedKeySet].every((key) => currentKeySet.has(key));
+  }
+
+  function getInitialCheckedKeys() {
+    if (props.modelValue !== undefined) {
+      return normalizeKeys(props.modelValue);
+    }
+
+    return normalizeKeys(props.defaultCheckedKeys);
+  }
+
+  function getSelectionIconClass(node: TreeNode) {
+    if (isMultiple.value) {
+      if (node.checked === CHECK_STATUS_MAP.checked) {
+        return "utv-tree-checkbox-checked";
+      }
+      if (node.checked === CHECK_STATUS_MAP.indeterminate) {
+        return "utv-tree-checkbox-indeterminate";
+      }
+      return "utv-tree-checkbox-outline";
+    }
+
+    if (node.checked === CHECK_STATUS_MAP.checked) {
+      return "utv-tree-radio-checked";
+    }
+
+    return "utv-tree-radio-outline";
+  }
+
+  function getCheckedKeys() {
+    return getCheckedNodes().map((node) => node.id);
+  }
+
+  function getHalfCheckedKeys() {
+    return getHalfCheckedNodes().map((node) => node.id);
+  }
+
+  function getUncheckedKeys() {
+    return getUncheckedNodes().map((node) => node.id);
+  }
+
+  function getCheckedNodes() {
+    return treeList.value.filter((node) => {
+      if (node.checked !== CHECK_STATUS_MAP.checked) {
+        return false;
+      }
+      return resolvedPackDisabledKey.value || !node.disabled;
+    });
+  }
+
+  function getHalfCheckedNodes() {
+    return treeList.value.filter((node) => node.checked === CHECK_STATUS_MAP.indeterminate);
+  }
+
+  function getUncheckedNodes() {
+    return treeList.value.filter((node) => node.checked === CHECK_STATUS_MAP.unchecked);
+  }
+
+  function getExpandedKeys() {
+    return getExpandedNodes().map((node) => node.id);
+  }
+
+  function getUnexpandedKeys() {
+    return getUnexpandedNodes().map((node) => node.id);
+  }
+
+  function getVisibleKeys() {
+    return getVisibleNodes().map((node) => node.id);
+  }
+
+  function getMatchedKeys() {
+    return getMatchedNodes().map((node) => node.id);
+  }
+
+  function getExpandedNodes() {
+    return treeList.value.filter((node) => !node.isLeaf && node.expanded);
+  }
+
+  function getUnexpandedNodes() {
+    return treeList.value.filter((node) => !node.isLeaf && !node.expanded);
+  }
+
+  function getVisibleNodes() {
+    return visibleTreeList.value;
+  }
+
+  function getMatchedNodes() {
+    return matchedTreeList.value;
+  }
+
+  function getNode(key: TreeKey) {
+    return nodeMap.value.get(key);
+  }
+
+  function getNodePath(keyOrNode: TreeKey | TreeNode) {
+    const targetNode = typeof keyOrNode === "object" ? keyOrNode : nodeMap.value.get(keyOrNode);
+    if (!targetNode) {
+      return [];
+    }
+
+    return [...targetNode.parentIds, targetNode.id]
+      .map((key) => nodeMap.value.get(key))
+      .filter((node): node is TreeNode => Boolean(node));
+  }
+
+  function buildCheckChangePayload(node: TreeNode): TreeCheckChangePayload {
+    const nodes = getCheckedNodes();
+    const keys = nodes.map((checkedNode) => checkedNode.id);
+    return {
+      value: isMultiple.value ? keys : (keys[0] ?? null),
+      keys,
+      nodes,
+      halfCheckedKeys: getHalfCheckedKeys(),
+      halfCheckedNodes: getHalfCheckedNodes(),
+      node
+    };
+  }
+
+  function publishPendingSelectionChange(keys: TreeKey[]) {
+    const changedNode = keys
+      .map((key) => nodeMap.value.get(key))
+      .find((node): node is TreeNode => node !== undefined && canSelectNode(node));
+    if (changedNode) {
+      pendingCheckChangePayload.value = buildCheckChangePayload(changedNode);
+    }
+  }
+
+  function reconcileMissingControlledKeys(wasInitialized: boolean, configuredKeys: TreeKey[]) {
+    if (!wasInitialized || props.modelValue === undefined || props.loadMode) {
+      return;
+    }
+    if (!configuredKeys.some((key) => !nodeMap.value.has(key))) {
+      return;
+    }
+
+    const keys = getCheckedKeys();
+    reconciledModelValue.value = {
+      value: isMultiple.value ? keys : (keys[0] ?? null)
+    };
+  }
+
+  function warnAboutInvalidKey(id: TreeKey, label: string) {
+    if (id !== undefined && id !== null) {
+      return;
+    }
+    warnOnce(
+      "missing",
+      `[uni-tree-view] 检测到缺失节点 key。请检查 tree-props.id 映射;问题节点 label:${label || "(空)"}`
+    );
+  }
+
+  function warnOnce(key: string, message: string) {
+    if (!(import.meta as ImportMeta & { env?: { DEV?: boolean } }).env?.DEV || warnedInvalidKeys.has(key)) {
+      return;
+    }
+    warnedInvalidKeys.add(key);
+    console.warn(message);
+  }
+
+  function setCheckedKeys(keys: TreeKey | TreeKey[], checked = true) {
+    const normalizedKeys = normalizeKeys(keys);
+
+    if (!checked) {
+      for (const key of normalizedKeys) {
+        pendingCheckedKeys.delete(key);
+        pendingImperativeCheckedKeys.delete(key);
+      }
+      const changedNode = normalizedKeys
+        .map((key) => nodeMap.value.get(key))
+        .find((node): node is TreeNode => node !== undefined && canSelectNode(node));
+      if (!changedNode) {
+        return null;
+      }
+
+      if (isMultiple.value) {
+        if (props.checkStrictly) {
+          for (const key of normalizedKeys) {
+            const node = nodeMap.value.get(key);
+            if (node && canSelectDisabledNode(node)) {
+              node.checked = CHECK_STATUS_MAP.unchecked;
+            }
+          }
+        } else {
+          updateNodeAndDescendantsStatus(normalizedKeys, CHECK_STATUS_MAP.unchecked, Boolean(props.checkedDisabled));
+          updateParentNodesStatus(normalizedKeys);
+        }
+      } else {
+        for (const key of normalizedKeys) {
+          const node = nodeMap.value.get(key);
+          if (node && canSelectNode(node)) {
+            node.checked = CHECK_STATUS_MAP.unchecked;
+          }
+        }
+      }
+      return buildCheckChangePayload(changedNode);
+    }
+
+    if (!isMultiple.value) {
+      const targetKey = normalizedKeys.find((key) => {
+        const node = nodeMap.value.get(key);
+        return node === undefined || canSelectNode(node);
+      });
+      if (targetKey === undefined) {
+        return null;
+      }
+
+      const targetNode = nodeMap.value.get(targetKey);
+      pendingCheckedKeys = new Set();
+      pendingImperativeCheckedKeys = new Set();
+      if (!targetNode) {
+        pendingCheckedKeys.add(targetKey);
+        pendingImperativeCheckedKeys.add(targetKey);
+        return null;
+      }
+
+      clearCheckedStatus();
+      targetNode.checked = CHECK_STATUS_MAP.checked;
+      return buildCheckChangePayload(targetNode);
+    }
+
+    const unresolvedKeys = normalizedKeys.filter((key) => !nodeMap.value.has(key));
+    for (const key of unresolvedKeys) {
+      pendingCheckedKeys.add(key);
+      pendingImperativeCheckedKeys.add(key);
+    }
+    const changedNode = normalizedKeys
+      .map((key) => nodeMap.value.get(key))
+      .find((node): node is TreeNode => node !== undefined && canSelectNode(node));
+    if (!changedNode) {
+      return null;
+    }
+
+    if (props.checkStrictly) {
+      for (const key of normalizedKeys) {
+        const node = nodeMap.value.get(key);
+        if (node && canSelectDisabledNode(node)) {
+          node.checked = CHECK_STATUS_MAP.checked;
+        }
+      }
+    } else {
+      updateNodeAndDescendantsStatus(normalizedKeys, CHECK_STATUS_MAP.checked, Boolean(props.checkedDisabled));
+      updateParentNodesStatus(normalizedKeys);
+    }
+
+    return buildCheckChangePayload(changedNode);
+  }
+
+  function setExpandedKeys(keys: TreeKey[] | "all", expanded = true) {
+    if (keys === "all") {
+      for (const node of treeList.value) {
+        if (!node.isLeaf) {
+          node.expanded = expanded;
+          syncExpandedCacheForNode(node);
+        }
+      }
+      updateVisibility();
+      return;
+    }
+
+    for (const key of keys) {
+      const node = nodeMap.value.get(key);
+      if (node && !node.isLeaf) {
+        node.expanded = expanded;
+        syncExpandedCacheForNode(node);
+      }
+    }
+    updateVisibility();
+  }
+
+  function syncExpandedCacheForNode(node: TreeNode) {
+    if (!props.cacheExpandedKeys) {
+      return;
+    }
+
+    if (node.expanded) {
+      cachedExpandedKeys.value.add(node.id);
+    } else {
+      cachedExpandedKeys.value.delete(node.id);
+    }
+  }
+
+  function expandAll() {
+    setExpandedKeys("all", true);
+  }
+
+  function collapseAll() {
+    setExpandedKeys("all", false);
+  }
+
+  function resolveIsLeaf(item: TreeDataItem, node: TreeNode) {
+    if (props.isLeafFn) {
+      return props.isLeafFn(item, node);
+    }
+
+    const { children: childrenKey, leaf: leafKey = "leaf" } = resolvedTreeProps.value;
+    const children = item[childrenKey];
+    if (props.loadMode && item[leafKey] !== undefined) {
+      return Boolean(item[leafKey]);
+    }
+
+    if (props.loadMode) {
+      return false;
+    }
+
+    return !(Array.isArray(children) && children.length > 0);
+  }
+
+  function addDescendantVisibleKeys(nodeId: TreeKey, visibleKeySet: Set<TreeKey>) {
+    const pendingNodes = [...(childrenMap.value.get(nodeId) ?? [])];
+    while (pendingNodes.length > 0) {
+      const child = pendingNodes.pop();
+      if (!child) {
+        continue;
+      }
+      visibleKeySet.add(child.id);
+      pendingNodes.push(...(childrenMap.value.get(child.id) ?? []));
+    }
+  }
+
+  function removeDescendants(nodeId: TreeKey) {
+    const children = childrenMap.value.get(nodeId) ?? [];
+    const descendantIds = new Set<TreeKey>();
+    for (const child of children) {
+      collectDescendantIds(child.id, descendantIds);
+    }
+
+    if (descendantIds.size === 0) {
+      return;
+    }
+
+    treeList.value = treeList.value.filter((node) => !descendantIds.has(node.id));
+    for (const id of descendantIds) {
+      nodeMap.value.delete(id);
+      childrenMap.value.delete(id);
+    }
+    childrenMap.value.delete(nodeId);
+  }
+
+  function collectDescendantIds(nodeId: TreeKey, ids: Set<TreeKey>) {
+    ids.add(nodeId);
+    const children = childrenMap.value.get(nodeId);
+    if (!children?.length) {
+      return;
+    }
+
+    for (const child of children) {
+      collectDescendantIds(child.id, ids);
+    }
+  }
+
+  function replaceNodeChildren(node: TreeNode, children: TreeDataItem[]) {
+    const shouldInheritChecked = isMultiple.value
+      && !props.checkStrictly
+      && node.checked === CHECK_STATUS_MAP.checked;
+    removeDescendants(node.id);
+
+    const childNodes = flattenTree(children, node.level + 1, [...node.parentIds, node.id], [...node.parents, node.source]);
+    treeVersion.value += 1;
+    const nodeIndex = treeList.value.findIndex((item) => item.id === node.id);
+    if (nodeIndex === -1) {
+      treeList.value.push(...childNodes);
+    } else {
+      treeList.value.splice(nodeIndex + 1, 0, ...childNodes);
+    }
+
+    node.isLeaf = children.length === 0 && Boolean(props.loadMode);
+    node.loaded = true;
+    if (shouldInheritChecked) {
+      updateNodeAndDescendantsStatus(childNodes.map((child) => child.id), CHECK_STATUS_MAP.checked);
+    }
+    applyPendingCheckedState();
+    updateParentNodesStatus(node.id);
+    updateVisibility();
+  }
+
+  async function loadNode(node: TreeNode) {
+    if (!props.loadMode || !props.loadApi || node.isLeaf || node.loading || node.loaded) {
+      return [];
+    }
+
+    node.loading = true;
+    node.loadError = null;
+    try {
+      const children = await props.loadApi(node);
+      const normalizedChildren = Array.isArray(children) ? children : [];
+      if (nodeMap.value.get(node.id) !== node) {
+        return normalizedChildren;
+      }
+      replaceNodeChildren(node, normalizedChildren);
+      return normalizedChildren;
+    } catch (error) {
+      if (nodeMap.value.get(node.id) !== node) {
+        return [];
+      }
+      node.loadError = error;
+      throw error;
+    } finally {
+      node.loading = false;
+    }
+  }
+
+  return {
+    treeList,
+    childrenMap,
+    nodeMap,
+    resolvedTreeProps,
+    isMultiple,
+    visibleTreeList,
+    matchedTreeList,
+    treeVersion,
+    reconciledModelValue,
+    pendingCheckChangePayload,
+    initializeTree,
+    toggleExpand,
+    checkNode,
+    applyCheckedState,
+    applyExpandedState,
+    hasChildren,
+    isExpandable,
+    getSelectionIconClass,
+    setCheckedKeys,
+    getCheckedKeys,
+    getHalfCheckedKeys,
+    getUncheckedKeys,
+    getCheckedNodes,
+    getHalfCheckedNodes,
+    getUncheckedNodes,
+    setExpandedKeys,
+    getExpandedKeys,
+    getUnexpandedKeys,
+    getVisibleKeys,
+    getMatchedKeys,
+    getExpandedNodes,
+    getUnexpandedNodes,
+    getVisibleNodes,
+    getMatchedNodes,
+    getNode,
+    getNodePath,
+    expandAll,
+    collapseAll,
+    loadNode
+  };
+}

+ 120 - 0
uni_modules/KieranYin9527-tree/components/uni-tree-view/useVirtualTreeList.ts

@@ -0,0 +1,120 @@
+import { computed, shallowRef, toValue } from "vue";
+import type { MaybeRefOrGetter } from "vue";
+
+export interface UseVirtualTreeListOptions<T> {
+  items: MaybeRefOrGetter<readonly T[]>;
+  virtual: MaybeRefOrGetter<boolean | undefined>;
+  itemHeight: MaybeRefOrGetter<number | undefined>;
+  height: MaybeRefOrGetter<number | undefined>;
+  overscan: MaybeRefOrGetter<number | undefined>;
+}
+
+export interface UniTreeVirtualScrollEvent {
+  detail?: {
+    scrollTop?: number;
+  };
+}
+
+export function useVirtualTreeList<T>(options: UseVirtualTreeListOptions<T>) {
+  const scrollTop = shallowRef(0);
+
+  const itemHeight = computed(() => normalizePositiveNumber(options.itemHeight));
+  const height = computed(() => normalizePositiveNumber(options.height));
+
+  const virtualEnabled = computed(() => {
+    return Boolean(toValue(options.virtual) && itemHeight.value > 0 && height.value > 0);
+  });
+
+  const overscan = computed(() => {
+    const value = Number(toValue(options.overscan));
+    return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
+  });
+
+  const totalCount = computed(() => toValue(options.items).length);
+
+  const startIndex = computed(() => {
+    if (!virtualEnabled.value || totalCount.value === 0) {
+      return 0;
+    }
+
+    const rawStart = Math.floor(scrollTop.value / itemHeight.value) - overscan.value;
+    return Math.min(Math.max(0, rawStart), totalCount.value - 1);
+  });
+
+  const endIndex = computed(() => {
+    if (!virtualEnabled.value) {
+      return totalCount.value;
+    }
+
+    const visibleCount = Math.ceil(height.value / itemHeight.value) + overscan.value * 2;
+    return Math.min(totalCount.value, startIndex.value + visibleCount);
+  });
+
+  const renderedItems = computed(() => {
+    const items = toValue(options.items);
+    if (!virtualEnabled.value) {
+      return items;
+    }
+
+    return items.slice(startIndex.value, endIndex.value);
+  });
+
+  const topPadding = computed(() => {
+    return virtualEnabled.value ? startIndex.value * itemHeight.value : 0;
+  });
+
+  const bottomPadding = computed(() => {
+    if (!virtualEnabled.value) {
+      return 0;
+    }
+
+    return Math.max(0, (totalCount.value - endIndex.value) * itemHeight.value);
+  });
+
+  const scrollViewStyle = computed(() => {
+    if (!virtualEnabled.value) {
+      return undefined;
+    }
+
+    return {
+      height: `${height.value}px`
+    };
+  });
+
+  function handleScroll(event: UniTreeVirtualScrollEvent) {
+    if (!virtualEnabled.value) {
+      return;
+    }
+
+    const nextScrollTop = Number(event.detail?.scrollTop ?? 0);
+    scrollTop.value = Number.isFinite(nextScrollTop) ? Math.max(0, nextScrollTop) : 0;
+  }
+
+  function scrollToIndex(index: number) {
+    if (!virtualEnabled.value || totalCount.value === 0) {
+      return false;
+    }
+
+    const normalizedIndex = Math.min(Math.max(0, Math.floor(index)), totalCount.value - 1);
+    scrollTop.value = normalizedIndex * itemHeight.value;
+    return true;
+  }
+
+  return {
+    scrollTop,
+    virtualEnabled,
+    startIndex,
+    endIndex,
+    renderedItems,
+    topPadding,
+    bottomPadding,
+    scrollViewStyle,
+    handleScroll,
+    scrollToIndex
+  };
+}
+
+function normalizePositiveNumber(value: MaybeRefOrGetter<number | undefined>) {
+  const numericValue = Number(toValue(value));
+  return Number.isFinite(numericValue) && numericValue > 0 ? numericValue : 0;
+}

+ 7 - 0
uni_modules/KieranYin9527-tree/global.d.ts

@@ -0,0 +1,7 @@
+declare module "vue" {
+  export interface GlobalComponents {
+    UniTreeView: typeof import("./components/uni-tree-view/uni-tree-view.vue")["default"];
+  }
+}
+
+export {};

+ 365 - 0
uni_modules/KieranYin9527-tree/index.d.ts

@@ -0,0 +1,365 @@
+import { ComponentPublicInstance } from 'vue';
+
+declare const CHECK_STATUS_MAP: {
+    readonly checked: "checked";
+    readonly unchecked: "unchecked";
+    readonly indeterminate: "indeterminate";
+};
+
+type TreeKey = string | number;
+type CheckStatus = typeof CHECK_STATUS_MAP[keyof typeof CHECK_STATUS_MAP];
+type TreeModelValue = TreeKey | TreeKey[] | null;
+interface TreeDataItem {
+    [key: string]: any;
+}
+interface TreeNode {
+    id: TreeKey;
+    label: string;
+    append: string;
+    icon: string;
+    path: string[];
+    source: TreeDataItem;
+    parentId?: TreeKey;
+    parentIds: TreeKey[];
+    parents: TreeDataItem[];
+    level: number;
+    disabled: boolean;
+    checked: CheckStatus;
+    expanded: boolean;
+    visible: boolean;
+    isLeaf: boolean;
+    loaded: boolean;
+    loading: boolean;
+    loadError: unknown;
+}
+interface TreeProps {
+    id: string;
+    label: string;
+    children: string;
+    disabled?: string;
+    leaf?: string;
+    append?: string;
+    icon?: string;
+}
+interface TreeCheckChangePayload {
+    value: TreeModelValue;
+    keys: TreeKey[];
+    nodes: TreeNode[];
+    /** Current half-checked keys in linked multiple-selection mode. */
+    halfCheckedKeys: TreeKey[];
+    /** Current half-checked nodes in linked multiple-selection mode. */
+    halfCheckedNodes: TreeNode[];
+    node: TreeNode;
+}
+interface TreeLoadPayload {
+    node: TreeNode;
+    children: TreeDataItem[];
+}
+interface TreeLoadErrorPayload {
+    node: TreeNode;
+    error: unknown;
+}
+interface TreeScrollToOptions {
+    /** Expand ancestors before locating the node. */
+    expandParents?: boolean;
+}
+interface TreeExpandPayload {
+    expanded: boolean;
+    node: TreeNode;
+}
+interface TreeNodeClickPayload {
+    id: TreeKey;
+    node: TreeNode;
+    path: TreeNode[];
+}
+interface TreeFilterPayload {
+    value: string;
+    /** Visible keys after filtering, including direct matches, ancestors and descendants. */
+    keys: TreeKey[];
+    /** Visible nodes after filtering, including direct matches, ancestors and descendants. */
+    nodes: TreeNode[];
+    /** Keys directly matched by the built-in or custom filter rule. */
+    matchedKeys: TreeKey[];
+    /** Nodes directly matched by the built-in or custom filter rule. */
+    matchedNodes: TreeNode[];
+}
+interface TreeSlotProps {
+    node: TreeNode;
+    data: TreeDataItem;
+    path: TreeNode[];
+}
+interface TreeEmptySlotProps {
+    filterValue: string;
+}
+interface UniTreeViewSlots {
+    default?: (props: TreeSlotProps) => unknown;
+    label?: (props: TreeSlotProps) => unknown;
+    icon?: (props: TreeSlotProps) => unknown;
+    append?: (props: TreeSlotProps) => unknown;
+    empty?: (props: TreeEmptySlotProps) => unknown;
+    "empty-filter"?: (props: TreeEmptySlotProps) => unknown;
+}
+interface UniTreeViewProps {
+    /** Current selected value. Single select uses one key, multiple select uses an array. */
+    modelValue?: TreeModelValue;
+    /** Tree data. */
+    data?: TreeDataItem[];
+    /** Filter keyword. Matching nodes and their related branch stay visible. */
+    filterValue?: string;
+    /** Custom node matcher used when filterValue is not empty. */
+    filterMethod?: (value: string, node: TreeNode) => boolean;
+    /** Highlight literal filter keyword matches in the built-in label. */
+    highlightFilter?: boolean;
+    /** Default checked keys for uncontrolled initial state. */
+    defaultCheckedKeys?: TreeKey | TreeKey[] | null;
+    /** Field mapping for id, label, children, disabled, leaf, append and icon. */
+    treeProps?: Partial<TreeProps>;
+    /** Theme color for active checkbox/radio. */
+    themeColor?: string;
+    /** Whether to enable and show the selection control. */
+    selectable?: boolean;
+    /** Whether to show radio UI in single-select mode. */
+    showRadioIcon?: boolean;
+    /** Whether to support multiple selection. */
+    multiple?: boolean;
+    /** Whether clicking a node row changes its selection state. */
+    checkOnClickNode?: boolean;
+    /** Whether clicking a leaf node row changes its selection state. */
+    checkOnClickLeaf?: boolean;
+    /** Whether clicking a node row expands or collapses it. */
+    expandOnClickNode?: boolean;
+    /** Whether expanding a node collapses its expanded siblings. */
+    accordion?: boolean;
+    /** Whether parent and child checked states are independent. */
+    checkStrictly?: boolean;
+    /** Single-select mode can only select leaf nodes. Ignored when `multiple` is true. */
+    onlyRadioLeaf?: boolean;
+    /** Whether all nodes are expanded initially. */
+    defaultExpandAll?: boolean;
+    /** Default expanded node keys. */
+    defaultExpandedKeys?: TreeKey[];
+    /** Whether default expanded keys also expand all their ancestors. */
+    defaultExpandParent?: boolean;
+    /** Expand ancestors of checked nodes initially. */
+    expandChecked?: boolean;
+    /** Preserve runtime expanded state when tree data is rebuilt. */
+    cacheExpandedKeys?: boolean;
+    /** Lazy load mode. Nodes can be expanded before children exist. */
+    loadMode?: boolean;
+    /** Lazy load function. */
+    loadApi?: (node: TreeNode) => TreeDataItem[] | Promise<TreeDataItem[]>;
+    /** Custom leaf resolver. */
+    isLeafFn?: (item: TreeDataItem, node: TreeNode) => boolean;
+    /** Load once on first expand even when static children exist. */
+    alwaysFirstLoad?: boolean;
+    /** Whether disabled nodes can participate in selection state changes. */
+    checkedDisabled?: boolean;
+    /** Whether checked disabled nodes are included in returned keys/nodes. */
+    packDisabledKey?: boolean;
+    /** @deprecated Use `packDisabledKey` instead. */
+    packDisabledkey?: boolean;
+    /** Custom class name added to every node row. */
+    nodeClass?: string;
+    /** Tree item indent in rpx. */
+    indent?: number;
+    /** Selection control placement. */
+    selectionPlacement?: "left" | "right";
+    /** Empty text shown when data is empty. */
+    emptyText?: string;
+    /** Show label path under the node label. */
+    showPath?: boolean;
+    /** Separator used by the built-in path display. */
+    pathSeparator?: string;
+    /** Enable fixed-height virtual rendering for very large visible node lists. */
+    virtual?: boolean;
+    /** Row height in px when virtual rendering is enabled. */
+    virtualItemHeight?: number;
+    /** Scroll container height in px when virtual rendering is enabled. */
+    virtualHeight?: number;
+    /** Extra rows rendered before and after the viewport in virtual mode. */
+    virtualOverscan?: number;
+}
+interface UniTreeViewExposed {
+    setCheckedKeys: (keys: TreeKey | TreeKey[], checked?: boolean) => TreeModelValue;
+    getCheckedKeys: () => TreeKey[];
+    getHalfCheckedKeys: () => TreeKey[];
+    getUncheckedKeys: () => TreeKey[];
+    getCheckedNodes: () => TreeNode[];
+    getHalfCheckedNodes: () => TreeNode[];
+    getUncheckedNodes: () => TreeNode[];
+    setExpandedKeys: (keys: TreeKey[] | "all", expanded?: boolean) => void;
+    getExpandedKeys: () => TreeKey[];
+    getUnexpandedKeys: () => TreeKey[];
+    getVisibleKeys: () => TreeKey[];
+    getMatchedKeys: () => TreeKey[];
+    getExpandedNodes: () => TreeNode[];
+    getUnexpandedNodes: () => TreeNode[];
+    getVisibleNodes: () => TreeNode[];
+    getMatchedNodes: () => TreeNode[];
+    getNode: (key: TreeKey) => TreeNode | undefined;
+    getNodePath: (keyOrNode: TreeKey | TreeNode) => TreeNode[];
+    expandAll: () => void;
+    collapseAll: () => void;
+    loadNode: (node: TreeNode) => Promise<TreeDataItem[]>;
+    retryLoad: (keyOrNode: TreeKey | TreeNode) => Promise<TreeDataItem[]>;
+    scrollToKey: (key: TreeKey, options?: TreeScrollToOptions) => Promise<boolean>;
+}
+/**
+ * 事件表使用 Vue 3.3+ 的「具名元组」形态,且必须是 type 别名而非 interface:
+ * `defineEmits<T>()` 的约束是 `Record<string, any[]>`,interface 没有隐式索引签名,无法满足。
+ */
+type UniTreeViewEmits = {
+    "update:modelValue": [value: TreeModelValue];
+    "check-change": [payload: TreeCheckChangePayload];
+    "expand-change": [payload: TreeExpandPayload];
+    "load": [payload: TreeLoadPayload];
+    "load-error": [payload: TreeLoadErrorPayload];
+    "node-click": [payload: TreeNodeClickPayload];
+    "filter-change": [payload: TreeFilterPayload];
+};
+
+declare function getIsPc(): boolean;
+
+interface IPlatform {
+  APP: "APP";
+  APP_ANDROID: "APP-ANDROID";
+  APP_IOS: "APP-IOS";
+  APP_HARMONY: "APP-HARMONY";
+  WEB: "WEB";
+  MP: "MP";
+  MP_WEIXIN: "MP-WEIXIN";
+  MP_ALIPAY: "MP-ALIPAY";
+  MP_BAIDU: "MP-BAIDU";
+  MP_TOUTIAO: "MP-TOUTIAO";
+  MP_LARK: "MP-LARK";
+  MP_QQ: "MP-QQ";
+  MP_KUAISHOU: "MP-KUAISHOU";
+  MP_JD: "MP-JD";
+  MP_360: "MP-360";
+  MP_XHS: "MP-XHS";
+  MP_HARMONY: "MP-HARMONY";
+  QUICKAPP_WEBVIEW: "QUICKAPP-WEBVIEW";
+  QUICKAPP_WEBVIEW_UNION: "QUICKAPP-WEBVIEW-UNION";
+  QUICKAPP_WEBVIEW_HUAWEI: "QUICKAPP-WEBVIEW-HUAWEI";
+  OTHER: "OTHER";
+}
+
+declare const Platform: IPlatform;
+
+declare function getPlatform(): IPlatform[keyof IPlatform];
+
+declare const platform: ReturnType<typeof getPlatform>;
+
+/** App */
+declare const isApp: boolean;
+/** App Android */
+declare const isAppAndroid: boolean;
+/** App iOS */
+declare const isAppIos: boolean;
+/** App HarmonyOS Next */
+declare const isAppHarmony: boolean;
+/** Web */
+declare const isWeb: boolean;
+/** 小程序 */
+declare const isMp: boolean;
+/** 微信小程序 */
+declare const isMpWeixin: boolean;
+/** 支付宝小程序 */
+declare const isMpAlipay: boolean;
+/** 百度小程序 */
+declare const isMpBaidu: boolean;
+/** 头条小程序 */
+declare const isMpToutiao: boolean;
+/** 飞书小程序 */
+declare const isMpLark: boolean;
+/** QQ小程序 */
+declare const isMpQq: boolean;
+/** 快手小程序 */
+declare const isMpKuaishou: boolean;
+/** 京东小程序 */
+declare const isMpJd: boolean;
+/** 360小程序 */
+declare const isMp360: boolean;
+/** 小红书小程序 */
+declare const isMpXhs: boolean;
+/** 鸿蒙元服务 */
+declare const isMpHarmony: boolean;
+/** 快应用 */
+declare const isQuickappWebview: boolean;
+/** 快应用联盟 */
+declare const isQuickappWebviewUnion: boolean;
+/** 快应用华为 */
+declare const isQuickappWebviewHuawei: boolean;
+/** 其他平台 */
+declare const isOtherPlatform: boolean;
+
+declare function isEmpty(value: unknown): boolean;
+
+declare function defaultTo(value: any, ...defaultValues: any[]): any;
+
+declare function sleep(timeout: number): Promise<void>;
+
+declare function upperFirst(value: string): string;
+
+declare function lowerFirst(value: string): string;
+
+type EventType = string | symbol;
+
+type Handler<T = unknown> = (event: T) => void;
+
+type WildcardHandler<T = Record<string, unknown>> = (
+  type: keyof T,
+  event: T[keyof T]
+) => void;
+
+type EventHandlerList<T = unknown> = Array<Handler<T>>;
+
+type WildCardEventHandlerList<T = Record<string, unknown>> = Array<
+  WildcardHandler<T>
+>;
+
+type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
+  keyof Events | "*",
+  EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
+>;
+
+interface Emitter<Events extends Record<EventType, unknown>> {
+  events: EventHandlerMap<Events>;
+
+  on: (<Key extends keyof Events>(
+    type: Key,
+    handler: Handler<Events[Key]>
+  ) => void) & ((type: "*", handler: WildcardHandler<Events>) => void);
+
+  off: (<Key extends keyof Events>(
+    type: Key,
+    handler?: Handler<Events[Key]>
+  ) => void) & ((type: "*", handler?: WildcardHandler<Events>) => void);
+
+  emit: (<Key extends keyof Events>(type: Key, event: Events[Key]) => void) & (<Key extends keyof Events>(
+    type: undefined extends Events[Key] ? Key : never
+  ) => void);
+}
+
+declare function mitt<Events extends Record<EventType, unknown>>(
+  events?: EventHandlerMap<Events>
+): Emitter<Events>;
+
+declare function getDeviceInfo(): UniApp.GetDeviceInfoResult | UniApp.GetSystemInfoResult;
+
+declare function getWindowInfo(): UniApp.GetWindowInfoResult | UniApp.GetSystemInfoResult;
+
+declare function getAppBaseInfo(): UniApp.GetAppBaseInfoResult | UniApp.GetSystemInfoResult;
+
+declare function getVersion(): string;
+
+declare function compareVersion(v1: string, v2: string): 0 | 1 | -1;
+
+declare function querySelect(
+  component: ComponentPublicInstance,
+  selector: string,
+  fields: UniApp.NodeField
+): Promise<UniApp.NodeInfo>;
+
+export { Platform, compareVersion, defaultTo, getAppBaseInfo, getDeviceInfo, getIsPc, getPlatform, getVersion, getWindowInfo, isApp, isAppAndroid, isAppHarmony, isAppIos, isEmpty, isMp, isMp360, isMpAlipay, isMpBaidu, isMpHarmony, isMpJd, isMpKuaishou, isMpLark, isMpQq, isMpToutiao, isMpWeixin, isMpXhs, isOtherPlatform, isQuickappWebview, isQuickappWebviewHuawei, isQuickappWebviewUnion, isWeb, lowerFirst, mitt, platform, querySelect, sleep, upperFirst };
+export type { CheckStatus, Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, IPlatform, TreeCheckChangePayload, TreeDataItem, TreeEmptySlotProps, TreeExpandPayload, TreeFilterPayload, TreeKey, TreeLoadErrorPayload, TreeLoadPayload, TreeModelValue, TreeNode, TreeNodeClickPayload, TreeProps, TreeScrollToOptions, TreeSlotProps, UniTreeViewEmits, UniTreeViewExposed, UniTreeViewProps, UniTreeViewSlots, WildCardEventHandlerList, WildcardHandler };

+ 7 - 0
uni_modules/KieranYin9527-tree/index.js

@@ -0,0 +1,7 @@
+export * from "./components";
+
+export * from "./utils/device";
+export * from "./utils/env";
+export * from "./utils/helpers";
+export * from "./utils/mitt";
+export * from "./utils/uni";

+ 21 - 0
uni_modules/KieranYin9527-tree/license.md

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025-present OFreshman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 106 - 0
uni_modules/KieranYin9527-tree/package.json

@@ -0,0 +1,106 @@
+{
+  "id": "KieranYin9527-tree",
+  "displayName": "Uni Tree View",
+  "version": "0.6.2",
+  "description": "适用于 uni-app 与 Vue 3 的跨平台树形组件,支持单选、多选、筛选、懒加载和虚拟列表",
+  "author": "OFreshman <415561402@qq.com>",
+  "license": "MIT",
+  "homepage": "https://ofreshman.github.io/uni-tree-view/",
+  "repository": "https://github.com/OFreshman/uni-tree-view",
+  "bugs": "https://github.com/OFreshman/uni-tree-view/issues",
+  "keywords": [
+    "树形组件",
+    "tree",
+    "tree-view",
+    "uni-app",
+    "vue3"
+],
+  "engines": {
+    "HBuilderX": "^4.15.0",
+    "uni-app": "^4.15",
+    "uni-app-x": ""
+  },
+  "dcloudext": {
+    "type": "component-vue",
+    "sale": {
+      "regular": {
+        "price": "0.00"
+    },
+    "sourcecode": {
+        "price": "0.00"
+    }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "插件不采集任何数据",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/uni-tree-view",
+    "darkmode": "x",
+    "i18n": "x",
+    "widescreen": "√"
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "client": {
+        "uni-app": {
+          "vue": {
+            "vue2": "x",
+            "vue3": "√"
+          },
+          "web": {
+            "safari": "√",
+            "chrome": "√"
+          },
+          "app": {
+            "vue": "-",
+            "nvue": "-",
+            "android": "-",
+            "ios": "-",
+            "harmony": "-"
+          },
+          "mp": {
+            "weixin": "√",
+            "alipay": "√",
+            "toutiao": "-",
+            "baidu": "-",
+            "kuaishou": "-",
+            "jd": "-",
+            "harmony": "-",
+            "qq": "-",
+            "lark": "-",
+            "xhs": "-"
+          },
+          "quickapp": {
+            "huawei": "x",
+            "union": "x"
+          }
+        },
+        "uni-app-x": {
+          "web": {
+            "safari": "x",
+            "chrome": "x"
+          },
+          "app": {
+            "android": "x",
+            "ios": "x",
+            "harmony": "x"
+          },
+          "mp": {
+            "weixin": "x"
+          }
+        }
+      },
+      "cloud": {
+        "aliyun": "x",
+        "tcb": "x",
+        "alipay": "x"
+      }
+    }
+  }
+}

+ 187 - 0
uni_modules/KieranYin9527-tree/readme.md

@@ -0,0 +1,187 @@
+# Uni Tree View(`uni_modules` 插件)
+
+插件已按 `uni_modules` 规范导入,组件目录符合 easycom 约定,模板里直接写 `<uni-tree-view>` 即可,**不需要** import:
+
+```vue
+<template>
+  <uni-tree-view :data="treeData" />
+</template>
+```
+
+需要 TypeScript 类型时从插件目录导入:
+
+```ts
+// CLI 工程(插件在 src/uni_modules 下,`@` 指向 src)
+import type { TreeDataItem, UniTreeViewExposed } from "@/uni_modules/KieranYin9527-tree";
+```
+
+HBuilderX 可视化工程没有 `@` 别名、插件也在工程根目录,改用相对路径指向 `uni_modules/KieranYin9527-tree`。
+
+下面是与 npm 包共用的完整说明,其中「npm 方式」一节只适用于 npm 通道。
+
+---
+
+# uni-tree-view
+
+<p align="center">
+  <img src="https://raw.githubusercontent.com/OFreshman/uni-tree-view/main/assets/uni-tree-view-logo.svg" alt="uni-tree-view Logo" width="180" />
+</p>
+
+[![npm version](https://img.shields.io/npm/v/uni-tree-view.svg)](https://www.npmjs.com/package/uni-tree-view)
+[![CI](https://github.com/OFreshman/uni-tree-view/actions/workflows/ci.yml/badge.svg)](https://github.com/OFreshman/uni-tree-view/actions/workflows/ci.yml)
+[![license](https://img.shields.io/npm/l/uni-tree-view.svg)](./LICENSE)
+
+适用于 uni-app + Vue 3 的跨端树形列表/选择组件,一套代码运行在微信小程序、支付宝小程序和 H5。
+
+**📖 [完整文档](https://ofreshman.github.io/uni-tree-view/)** · [在线演示](https://ofreshman.github.io/uni-tree-view/ui/index.html#/) · [快速上手](https://ofreshman.github.io/uni-tree-view/guide/quick-start) · [API 参考](https://ofreshman.github.io/uni-tree-view/apis/props) · [常见问题](https://ofreshman.github.io/uni-tree-view/guide/faq)
+
+> 文档站双线部署,内容原则上保持一致:主入口为 GitHub Pages(上方链接);访问较慢时可切换到 [Netlify 镜像](https://uni-tree-view.netlify.app/)。
+
+> 使用 AI Coding 工具时,可将 [llms.txt](https://ofreshman.github.io/uni-tree-view/llms.txt) 作为精简的文档导航入口。
+
+> **项目状态:** 当前处于 `0.x` 早期阶段。核心能力已有自动化测试,并完成 H5 交互验证及微信/支付宝小程序构建验证;但在 `1.0.0` 前公开 API 和边界行为仍可能调整,升级前请查阅 [CHANGELOG](https://github.com/OFreshman/uni-tree-view/blob/main/CHANGELOG.md)。
+
+## 特性
+
+- 🌲 展开收起、单选/多选、父子联动、严格模式、禁用节点
+- 🔍 关键词过滤、自定义匹配、命中高亮
+- ⚡ 固定行高虚拟渲染,只渲染可视区域,适合大数据树
+- 🔌 懒加载子节点,内置加载中、加载失败和重试状态
+- 🎨 主题色、`node-class` 以及文本、图标、尾部内容和空状态插槽自由定制
+- 📦 零运行时依赖,npm 与 DCloud 插件市场双通道分发
+
+## 安装
+
+```bash
+pnpm add uni-tree-view
+```
+
+推荐使用 npm;也可以在 [DCloud 插件市场](https://ext.dcloud.net.cn/plugin?id=28897) 导入 `Uni Tree View`,插件按 `uni_modules` 规范发布,导入后位于 `uni_modules/KieranYin9527-tree`(CLI 工程为 `src/uni_modules/KieranYin9527-tree`)。两种方式的取舍见[安装说明](https://ofreshman.github.io/uni-tree-view/guide/installation)。
+
+## 使用
+
+### npm 方式
+
+通过 npm 安装后需要导入组件:
+
+```vue
+<template>
+  <uni-tree-view
+    v-model="checkedValue"
+    selectable
+    multiple
+    :data="treeData"
+    @check-change="handleCheckChange"
+  />
+</template>
+
+<script setup>
+import UniTreeView from "uni-tree-view";
+import { ref } from "vue";
+
+const checkedValue = ref([]);
+const treeData = [
+  {
+    id: "building-a",
+    label: "A 栋",
+    children: [
+      { id: "floor-a-1", label: "1 层" },
+      { id: "floor-a-2", label: "2 层", disabled: true }
+    ]
+  }
+];
+
+function handleCheckChange({ keys }) {
+  console.log("当前选中:", keys);
+}
+</script>
+```
+
+### DCloud 插件市场方式
+
+从插件市场导入到 `uni_modules` 后,通过 easycom 自动导入,无需手动 import:
+
+```vue
+<template>
+  <uni-tree-view
+    v-model="checkedValue"
+    selectable
+    multiple
+    :data="treeData"
+    @check-change="handleCheckChange"
+  />
+</template>
+
+<script setup>
+import { ref } from "vue";
+
+const checkedValue = ref([]);
+const treeData = [
+  {
+    id: "building-a",
+    label: "A 栋",
+    children: [
+      { id: "floor-a-1", label: "1 层" },
+      { id: "floor-a-2", label: "2 层", disabled: true }
+    ]
+  }
+];
+
+function handleCheckChange({ keys }) {
+  console.log("当前选中:", keys);
+}
+</script>
+```
+
+`selectable` 控制是否启用选择,`multiple` 控制单选/多选:
+
+| 用法 | 行为 |
+| --- | --- |
+| 不传 `selectable` | 纯展示树 |
+| `selectable` | 单选(单选按钮) |
+| `selectable multiple` | 多选(复选框,父子联动) |
+
+禁用节点默认锁定当前选中状态。全选、清空、父子联动、实例方法以及外部更新 `v-model` 时,都不会改变它;需要允许变更时传入 `checked-disabled`。
+
+普通 `class` 作用于组件根容器;需要使用自己的类名定制每个节点行时,传入 `node-class`:
+
+```vue
+<uni-tree-view
+  class="department-tree"
+  node-class="department-tree-node"
+  :data="treeData"
+/>
+```
+
+`tree-props` 只负责数据字段映射,不包含样式配置。
+
+完整的属性、事件、插槽和实例方法(Props / Events / Slots / Methods),以及懒加载与虚拟渲染示例,请见 **[文档站](https://ofreshman.github.io/uni-tree-view/)**。
+
+## 平台兼容性
+
+| 平台 | 状态 |
+| --- | --- |
+| H5 | ✅ 构建 + 交互验证 |
+| 微信小程序 | ✅ 构建验证 |
+| 支付宝小程序 | ✅ 构建验证 |
+| App / 其他小程序 | 理论可用,未充分验证 |
+
+点击反馈、内联图标和 `scroll-view` 虚拟滚动等实现说明,见 [平台兼容性文档](https://ofreshman.github.io/uni-tree-view/guide/platforms)。
+
+## 开发
+
+```bash
+pnpm install
+pnpm play        # H5 playground
+pnpm test        # 单元测试
+pnpm build       # 构建组件包
+pnpm docs        # 本地文档站
+```
+
+贡献前请阅读 [CONTRIBUTING.md](https://github.com/OFreshman/uni-tree-view/blob/main/CONTRIBUTING.md)。
+
+## License
+
+本项目使用 [MIT](./LICENSE) 许可证,版权归 OFreshman 所有。再分发源码、构建产物或主要部分时,请保留版权声明和许可证全文;MIT 不要求在产品界面展示作者名。
+
+使用或改造时的保留要求、推荐署名格式和第三方许可说明,见 [许可证与署名说明](https://ofreshman.github.io/uni-tree-view/guide/license)。

+ 271 - 0
uni_modules/KieranYin9527-tree/style/index.scss

@@ -0,0 +1,271 @@
+.utv-tree-item {
+  position: relative;
+  z-index: 0;
+  box-sizing: border-box;
+  display: flex;
+  align-items: center;
+  min-height: 88rpx;
+  padding-right: 24rpx;
+  color: #666666;
+  border-bottom: 2rpx solid #F3F3F3;
+  &:nth-child(1){
+	border-top: 2rpx solid #F3F3F3;  
+  }
+
+  // hover-class 按压反馈,兼容微信/支付宝小程序与 H5
+  &--hover {
+    background-color: rgba(17, 24, 39, 0.04);
+  }
+
+  &.is-disabled {
+    color: #666666;
+  }
+
+  // 状态层始终保留在节点树中,选中时只改 opacity。
+  // 避免小程序在选中/反选时反复创建伪元素并重新合成整行。
+  &__state-layer {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 0;
+    // background-color: var(--theme-color, #007aff);
+    opacity: 0;
+    pointer-events: none;
+    transition: opacity 0.2s ease;
+  }
+
+  &.is-checked &__state-layer {
+    opacity: 0.06;
+  }
+
+  &.is-checked .utv-tree-node-label {
+    color: var(--theme-color, #007aff);
+  }
+
+  &.is-disabled &__state-layer {
+    opacity: 0;
+  }
+
+  &.is-disabled.is-checked .utv-tree-node-label {
+    color: #666666;
+  }
+
+  &__arrow-placeholder {
+    flex: 0 0 40rpx;
+    width: 40rpx;
+    height: 40rpx;
+  }
+
+  &__arrow-icon {
+    position: relative;
+    flex: 0 0 40rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 40rpx;
+    height: 40rpx;
+    &::after {
+      position: relative;
+      z-index: 1;
+      overflow: hidden;
+      /* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
+      font-family: "uni-tree-iconfont" !important;
+      font-size: 32rpx;
+      font-style: normal;
+      color: #999999;
+      transition: transform 0.2s ease;
+      -webkit-font-smoothing: antialiased;
+      -moz-osx-font-smoothing: grayscale;
+    }
+
+    &.is-right::after {
+      content: '\e604';
+      transform: rotate(-90deg);
+    }
+
+    &.is-expand::after {
+      transform: rotate(0deg);
+    }
+
+    &.is-loading {
+      animation: IconLoading 1s linear 0s infinite;
+
+      &::after {
+        content: '\e7f1';
+      }
+    }
+
+    &.is-load-error::after {
+      color: #e5484d;
+      content: "!";
+      font-family: sans-serif !important;
+      font-weight: 600;
+      transform: none;
+    }
+  }
+
+  &__checkbox {
+    flex: 0 0 48rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 48rpx;
+    height: 48rpx;
+    overflow: hidden;
+
+    &.is-disabled {
+      // opacity: 0.45;
+	  .utv-tree-checkbox-checked::after {
+	    color: #C0C4CC;
+	  }
+    }
+
+    &-icon {
+      position: relative;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 40rpx;
+      height: 40rpx;
+
+      &::after {
+        position: relative;
+        top: 0;
+        left: 0;
+        z-index: 1;
+        overflow: hidden;
+        /* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
+        font-family: "uni-tree-iconfont" !important;
+        font-size: 36rpx;
+        font-style: normal;
+        transition: color 0.2s ease;
+        -webkit-font-smoothing: antialiased;
+        -moz-osx-font-smoothing: grayscale;
+      }
+
+      &.utv-tree-checkbox-outline::after {
+        color: #bbb;
+        content: "\ead5";
+      }
+
+      &.utv-tree-checkbox-checked::after {
+        color: var(--theme-color,#007aff);
+        content: "\ead4";
+      }
+
+      &.utv-tree-checkbox-indeterminate::after {
+        color: var(--theme-color,#007aff);
+        content: "\ebce";
+      }
+
+      &.utv-tree-radio-outline::after {
+        color: #bbb;
+        content: "\ecc5";
+      }
+
+      &.utv-tree-radio-checked::after {
+        color: var(--theme-color,#007aff);
+        content: "\ecc4";
+      }
+
+      &.utv-tree-radio-indeterminate::after {
+        color: var(--theme-color,#007aff);
+        content: "\ea4f";
+      }
+    }
+  }
+
+  &.is-virtual &__checkbox {
+    height: 100%;
+  }
+}
+
+.utv-tree-node-label {
+  flex: 1;
+  min-width: 0;
+  padding: 0 4rpx;
+  overflow: hidden;
+  font-size: 28rpx;
+  line-height: 40rpx;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  &.parent{
+	font-weight: 500;
+	color: #333333;  
+  }
+}
+
+.utv-tree-node-label__match {
+  color: var(--theme-color, #007aff);
+  font-weight: 600;
+}
+
+.utv-tree-item.is-disabled .utv-tree-node-label__match {
+  color: inherit;
+}
+
+.utv-tree-node-content {
+  display: flex;
+  flex: 1;
+  align-items: center;
+  min-width: 0;
+}
+
+.utv-tree-node-main {
+  flex: 1;
+  min-width: 0;
+}
+
+.utv-tree-node-icon {
+  flex: 0 0 auto;
+  padding-left: 16rpx;
+  font-size: 28rpx;
+  line-height: 40rpx;
+}
+
+.utv-tree-node-icon + .utv-tree-node-main .utv-tree-node-label {
+  padding-left: 8rpx;
+}
+
+.utv-tree-node-append {
+  flex: 0 0 auto;
+  max-width: 40%;
+  padding-left: 16rpx;
+  overflow: hidden;
+  color: #667085;
+  font-size: 24rpx;
+  line-height: 36rpx;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.utv-tree-node-path {
+  padding: 0 16rpx;
+  overflow: hidden;
+  color: #98a2b3;
+  font-size: 22rpx;
+  line-height: 32rpx;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.utv-tree-empty {
+  padding: 64rpx 24rpx;
+  color: #98a2b3;
+  font-size: 28rpx;
+  line-height: 40rpx;
+  text-align: center;
+}
+
+// 加载图标旋转动画;此前被 .is-loading 引用但未定义
+@keyframes IconLoading {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}

+ 11 - 0
uni_modules/KieranYin9527-tree/types.d.ts

@@ -0,0 +1,11 @@
+import type { MaybeRefOrGetter, StyleValue } from "vue";
+
+export type OptionalValue<T> = T | undefined;
+export type NullableValue<T> = T | null;
+
+export interface AllowedComponentProps {
+  class?: any;
+  style?: StyleValue;
+}
+
+export type Injection<T> = MaybeRefOrGetter<NullableValue<T>>;

+ 1 - 0
uni_modules/KieranYin9527-tree/utils/device.d.ts

@@ -0,0 +1 @@
+export declare function getIsPc(): boolean;

+ 16 - 0
uni_modules/KieranYin9527-tree/utils/device.js

@@ -0,0 +1,16 @@
+import { isMpAlipay, isMpToutiao, isMpWeixin } from "./env";
+import { getDeviceInfo } from "./uni";
+
+export function getIsPc() {
+  // #ifdef WEB
+  if (!("ontouchstart" in window)) {
+    return true;
+  }
+  // #endif
+
+  if (isMpWeixin || isMpToutiao || isMpAlipay) {
+    return /windows/i.test(getDeviceInfo().platform);
+  }
+
+  return false;
+}

+ 72 - 0
uni_modules/KieranYin9527-tree/utils/env.d.ts

@@ -0,0 +1,72 @@
+interface IPlatform {
+  APP: "APP";
+  APP_ANDROID: "APP-ANDROID";
+  APP_IOS: "APP-IOS";
+  APP_HARMONY: "APP-HARMONY";
+  WEB: "WEB";
+  MP: "MP";
+  MP_WEIXIN: "MP-WEIXIN";
+  MP_ALIPAY: "MP-ALIPAY";
+  MP_BAIDU: "MP-BAIDU";
+  MP_TOUTIAO: "MP-TOUTIAO";
+  MP_LARK: "MP-LARK";
+  MP_QQ: "MP-QQ";
+  MP_KUAISHOU: "MP-KUAISHOU";
+  MP_JD: "MP-JD";
+  MP_360: "MP-360";
+  MP_XHS: "MP-XHS";
+  MP_HARMONY: "MP-HARMONY";
+  QUICKAPP_WEBVIEW: "QUICKAPP-WEBVIEW";
+  QUICKAPP_WEBVIEW_UNION: "QUICKAPP-WEBVIEW-UNION";
+  QUICKAPP_WEBVIEW_HUAWEI: "QUICKAPP-WEBVIEW-HUAWEI";
+  OTHER: "OTHER";
+}
+
+export declare const Platform: IPlatform;
+
+export declare function getPlatform(): IPlatform[keyof IPlatform];
+
+export declare const platform: ReturnType<typeof getPlatform>;
+
+/** App */
+export declare const isApp: boolean;
+/** App Android */
+export declare const isAppAndroid: boolean;
+/** App iOS */
+export declare const isAppIos: boolean;
+/** App HarmonyOS Next */
+export declare const isAppHarmony: boolean;
+/** Web */
+export declare const isWeb: boolean;
+/** 小程序 */
+export declare const isMp: boolean;
+/** 微信小程序 */
+export declare const isMpWeixin: boolean;
+/** 支付宝小程序 */
+export declare const isMpAlipay: boolean;
+/** 百度小程序 */
+export declare const isMpBaidu: boolean;
+/** 头条小程序 */
+export declare const isMpToutiao: boolean;
+/** 飞书小程序 */
+export declare const isMpLark: boolean;
+/** QQ小程序 */
+export declare const isMpQq: boolean;
+/** 快手小程序 */
+export declare const isMpKuaishou: boolean;
+/** 京东小程序 */
+export declare const isMpJd: boolean;
+/** 360小程序 */
+export declare const isMp360: boolean;
+/** 小红书小程序 */
+export declare const isMpXhs: boolean;
+/** 鸿蒙元服务 */
+export declare const isMpHarmony: boolean;
+/** 快应用 */
+export declare const isQuickappWebview: boolean;
+/** 快应用联盟 */
+export declare const isQuickappWebviewUnion: boolean;
+/** 快应用华为 */
+export declare const isQuickappWebviewHuawei: boolean;
+/** 其他平台 */
+export declare const isOtherPlatform: boolean;

+ 137 - 0
uni_modules/KieranYin9527-tree/utils/env.js

@@ -0,0 +1,137 @@
+// noinspection JSUnusedAssignment
+
+export const Platform = {
+  APP: "APP",
+  APP_ANDROID: "APP-ANDROID",
+  APP_IOS: "APP-IOS",
+  APP_HARMONY: "APP-HARMONY",
+  WEB: "WEB",
+  MP: "MP",
+  MP_WEIXIN: "MP-WEIXIN",
+  MP_ALIPAY: "MP-ALIPAY",
+  MP_BAIDU: "MP-BAIDU",
+  MP_TOUTIAO: "MP-TOUTIAO",
+  MP_LARK: "MP-LARK",
+  MP_QQ: "MP-QQ",
+  MP_KUAISHOU: "MP-KUAISHOU",
+  MP_JD: "MP-JD",
+  MP_360: "MP-360",
+  MP_XHS: "MP-XHS",
+  MP_HARMONY: "MP-HARMONY",
+  QUICKAPP_WEBVIEW: "QUICKAPP-WEBVIEW",
+  QUICKAPP_WEBVIEW_UNION: "QUICKAPP-WEBVIEW-UNION",
+  QUICKAPP_WEBVIEW_HUAWEI: "QUICKAPP-WEBVIEW-HUAWEI",
+  OTHER: "OTHER"
+};
+
+export function getPlatform() {
+  let platform = Platform.OTHER;
+
+  // #ifdef APP
+  platform = Platform.APP;
+  // #endif
+  // #ifdef APP-ANDROID
+  platform = Platform.APP_ANDROID;
+  // #endif
+  // #ifdef APP-IOS
+  platform = Platform.APP_IOS;
+  // #endif
+  // #ifdef APP-HARMONY
+  platform = Platform.APP_HARMONY;
+  // #endif
+  // #ifdef WEB
+  platform = Platform.WEB;
+  // #endif
+  // #ifdef MP
+  platform = Platform.MP;
+  // #endif
+  // #ifdef MP-WEIXIN
+  platform = Platform.MP_WEIXIN;
+  // #endif
+  // #ifdef MP-ALIPAY
+  platform = Platform.MP_ALIPAY;
+  // #endif
+  // #ifdef MP-BAIDU
+  platform = Platform.MP_BAIDU;
+  // #endif
+  // #ifdef MP-TOUTIAO
+  platform = Platform.MP_TOUTIAO;
+  // #endif
+  // #ifdef MP-LARK
+  platform = Platform.MP_LARK;
+  // #endif
+  // #ifdef MP-QQ
+  platform = Platform.MP_QQ;
+  // #endif
+  // #ifdef MP-KUAISHOU
+  platform = Platform.MP_KUAISHOU;
+  // #endif
+  // #ifdef MP-JD
+  platform = Platform.MP_JD;
+  // #endif
+  // #ifdef MP-360
+  platform = Platform.MP_360;
+  // #endif
+  // #ifdef MP-XHS
+  platform = Platform.MP_XHS;
+  // #endif
+  // #ifdef MP-HARMONY
+  platform = Platform.MP_HARMONY;
+  // #endif
+  // #ifdef QUICKAPP-WEBVIEW
+  platform = Platform.QUICKAPP_WEBVIEW;
+  // #endif
+  // #ifdef QUICKAPP-WEBVIEW-UNION
+  platform = Platform.QUICKAPP_WEBVIEW_UNION;
+  // #endif
+  // #ifdef QUICKAPP-WEBVIEW-HUAWEI
+  platform = Platform.QUICKAPP_WEBVIEW_HUAWEI;
+  // #endif
+
+  return platform;
+}
+
+export const platform = getPlatform();
+
+/** App */
+export const isApp = platform === Platform.APP;
+/** App Android */
+export const isAppAndroid = platform === Platform.APP_ANDROID;
+/** App iOS */
+export const isAppIos = platform === Platform.APP_IOS;
+/** App HarmonyOS Next */
+export const isAppHarmony = platform === Platform.APP_HARMONY;
+/** Web */
+export const isWeb = platform === Platform.WEB;
+/** 小程序 */
+export const isMp = platform === Platform.MP;
+/** 微信小程序 */
+export const isMpWeixin = platform === Platform.MP_WEIXIN;
+/** 支付宝小程序 */
+export const isMpAlipay = platform === Platform.MP_ALIPAY;
+/** 百度小程序 */
+export const isMpBaidu = platform === Platform.MP_BAIDU;
+/** 头条小程序 */
+export const isMpToutiao = platform === Platform.MP_TOUTIAO;
+/** 飞书小程序 */
+export const isMpLark = platform === Platform.MP_LARK;
+/** QQ小程序 */
+export const isMpQq = platform === Platform.MP_QQ;
+/** 快手小程序 */
+export const isMpKuaishou = platform === Platform.MP_KUAISHOU;
+/** 京东小程序 */
+export const isMpJd = platform === Platform.MP_JD;
+/** 360小程序 */
+export const isMp360 = platform === Platform.MP_360;
+/** 小红书小程序 */
+export const isMpXhs = platform === Platform.MP_XHS;
+/** 鸿蒙元服务 */
+export const isMpHarmony = platform === Platform.MP_HARMONY;
+/** 快应用 */
+export const isQuickappWebview = platform === Platform.QUICKAPP_WEBVIEW;
+/** 快应用联盟 */
+export const isQuickappWebviewUnion = platform === Platform.QUICKAPP_WEBVIEW_UNION;
+/** 快应用华为 */
+export const isQuickappWebviewHuawei = platform === Platform.QUICKAPP_WEBVIEW_HUAWEI;
+/** 其他平台 */
+export const isOtherPlatform = platform === Platform.OTHER;

+ 9 - 0
uni_modules/KieranYin9527-tree/utils/helpers.d.ts

@@ -0,0 +1,9 @@
+export declare function isEmpty(value: unknown): boolean;
+
+export declare function defaultTo(value: any, ...defaultValues: any[]): any;
+
+export declare function sleep(timeout: number): Promise<void>;
+
+export declare function upperFirst(value: string): string;
+
+export declare function lowerFirst(value: string): string;

+ 42 - 0
uni_modules/KieranYin9527-tree/utils/helpers.js

@@ -0,0 +1,42 @@
+export function isEmpty(value) {
+  if (value == null) {
+    return true;
+  }
+
+  if (typeof value === "string" || Array.isArray(value)) {
+    return value.length === 0;
+  }
+
+  if (typeof value === "object") {
+    return Object.keys(value).length === 0;
+  }
+
+  return false;
+}
+
+export function defaultTo(value, ...defaultValues) {
+  if (defaultValues.length === 0) {
+    return value;
+  }
+
+  // eslint-disable-next-line no-self-compare
+  if (value == null || value !== value) {
+    return defaultTo(defaultValues[0], ...defaultValues.slice(1));
+  }
+
+  return value;
+}
+
+export function sleep(timeout) {
+  return new Promise((resolve) => {
+    setTimeout(resolve, timeout);
+  });
+}
+
+export function upperFirst(value) {
+  return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
+}
+
+export function lowerFirst(value) {
+  return `${value.charAt(0).toLowerCase()}${value.slice(1)}`;
+}

+ 41 - 0
uni_modules/KieranYin9527-tree/utils/mitt.d.ts

@@ -0,0 +1,41 @@
+export type EventType = string | symbol;
+
+export type Handler<T = unknown> = (event: T) => void;
+
+export type WildcardHandler<T = Record<string, unknown>> = (
+  type: keyof T,
+  event: T[keyof T]
+) => void;
+
+export type EventHandlerList<T = unknown> = Array<Handler<T>>;
+
+export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<
+  WildcardHandler<T>
+>;
+
+export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
+  keyof Events | "*",
+  EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
+>;
+
+export interface Emitter<Events extends Record<EventType, unknown>> {
+  events: EventHandlerMap<Events>;
+
+  on: (<Key extends keyof Events>(
+    type: Key,
+    handler: Handler<Events[Key]>
+  ) => void) & ((type: "*", handler: WildcardHandler<Events>) => void);
+
+  off: (<Key extends keyof Events>(
+    type: Key,
+    handler?: Handler<Events[Key]>
+  ) => void) & ((type: "*", handler?: WildcardHandler<Events>) => void);
+
+  emit: (<Key extends keyof Events>(type: Key, event: Events[Key]) => void) & (<Key extends keyof Events>(
+    type: undefined extends Events[Key] ? Key : never
+  ) => void);
+}
+
+export declare function mitt<Events extends Record<EventType, unknown>>(
+  events?: EventHandlerMap<Events>
+): Emitter<Events>;

+ 49 - 0
uni_modules/KieranYin9527-tree/utils/mitt.js

@@ -0,0 +1,49 @@
+import { defaultTo } from "./helpers";
+
+export function mitt(events) {
+  const _events = defaultTo(events, new Map());
+
+  return {
+    events: _events,
+
+    on(topic, handler) {
+      const handlers = _events.get(topic);
+
+      if (handlers) {
+        handlers.push(handler);
+      } else {
+        _events.set(topic, [handler]);
+      }
+    },
+
+    off(topic, handler) {
+      const handlers = _events.get(topic);
+
+      if (handlers) {
+        if (handler) {
+          handlers.splice(handlers.indexOf(handler) >>> 0, 1);
+        } else {
+          _events.set(topic, []);
+        }
+      }
+    },
+
+    emit(topic, event) {
+      const handlers = _events.get(topic);
+
+      if (handlers) {
+        for (const handler of handlers.slice()) {
+          handler(event);
+        }
+      }
+
+      const hdlrs = _events.get("*");
+
+      if (hdlrs) {
+        for (const handler of hdlrs.slice()) {
+          handler(topic, event);
+        }
+      }
+    }
+  };
+}

+ 17 - 0
uni_modules/KieranYin9527-tree/utils/uni.d.ts

@@ -0,0 +1,17 @@
+import type { ComponentPublicInstance } from "vue";
+
+export declare function getDeviceInfo(): UniApp.GetDeviceInfoResult | UniApp.GetSystemInfoResult;
+
+export declare function getWindowInfo(): UniApp.GetWindowInfoResult | UniApp.GetSystemInfoResult;
+
+export declare function getAppBaseInfo(): UniApp.GetAppBaseInfoResult | UniApp.GetSystemInfoResult;
+
+export declare function getVersion(): string;
+
+export declare function compareVersion(v1: string, v2: string): 0 | 1 | -1;
+
+export declare function querySelect(
+  component: ComponentPublicInstance,
+  selector: string,
+  fields: UniApp.NodeField
+): Promise<UniApp.NodeInfo>;

+ 74 - 0
uni_modules/KieranYin9527-tree/utils/uni.js

@@ -0,0 +1,74 @@
+/* eslint-disable no-undef */
+
+import { isMpAlipay } from "./env";
+import { defaultTo } from "./helpers";
+
+export function getDeviceInfo() {
+  if (uni.canIUse("getDeviceInfo") || uni.getDeviceInfo) {
+    return uni.getDeviceInfo();
+  } else {
+    return uni.getSystemInfoSync();
+  }
+}
+
+export function getWindowInfo() {
+  if (uni.canIUse("getWindowInfo") || uni.getWindowInfo) {
+    return uni.getWindowInfo();
+  } else {
+    return uni.getSystemInfoSync();
+  }
+}
+
+export function getAppBaseInfo() {
+  if (uni.canIUse("getAppBaseInfo") || uni.getAppBaseInfo) {
+    return uni.getAppBaseInfo();
+  } else {
+    return uni.getSystemInfoSync();
+  }
+}
+
+export function getVersion() {
+  if (isMpAlipay) {
+    return my.SDKVersion;
+  }
+
+  return getAppBaseInfo().SDKVersion;
+}
+
+export function compareVersion(v1, v2) {
+  const s1 = v1.split(".");
+  const s2 = v2.split(".");
+
+  for (let i = 0; i < Math.max(s1.length, s2.length); i += 1) {
+    const num1 = Number.parseInt(defaultTo(s1[i], "0"));
+    const num2 = Number.parseInt(defaultTo(s2[i], "0"));
+
+    if (num1 > num2) {
+      return 1;
+    } else if (num1 < num2) {
+      return -1;
+    }
+  }
+
+  return 0;
+}
+
+// function gte(version) {
+//   return compareVersion(getVersion(), version) >= 0;
+// }
+
+export function querySelect(component, selector, fields) {
+  return new Promise((resolve, reject) => {
+    uni.createSelectorQuery()
+      .in(component)
+      .select(selector)
+      .fields(fields, () => {})
+      .exec(([node]) => {
+        if (node) {
+          resolve(node);
+        } else {
+          reject();
+        }
+      });
+  });
+}

Some files were not shown because too many files changed in this diff