Преглед изворни кода

feat(employeeProfile): 实现部门树状员工选择组件

1.  新增EmployeeTreeNode树形节点组件用于渲染员工/部门树
2.  重构员工列表为部门树结构,支持展开收起部门节点
3.  添加搜索时切换树结构和普通列表的逻辑
4.  新增自动展开所有部门、默认选中第一个员工的功能
5.  优化搜索逻辑,改为通过计算属性实时过滤结果
huoyi пре 1 недеља
родитељ
комит
1f116a933b
2 измењених фајлова са 230 додато и 45 уклоњено
  1. 125 0
      src/pages/components/EmployeeTreeNode.vue
  2. 105 45
      src/pages/employeeProfile/index.vue

+ 125 - 0
src/pages/components/EmployeeTreeNode.vue

@@ -0,0 +1,125 @@
1
+<template>
2
+    <view class="tree-node">
3
+        <view 
4
+            :class="['node-row', { 'is-dept': isDept, 'is-user': isUser }]"
5
+            @click="handleClick"
6
+        >
7
+            <view class="toggle-icon" v-if="isDept">
8
+                <u-icon :name="expanded ? 'arrow-down' : 'arrow-right'" size="16" color="#999" />
9
+            </view>
10
+            <view class="toggle-icon" v-else>
11
+                <text class="user-dot">●</text>
12
+            </view>
13
+            <text class="node-label">{{ nodeLabel }}</text>
14
+            <u-icon v-if="isUser && nodeId === selectedId" name="checkmark" color="#34D399" size="18" />
15
+        </view>
16
+        <view v-if="isDept && expanded" class="node-children">
17
+            <template v-for="child in nodeChildren">
18
+                <employee-tree-node
19
+                    :key="child.id"
20
+                    :node="child"
21
+                    :expanded-ids="expandedIds"
22
+                    :selected-id="selectedId"
23
+                    @toggle="$emit('toggle', $event)"
24
+                    @select="$emit('select', $event)"
25
+                />
26
+            </template>
27
+        </view>
28
+    </view>
29
+</template>
30
+
31
+<script>
32
+export default {
33
+    name: 'EmployeeTreeNode',
34
+    props: {
35
+        node: {
36
+            type: Object,
37
+            required: true
38
+        },
39
+        expandedIds: {
40
+            type: [Array, Set],
41
+            default: () => []
42
+        },
43
+        selectedId: {
44
+            type: [String, Number],
45
+            default: null
46
+        }
47
+    },
48
+    computed: {
49
+        isDept() {
50
+            const hasChildren = this.node.children && this.node.children.length > 0
51
+            return hasChildren || this.node.nodeType === 'dept'
52
+        },
53
+        isUser() {
54
+            return !this.isDept
55
+        },
56
+        nodeId() {
57
+            return this.node.userId || this.node.id
58
+        },
59
+        nodeLabel() {
60
+            return this.node.nickName || this.node.label || this.node.userName || this.node.name || ''
61
+        },
62
+        nodeChildren() {
63
+            return this.node.children || []
64
+        },
65
+        expanded() {
66
+            if (this.expandedIds instanceof Set) {
67
+                return this.expandedIds.has(this.node.id)
68
+            }
69
+            return this.expandedIds.includes(this.node.id)
70
+        }
71
+    },
72
+    methods: {
73
+        handleClick() {
74
+            if (this.isDept) {
75
+                this.$emit('toggle', this.node.id)
76
+            } else {
77
+                this.$emit('select', {
78
+                    userId: this.nodeId,
79
+                    nickName: this.nodeLabel
80
+                })
81
+            }
82
+        }
83
+    }
84
+}
85
+</script>
86
+
87
+<style lang="scss" scoped>
88
+.tree-node {
89
+    width: 100%;
90
+}
91
+
92
+.node-row {
93
+    display: flex;
94
+    align-items: center;
95
+    padding: 20rpx 32rpx 20rpx 16rpx;
96
+    gap: 12rpx;
97
+
98
+    &.is-dept {
99
+        padding-left: 16rpx;
100
+    }
101
+}
102
+
103
+.toggle-icon {
104
+    width: 40rpx;
105
+    height: 40rpx;
106
+    display: flex;
107
+    align-items: center;
108
+    justify-content: center;
109
+}
110
+
111
+.user-dot {
112
+    font-size: 16rpx;
113
+    color: #999;
114
+}
115
+
116
+.node-label {
117
+    flex: 1;
118
+    font-size: 28rpx;
119
+    color: #333;
120
+}
121
+
122
+.node-children {
123
+    padding-left: 32rpx;
124
+}
125
+</style>

+ 105 - 45
src/pages/employeeProfile/index.vue

@@ -239,9 +239,21 @@
239 239
                 </view>
240 240
                 <view class="search-box">
241 241
                     <u-input v-model="employeeSearchKeyword" placeholder="搜索员工" @confirm="onEmployeeSearch"
242
-                        @input="onEmployeeSearch" />
242
+                        @input="onEmployeeSearch"></u-input>
243 243
                 </view>
244
-                <scroll-view scroll-y class="employee-list">
244
+                <scroll-view v-if="!employeeSearchKeyword.trim()" scroll-y class="tree-list">
245
+                    <template v-for="(node, index) in deptTreeData">
246
+                        <employee-tree-node
247
+                            :key="node.id"
248
+                            :node="node"
249
+                            :expanded-ids="expandedDeptIds"
250
+                            :selected-id="selectedEmployeeId"
251
+                            @toggle="toggleDeptExpand"
252
+                            @select="onEmployeeSelect"
253
+                        />
254
+                    </template>
255
+                </scroll-view>
256
+                <scroll-view v-else scroll-y class="employee-list">
245 257
                     <view class="employee-item" v-for="item in filteredEmployeeList" :key="item.userId"
246 258
                         @click="onEmployeeSelect(item)">
247 259
                         <text class="employee-item-name">{{ item.nickName }}</text>
@@ -265,6 +277,7 @@ import * as echarts from 'echarts'
265 277
 import { getEmployeePortrait, countTagScore } from '@/api/portraitManagement/portraitManagement'
266 278
 import { listAllUser, getDeptUserTree } from '@/api/system/user'
267 279
 import SectionTitle from '@/components/SectionTitle.vue'
280
+import EmployeeTreeNode from '@/pages/components/EmployeeTreeNode.vue'
268 281
 
269 282
 const honorColors = ['#60A5FA', '#34D399', '#FBBF24', '#EF4444', '#A78BFA', '#F472B6', '#6EE7B7', '#FB923C']
270 283
 
@@ -286,7 +299,8 @@ function getRandomHexColor() {
286 299
 export default {
287 300
     name: 'EmployeeProfile',
288 301
     components: {
289
-        SectionTitle
302
+        SectionTitle,
303
+        EmployeeTreeNode
290 304
     },
291 305
     data() {
292 306
         return {
@@ -308,10 +322,12 @@ export default {
308 322
             showEmployeePicker: false,
309 323
             selectedEmployeeId: null,
310 324
             selectedEmployeeName: '',
325
+            deptTreeData: [],
326
+            pickerTreeData: [],
311 327
             employeeList: [],
312
-            filteredEmployeeList: [],
313 328
             employeeSearchKeyword: '',
314 329
             employeeLoading: false,
330
+            expandedDeptIds: [],
315 331
             isSecurityCheck: false,
316 332
             userInfo: null
317 333
         }
@@ -353,6 +369,13 @@ export default {
353 369
             const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
354 370
             const weekDay = weekDays[now.getDay()]
355 371
             return `${year}年${month}月${day}日 ${weekDay}`
372
+        },
373
+        filteredEmployeeList() {
374
+            const keyword = this.employeeSearchKeyword.trim().toLowerCase()
375
+            if (!keyword) return this.employeeList
376
+            return this.employeeList.filter(item =>
377
+                (item.nickName || '').toLowerCase().includes(keyword)
378
+            )
356 379
         }
357 380
     },
358 381
     mounted() {
@@ -382,6 +405,58 @@ export default {
382 405
             this.currentTime = `${hours}:${minutes}:${seconds}`
383 406
         },
384 407
         // 员工相关方法
408
+        // 扁平化部门树,提取人员
409
+        flattenDeptTree(tree) {
410
+            const result = []
411
+            const traverse = (nodes) => {
412
+                nodes.forEach(node => {
413
+                    const hasChildren = node.children && node.children.length > 0
414
+                    if (!hasChildren && node.nodeType !== 'dept') {
415
+                        result.push({
416
+                            nodeType: node.nodeType || '',
417
+                            userId: node.userId || node.id,
418
+                            nickName: node.nickName || node.label || node.userName || ''
419
+                        })
420
+                    }
421
+                    if (hasChildren) {
422
+                        traverse(node.children)
423
+                    }
424
+                })
425
+            }
426
+            traverse(tree)
427
+            return result
428
+        },
429
+
430
+        // 从树中查找第一个用户节点
431
+        findFirstUser(nodes) {
432
+            for (const node of nodes) {
433
+                const hasChildren = node.children && node.children.length > 0
434
+                if (!hasChildren && node.nodeType !== 'dept') {
435
+                    return node
436
+                }
437
+                if (hasChildren) {
438
+                    const found = this.findFirstUser(node.children)
439
+                    if (found) return found
440
+                }
441
+            }
442
+            return null
443
+        },
444
+        // 展开所有部门节点
445
+        expandAllDepts(nodes) {
446
+            this.expandedDeptIds = []
447
+            const traverse = (list) => {
448
+                list.forEach(node => {
449
+                    const hasChildren = node.children && node.children.length > 0
450
+                    if (hasChildren || node.nodeType === 'dept') {
451
+                        this.expandedDeptIds.push(node.id)
452
+                        if (hasChildren) {
453
+                            traverse(node.children)
454
+                        }
455
+                    }
456
+                })
457
+            }
458
+            traverse(nodes)
459
+        },
385 460
         fetchEmployeeList() {
386 461
             this.employeeLoading = true
387 462
             const user = this.$store.state.user?.userInfo || this.$store.state.user?.user
@@ -396,11 +471,8 @@ export default {
396 471
                 this.selectedEmployeeId = user.userId || user.id
397 472
                 this.selectedEmployeeName = user.nickName || user.userName || ''
398 473
                 this.searchKeyword = this.selectedEmployeeName
399
-                this.employeeList = [{
400
-                    userId: user.userId || user.id,
401
-                    nickName: user.nickName || user.userName || ''
402
-                }]
403
-                this.filteredEmployeeList = [...this.employeeList]
474
+                this.deptTreeData = []
475
+                this.pickerTreeData = []
404 476
                 this.employeeLoading = false
405 477
                 // 自动加载画像数据
406 478
                 this.fetchEmployeePortrait()
@@ -409,14 +481,16 @@ export default {
409 481
                 if (user && user.deptId) {
410 482
                     getDeptUserTree({ deptId: user.deptId }).then(res => {
411 483
                         if (res.code === 200) {
412
-                            let allUsers = this.flattenDeptTree(res.data || [])
413
-                            this.employeeList = allUsers.filter(user => user.nodeType == 'user')
414
-
415
-                            this.filteredEmployeeList = [...this.employeeList]
416
-                            // 默认选中第一个
417
-                            if (this.employeeList.length > 0) {
418
-                                this.selectedEmployeeId = this.employeeList[0].userId
419
-                                this.selectedEmployeeName = this.employeeList[0].nickName
484
+                            this.deptTreeData = res.data || []
485
+                            const allUsers = this.flattenDeptTree(this.deptTreeData)
486
+                            this.employeeList = allUsers.filter(user => user.nodeType === 'user')
487
+                            // 默认展开所有部门
488
+                            this.expandAllDepts(this.deptTreeData)
489
+                            // 默认选中第一个用户
490
+                            const firstUser = this.findFirstUser(this.deptTreeData)
491
+                            if (firstUser) {
492
+                                this.selectedEmployeeId = firstUser.userId || firstUser.id
493
+                                this.selectedEmployeeName = firstUser.nickName || firstUser.label || firstUser.userName || ''
420 494
                                 this.searchKeyword = this.selectedEmployeeName
421 495
                                 this.fetchEmployeePortrait()
422 496
                             }
@@ -430,36 +504,15 @@ export default {
430 504
                 }
431 505
             }
432 506
         },
433
-        // 扁平化部门树,提取人员
434
-        flattenDeptTree(tree) {
435
-            const result = []
436
-            const traverse = (nodes) => {
437
-                nodes.forEach(node => {
438
-                    // 判断是否是人员节点,这里假设人员有 userId 或者特定字段
439
-                    // 根据实际接口返回调整判断条件
440
-                    if (node.userId || (node.id && !node.children)) {
441
-                        result.push({
442
-                            nodeType: node.nodeType || '',
443
-                            userId: node.userId || node.id,
444
-                            nickName: node.nickName || node.label || node.userName || ''
445
-                        })
446
-                    }
447
-                    if (node.children && node.children.length > 0) {
448
-                        traverse(node.children)
449
-                    }
450
-                })
451
-            }
452
-            traverse(tree)
453
-            return result
454
-        },
455 507
         onEmployeeSearch() {
456
-            const keyword = this.employeeSearchKeyword.trim()
457
-            if (!keyword) {
458
-                this.filteredEmployeeList = [...this.employeeList]
508
+            // 搜索由 computed 自动处理
509
+        },
510
+        toggleDeptExpand(id) {
511
+            const index = this.expandedDeptIds.indexOf(id)
512
+            if (index > -1) {
513
+                this.expandedDeptIds.splice(index, 1)
459 514
             } else {
460
-                this.filteredEmployeeList = this.employeeList.filter(item =>
461
-                    (item.nickName || '').includes(keyword)
462
-                )
515
+                this.expandedDeptIds.push(id)
463 516
             }
464 517
         },
465 518
         onEmployeeSelect(item) {
@@ -853,6 +906,13 @@ export default {
853 906
     flex-shrink: 0;
854 907
 }
855 908
 
909
+.tree-list {
910
+    flex: 1;
911
+    padding: 0;
912
+    height: 0;
913
+    overflow: hidden;
914
+}
915
+
856 916
 .employee-list {
857 917
     flex: 1;
858 918
     padding: 0 32rpx;