Pārlūkot izejas kodu

feat: 新增部门选择组件,优化多页面UI与交互

1. 重构SectionTitle组件,适配小程序view标签并优化样式
2. 新增部门选择弹窗组件DeptSelector
3. 优化部门概况页面,添加部门选择功能与适配小程序标签
4. 优化员工画像页面,替换原生搜索为弹窗选择组件,统一UI风格
5. 修复ProfileRadar组件的数据兼容与空状态处理
huoyi 2 nedēļas atpakaļ
vecāks
revīzija
5c9dcd46cd

+ 37 - 25
src/components/SectionTitle.vue

@@ -1,15 +1,15 @@
1 1
 <template>
2
-  <div class="section-wrapper">
3
-    <div class="section-header">
4
-      <div class="section-title">{{ title }}</div>
5
-      <div class="section-header-right">
2
+  <view class="section-wrapper">
3
+    <view class="section-header">
4
+      <view class="section-title">{{ title }}</view>
5
+      <view class="section-header-right">
6 6
         <slot name="header-right"></slot>
7
-      </div>
8
-    </div>
9
-    <div class="section-body">
7
+      </view>
8
+    </view>
9
+    <view class="section-body">
10 10
       <slot></slot>
11
-    </div>
12
-  </div>
11
+    </view>
12
+  </view>
13 13
 </template>
14 14
 
15 15
 <script>
@@ -26,30 +26,42 @@ export default {
26 26
 
27 27
 <style lang="scss" scoped>
28 28
 .section-wrapper {
29
-  background: rgba(45, 42, 85, 0.9);
30
-  border-radius: 24rpx;
31
-  padding: 32rpx;
32
-  margin-bottom: 32rpx;
33
-  box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.2);
29
+    background: rgba(45, 42, 85, 0.6);
30
+    border-radius: 20rpx;
31
+    padding: 32rpx;
32
+    margin-bottom: 24rpx;
33
+    border: 1rpx solid rgba(167, 139, 250, 0.2);
34 34
 }
35 35
 
36 36
 .section-header {
37
-  display: flex;
38
-  justify-content: space-between;
39
-  align-items: center;
40
-  margin-bottom: 24rpx;
37
+    display: flex;
38
+    justify-content: space-between;
39
+    align-items: center;
40
+    margin-bottom: 24rpx;
41 41
 }
42 42
 
43 43
 .section-title {
44
-  font-size: 28rpx;
45
-  font-weight: 500;
46
-  color: #A78BFA;
47
-}
48
-
44
+    font-size: 30rpx;
45
+    font-weight: 600;
46
+    color: #A78BFA;
47
+    position: relative;
48
+    padding-left: 16rpx;
49 49
 
50
+    &::before {
51
+        content: '';
52
+        position: absolute;
53
+        left: 0;
54
+        top: 50%;
55
+        transform: translateY(-50%);
56
+        width: 4rpx;
57
+        height: 28rpx;
58
+        background: #A78BFA;
59
+        border-radius: 2rpx;
60
+    }
61
+}
50 62
 
51 63
 .section-header-right {
52
-  font-size: 24rpx;
53
-  color: rgba(255, 255, 255, 0.5);
64
+    font-size: 24rpx;
65
+    color: rgba(255, 255, 255, 0.5);
54 66
 }
55 67
 </style>

+ 150 - 0
src/pages/components/DeptSelector.vue

@@ -0,0 +1,150 @@
1
+<template>
2
+    <u-popup :show="show" mode="bottom" :round="16" :mask-close-able="true" :safe-area-inset-bottom="true"
3
+        @close="onClose">
4
+        <view class="dept-picker">
5
+            <view class="picker-header">
6
+                <text class="picker-title">选择部门</text>
7
+            </view>
8
+            <view class="search-box">
9
+                <u-input v-model="deptSearchKeyword" placeholder="搜索部门" @confirm="onDeptSearch"
10
+                    @input="onDeptSearch" />
11
+            </view>
12
+            <scroll-view scroll-y class="dept-list">
13
+                <view class="dept-item" v-for="item in filteredDeptList" :key="item.deptId"
14
+                    @click="onDeptSelect(item)">
15
+                    <text class="dept-item-name">{{ item.deptName }}</text>
16
+                    <u-icon v-if="item.deptId === selectedDeptId" name="checkmark" color="#34D399"
17
+                        size="18"></u-icon>
18
+                </view>
19
+                <view v-if="filteredDeptList.length === 0 && !loading" class="empty-state">
20
+                    <text>暂无数据</text>
21
+                </view>
22
+                <view v-if="loading" class="loading-state">
23
+                    <u-loading-icon text="加载中" textSize="14"></u-loading-icon>
24
+                </view>
25
+            </scroll-view>
26
+        </view>
27
+    </u-popup>
28
+</template>
29
+
30
+<script>
31
+export default {
32
+    name: 'DeptSelector',
33
+    props: {
34
+        show: {
35
+            type: Boolean,
36
+            default: false
37
+        },
38
+        value: {
39
+            type: [String, Number],
40
+            default: null
41
+        },
42
+        options: {
43
+            type: Array,
44
+            default: () => []
45
+        },
46
+        loading: {
47
+            type: Boolean,
48
+            default: false
49
+        }
50
+    },
51
+    data() {
52
+        return {
53
+            selectedDeptId: this.value,
54
+            filteredDeptList: [...this.options],
55
+            deptSearchKeyword: ''
56
+        }
57
+    },
58
+    watch: {
59
+        value(newVal) {
60
+            this.selectedDeptId = newVal
61
+        },
62
+        options(newVal) {
63
+            this.filteredDeptList = [...newVal]
64
+        },
65
+        show(newVal) {
66
+            if (newVal) {
67
+                this.deptSearchKeyword = ''
68
+                this.filteredDeptList = [...this.options]
69
+            }
70
+        }
71
+    },
72
+    methods: {
73
+        onDeptSearch() {
74
+            const keyword = this.deptSearchKeyword.trim()
75
+            if (!keyword) {
76
+                this.filteredDeptList = [...this.options]
77
+            } else {
78
+                this.filteredDeptList = this.options.filter(item =>
79
+                    item.deptName.includes(keyword)
80
+                )
81
+            }
82
+        },
83
+        onDeptSelect(item) {
84
+            this.selectedDeptId = item.deptId
85
+            this.$emit('input', item.deptId)
86
+            this.$emit('change', item)
87
+            this.$emit('update:show', false)
88
+        },
89
+        onClose() {
90
+            this.$emit('update:show', false)
91
+        }
92
+    }
93
+}
94
+</script>
95
+
96
+<style lang="scss" scoped>
97
+.dept-picker {
98
+    background: #1E1B4B;
99
+    display: flex;
100
+    flex-direction: column;
101
+    border-radius: 16rpx 16rpx 0 0;
102
+    width: 100%;
103
+    max-height: 70vh;
104
+}
105
+
106
+.picker-header {
107
+    padding: 24rpx 32rpx;
108
+    border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
109
+    display: flex;
110
+    justify-content: center;
111
+}
112
+
113
+.picker-title {
114
+    font-size: 32rpx;
115
+    font-weight: 600;
116
+    color: #fff;
117
+}
118
+
119
+.search-box {
120
+    padding: 16rpx 32rpx;
121
+}
122
+
123
+.dept-list {
124
+    flex: 1;
125
+    padding: 0 32rpx;
126
+}
127
+
128
+.dept-item {
129
+    display: flex;
130
+    align-items: center;
131
+    justify-content: space-between;
132
+    padding: 24rpx 0;
133
+    border-bottom: 1rpx solid rgba(255, 255, 255, 0.08);
134
+}
135
+
136
+.dept-item-name {
137
+    font-size: 28rpx;
138
+    color: rgba(255, 255, 255, 0.9);
139
+}
140
+
141
+.empty-state,
142
+.loading-state {
143
+    padding: 48rpx 0;
144
+    display: flex;
145
+    justify-content: center;
146
+    align-items: center;
147
+    color: rgba(255, 255, 255, 0.4);
148
+    font-size: 24rpx;
149
+}
150
+</style>

+ 60 - 28
src/pages/components/ProfileRadar.vue

@@ -1,21 +1,26 @@
1 1
 <template>
2
-    <div class="profile-radar">
3
-        <div class="chart-container" ref="radarChart"></div>
4
-        <div class="radar-list">
5
-            <div v-for="(item, index) in radarData" :key="index" class="radar-item">
6
-                <span class="item-label">{{ item.name }}</span>
7
-                <div class="progress-row">
8
-                    <div class="progress-bar">
9
-                        <div class="progress-fill"
10
-                            :style="{ width: ((item.finalScore || 0) / (maxScore || 1) * 100) + '%', background: `linear-gradient(90deg, ${item.color}33, ${item.color})` }">
11
-                            <span class="progress-end" :style="{ background: item.color }"></span>
12
-                        </div>
13
-                    </div>
14
-                    <span class="item-value">{{ item.finalScore || 0 }}</span>
15
-                </div>
16
-            </div>
17
-        </div>
18
-    </div>
2
+    <view class="profile-radar">
3
+        <view v-if="!radarData || radarData.length === 0" class="empty-state">
4
+            <text>暂无数据</text>
5
+        </view>
6
+        <template v-else>
7
+            <view class="chart-container" ref="radarChart"></view>
8
+            <view class="radar-list">
9
+                <view v-for="(item, index) in radarData" :key="index" class="radar-item">
10
+                    <text class="item-label">{{ item.name }}</text>
11
+                    <view class="progress-row">
12
+                        <view class="progress-bar">
13
+                            <view class="progress-fill"
14
+                                :style="{ width: ((item.finalScore || item.score || 0) / (maxScore || 1) * 100) + '%', background: `linear-gradient(90deg, ${item.color}33, ${item.color})` }">
15
+                                <view class="progress-end" :style="{ background: item.color }"></view>
16
+                            </view>
17
+                        </view>
18
+                        <text class="item-value">{{ item.finalScore || item.score || 0 }}</text>
19
+                    </view>
20
+                </view>
21
+            </view>
22
+        </template>
23
+    </view>
19 24
 </template>
20 25
 
21 26
 <script>
@@ -41,20 +46,26 @@ export default {
41 46
     },
42 47
     computed: {
43 48
         radarData() {
44
-            const data = this.chartsData.length > 0 ? this.chartsData : []
49
+            const data = this.chartsData || []
50
+            if (!Array.isArray(data)) return []
45 51
             return data.map((item, index) => ({
46 52
                 ...item,
47 53
                 color: item.color || freshColors[index % freshColors.length]
48 54
             }))
49 55
         },
50 56
         maxScore() {
51
-            const allScores = this.radarData.map(item => item.finalScore || 0)
57
+            const allScores = this.radarData.map(item => {
58
+                if (typeof item === 'object' && item !== null) {
59
+                    return item.finalScore || item.score || 0
60
+                }
61
+                return 0
62
+            })
52 63
             return allScores.length > 0 ? Math.max(...allScores) : 100
53 64
         },
54 65
         indicators() {
55 66
             const data = this.radarData
56 67
             return data.map(item => ({
57
-                name: item.name,
68
+                name: item.name || '',
58 69
                 max: Math.max(this.maxScore, 1)
59 70
             }))
60 71
         }
@@ -69,7 +80,6 @@ export default {
69 80
             deep: true,
70 81
             handler() {
71 82
                 this.$nextTick(() => {
72
-                    if (this.chart) this.chart.dispose()
73 83
                     this.initChart()
74 84
                 })
75 85
             }
@@ -78,16 +88,28 @@ export default {
78 88
     methods: {
79 89
         initChart() {
80 90
             if (!this.$refs.radarChart) return
91
+            if (this.chart) {
92
+                this.chart.dispose()
93
+                this.chart = null
94
+            }
95
+            
96
+            // 没有数据时不渲染图表
97
+            if (!this.radarData || this.radarData.length === 0) {
98
+                return
99
+            }
100
+            
101
+            const values = this.radarData.map(item => {
102
+                if (typeof item === 'object' && item !== null) {
103
+                    return item.finalScore || item.score || 0
104
+                }
105
+                return 0
106
+            })
107
+            
81 108
             this.chart = echarts.init(this.$refs.radarChart)
109
+            
82 110
             const option = {
83 111
                 responsive: true,
84 112
                 maintainAspectRatio: false,
85
-                grid: {
86
-                    top: 20,
87
-                    bottom: 20,
88
-                    left: 30,
89
-                    right: 30
90
-                },
91 113
                 radar: {
92 114
                     indicator: this.indicators,
93 115
                     center: ['50%', '50%'],
@@ -116,7 +138,7 @@ export default {
116 138
                         symbolSize: 8,
117 139
                         data: [
118 140
                             {
119
-                                value: this.radarData.map(item => item.finalScore || 0),
141
+                                value: values,
120 142
                                 name: '综合得分',
121 143
                                 lineStyle: {
122 144
                                     color: '#4DC8FE',
@@ -158,6 +180,16 @@ export default {
158 180
     flex-direction: column;
159 181
     gap: 20rpx;
160 182
 
183
+    .empty-state {
184
+        width: 100%;
185
+        height: 350rpx;
186
+        display: flex;
187
+        align-items: center;
188
+        justify-content: center;
189
+        color: rgba(255, 255, 255, 0.5);
190
+        font-size: 28rpx;
191
+    }
192
+
161 193
     .chart-container {
162 194
         width: 100%;
163 195
         height: 350rpx;

+ 165 - 42
src/pages/deptProfile/index.vue

@@ -1,53 +1,64 @@
1 1
 <template>
2 2
     <view class="dept-profile-page">
3
-        <div class="page-header">
4
-            <div class="header-title">
5
-                <div class="title-main">部门综合信息展示</div>
6
-                <div class="header-right">
7
-                    <div class="current-time">{{ currentTime }}</div>
8
-                </div>
9
-            </div>
10
-
11
-            <div class="time-filter">
3
+        <view class="page-header">
4
+            <view class="header-title">
5
+                <view class="title-main">部门综合信息展示</view>
6
+                <view class="header-right">
7
+                    <view class="current-time">{{ currentTime }}</view>
8
+                </view>
9
+            </view>
10
+
11
+            <view class="time-filter">
12 12
                 <scroll-view scroll-x class="time-scroll">
13
-                    <div class="time-tags">
14
-                        <div v-for="(tag, index) in timeTags" :key="index"
13
+                    <view class="time-tags">
14
+                        <view v-for="(tag, index) in timeTags" :key="index"
15 15
                             :class="['time-tag', { active: selectedTimeTag === index }]" @click="onTimeTagClick(index)">
16 16
                             {{ tag }}
17
-                        </div>
18
-                    </div>
17
+                        </view>
18
+                    </view>
19 19
                 </scroll-view>
20
-                <div v-if="selectedTimeTag === 4" class="date-range-picker">
20
+                <view v-if="selectedTimeTag === 4" class="date-range-picker">
21 21
                     <picker mode="date" :value="startDate" @change="onStartDateChange">
22
-                        <div class="date-input" :class="{ filled: startDate }">
22
+                        <view class="date-input" :class="{ filled: startDate }">
23 23
                             {{ startDate || '开始日期' }}
24
-                        </div>
24
+                        </view>
25 25
                     </picker>
26
-                    <span class="date-separator">至</span>
26
+                    <text class="date-separator">至</text>
27 27
                     <picker mode="date" :value="endDate" @change="onEndDateChange">
28
-                        <div class="date-input" :class="{ filled: endDate }">
28
+                        <view class="date-input" :class="{ filled: endDate }">
29 29
                             {{ endDate || '结束日期' }}
30
-                        </div>
30
+                        </view>
31 31
                     </picker>
32
-                </div>
33
-            </div>
32
+                </view>
33
+            </view>
34 34
 
35
-            <div class="tab-nav">
36
-                <div class="tab-item" :class="{ active: activeTab === 'profile' }" @click="activeTab = 'profile'">
35
+
36
+            <view class="dept-selector">
37
+                <view class="dept-select-trigger" :class="{ 'dept-select-trigger-disabled': !isStationMaster() }"
38
+                    @click="isStationMaster() && (showDeptPicker = true)">
39
+                    <u-icon name="list" color="#A78BFA" size="16"></u-icon>
40
+                    <text class="dept-name-text">{{ selectedDeptName || '请选择部门' }}</text>
41
+                    <u-icon name="arrow-down" color="#A78BFA" size="14"></u-icon>
42
+                </view>
43
+            </view>
44
+
45
+
46
+            <view class="tab-nav">
47
+                <view class="tab-item" :class="{ active: activeTab === 'profile' }" @click="activeTab = 'profile'">
37 48
                     能力画像
38
-                </div>
39
-                <div class="tab-item" :class="{ active: activeTab === 'data' }" @click="activeTab = 'data'">
49
+                </view>
50
+                <view class="tab-item" :class="{ active: activeTab === 'data' }" @click="activeTab = 'data'">
40 51
                     运行数据
41
-                </div>
42
-            </div>
43
-        </div>
52
+                </view>
53
+            </view>
54
+        </view>
44 55
 
45
-        <div class="page-content">
56
+        <view class="page-content">
46 57
             <SectionTitle title="部门概况">
47 58
                 <DeptStats :statsData="deptStatsData" />
48 59
             </SectionTitle>
49 60
 
50
-            <div v-if="activeTab === 'profile'">
61
+            <view v-if="activeTab === 'profile'">
51 62
                 <SectionTitle title="七维得分一览">
52 63
                     <ProfileRadar :chartsData="radarData" />
53 64
                 </SectionTitle>
@@ -63,9 +74,9 @@
63 74
                 <SectionTitle title="成员职位情况分布">
64 75
                     <MemberPositionDistribution :chartsData="memberPositionData" />
65 76
                 </SectionTitle>
66
-            </div>
77
+            </view>
67 78
 
68
-            <div v-if="activeTab === 'data'">
79
+            <view v-if="activeTab === 'data'">
69 80
                 <SectionTitle title="当日开航每小时通道过检率">
70 81
                     <PassengerChart :chartsData="passengerData" />
71 82
                 </SectionTitle>
@@ -111,8 +122,11 @@
111 122
                 <SectionTitle title="实时质控拦截物品分布">
112 123
                     <InterceptionDistribution :chartsData="interceptionData" />
113 124
                 </SectionTitle>
114
-            </div>
115
-        </div>
125
+            </view>
126
+        </view>
127
+
128
+        <DeptSelector :show.sync="showDeptPicker" v-model="selectedDeptId" :options="deptList" :loading="deptLoading"
129
+            @change="onDeptSelectChange" />
116 130
     </view>
117 131
 </template>
118 132
 
@@ -135,6 +149,7 @@ import SeizedNumAll from '../components/SeizedNumAll.vue'
135 149
 import SupervisionDistribution from '../components/SupervisionDistribution.vue'
136 150
 import InterceptionDistribution from '../components/InterceptionDistribution.vue'
137 151
 import DeptStats from '../components/DeptStats.vue'
152
+import DeptSelector from '../components/DeptSelector.vue'
138 153
 import {
139 154
     countStationTeamStats,
140 155
     getDeptMemberDistribution,
@@ -157,6 +172,7 @@ import {
157 172
     supervisionProblemPosition,
158 173
     countDeptTeamStats
159 174
 } from '@/api/portraitManagement/portraitManagement'
175
+import { getDeptUserTree } from '@/api/system/user'
160 176
 
161 177
 const itemColors = ['#60A5FA', '#34D399', '#FBBF24', '#EF4444', '#9CA3AF', '#A78BFA', '#F472B6', '#6EE7B7']
162 178
 
@@ -180,7 +196,8 @@ export default {
180 196
         SeizedNumAll,
181 197
         SupervisionDistribution,
182 198
         InterceptionDistribution,
183
-        DeptStats
199
+        DeptStats,
200
+        DeptSelector
184 201
     },
185 202
     data() {
186 203
         return {
@@ -191,6 +208,13 @@ export default {
191 208
             timer: null,
192 209
             startDate: '',
193 210
             endDate: '',
211
+
212
+            // 部门选择相关
213
+            selectedDeptId: null,
214
+            selectedDeptName: '',
215
+            showDeptPicker: false,
216
+            deptList: [],
217
+            deptLoading: false,
194 218
             radarData: [],
195 219
             teamMemberData: [],
196 220
             memberBasicData: {
@@ -281,7 +305,7 @@ export default {
281 305
         this.timer = setInterval(() => {
282 306
             this.updateTime()
283 307
         }, 1000)
284
-        this.onTimeTagClick(3)
308
+        this.fetchDeptList()
285 309
     },
286 310
     beforeDestroy() {
287 311
         if (this.timer) {
@@ -296,6 +320,70 @@ export default {
296 320
             const seconds = String(now.getSeconds()).padStart(2, '0')
297 321
             this.currentTime = `${hours}:${minutes}:${seconds}`
298 322
         },
323
+        // 部门选择改变
324
+        onDeptSelectChange(item) {
325
+            this.selectedDeptId = item.deptId
326
+            this.selectedDeptName = item.deptName
327
+            // 重新请求数据
328
+            const params = this.buildTimeParams()
329
+            this.fetchAllData(params)
330
+        },
331
+        getUserRoles() {
332
+            const userInfo = this.$store.state.user
333
+            return userInfo && userInfo.roles ? userInfo.roles : []
334
+        },
335
+        isStationMaster() {
336
+            const roles = this.getUserRoles()
337
+            return roles.includes('test') || roles.includes('zhijianke')
338
+        },
339
+        fetchDeptList() {
340
+            this.deptLoading = true
341
+            const userInfo = this.$store.state.user?.userInfo;
342
+            let params = this.isStationMaster() ? { parentId: userInfo.deptId } : { deptId: userInfo.deptId }
343
+            getDeptUserTree(params).then(res => {
344
+                if (res.code === 200) {
345
+                    let allDepts = this.flattenDeptTree(res.data || [])
346
+                    this.deptList = allDepts
347
+                    if (userInfo && userInfo.deptId) {
348
+                        const dept = this.deptList.find(d => d.deptId === userInfo.deptId)
349
+                        if (dept) {
350
+                            this.selectedDeptId = dept.deptId
351
+                            this.selectedDeptName = dept.deptName
352
+                        } else if (this.deptList.length > 0) {
353
+                            this.selectedDeptId = this.deptList[0].deptId
354
+                            this.selectedDeptName = this.deptList[0].deptName
355
+                        }
356
+                    } else if (this.deptList.length > 0) {
357
+                        this.selectedDeptId = this.deptList[0].deptId
358
+                        this.selectedDeptName = this.deptList[0].deptName
359
+                    }
360
+                    // 选择完部门后请求数据
361
+                    this.onTimeTagClick(3)
362
+                }
363
+            }).catch(() => {
364
+            }).finally(() => {
365
+                this.deptLoading = false
366
+            })
367
+        },
368
+        flattenDeptTree(tree) {
369
+            const result = []
370
+            const traverse = (nodes) => {
371
+                nodes.forEach(node => {
372
+                    if (node.id) {
373
+                        result.push({
374
+                            deptId: node.id,
375
+                            deptName: node.label,
376
+                            deptType: node.deptType
377
+                        })
378
+                    }
379
+                    if (node.children && node.children.length > 0) {
380
+                        traverse(node.children)
381
+                    }
382
+                })
383
+            }
384
+            traverse(tree)
385
+            return result
386
+        },
299 387
         fetchAllData(params) {
300 388
             this.fetchDeptStats(params)
301 389
             this.fetchRadarData(params)
@@ -318,11 +406,12 @@ export default {
318 406
         buildTimeParams() {
319 407
             const now = new Date()
320 408
             const today = this.formatDate(now)
409
+            const deptId = this.selectedDeptId || 100
321 410
             if (this.selectedTimeTag === 4) {
322 411
                 if (this.startDate && this.endDate) {
323
-                    return { startDate: this.startDate, endDate: this.endDate, deptId: 100 }
412
+                    return { startDate: this.startDate, endDate: this.endDate, deptId }
324 413
                 }
325
-                return { deptId: 100 }
414
+                return { deptId }
326 415
             }
327 416
             let startDate
328 417
             switch (this.selectedTimeTag) {
@@ -339,9 +428,9 @@ export default {
339 428
                     startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate())
340 429
                     break
341 430
                 default:
342
-                    return { deptId: 100 }
431
+                    return { deptId }
343 432
             }
344
-            return { startDate: this.formatDate(startDate), endDate: today, deptId: 100 }
433
+            return { startDate: this.formatDate(startDate), endDate: today, deptId }
345 434
         },
346 435
         formatDate(date) {
347 436
             const y = date.getFullYear()
@@ -381,9 +470,17 @@ export default {
381 470
         fetchRadarData(params) {
382 471
             getDimensionScoreOverview(params).then(res => {
383 472
                 if (res.code === 200 && res.data) {
384
-                    this.radarData = res.data.dimensions || []
473
+                    // 支持两种数据结构
474
+                    if (res.data.dimensions && Array.isArray(res.data.dimensions)) {
475
+
476
+                        this.radarData = res.data.dimensions
477
+                    } else {
478
+                        this.radarData = []
479
+                    }
385 480
                 }
386
-            }).catch(() => { })
481
+            }).catch(() => {
482
+                this.radarData = []
483
+            })
387 484
         },
388 485
         fetchTeamData(params) {
389 486
             countStationTeamStats(params).then(res => {
@@ -629,6 +726,32 @@ export default {
629 726
 </script>
630 727
 
631 728
 <style lang="scss" scoped>
729
+    .dept-selector {
730
+    padding: 16rpx 32rpx;
731
+    background: rgba(30, 27, 75, 0.8);
732
+}
733
+
734
+.dept-select-trigger {
735
+    display: flex;
736
+    align-items: center;
737
+    justify-content: center;
738
+    gap: 12rpx;
739
+    padding: 16rpx 24rpx;
740
+    background: rgba(45, 42, 85, 0.8);
741
+    border: 1rpx solid rgba(167, 139, 250, 0.3);
742
+    border-radius: 50rpx;
743
+}
744
+
745
+.dept-select-trigger-disabled {
746
+    opacity: 0.5;
747
+    pointer-events: none;
748
+}
749
+
750
+.dept-name-text {
751
+    font-size: 26rpx;
752
+    color: rgba(255, 255, 255, 0.9);
753
+}
754
+
632 755
 .dept-profile-page {
633 756
     min-height: 100vh;
634 757
     background: linear-gradient(135deg, #1E1B4B 0%, #312E81 100%);

+ 421 - 295
src/pages/employeeProfile/index.vue

@@ -1,261 +1,270 @@
1 1
 <template>
2 2
     <view class="employee-profile-page">
3
-        <div class="page-header">
4
-            <div class="header-title">
5
-                <div class="title-main">员工综合信息展示</div>
6
-                <div class="header-right">
7
-                    <div class="current-time">{{ currentTime }}</div>
8
-                </div>
9
-            </div>
10
-
11
-            <div class="time-filter">
3
+        <view class="page-header">
4
+            <view class="header-title">
5
+                <view class="title-main">员工综合信息展示</view>
6
+                <view class="header-right">
7
+                    <view class="current-time">{{ currentTime }}</view>
8
+                </view>
9
+            </view>
10
+
11
+            <view class="time-filter">
12 12
                 <scroll-view scroll-x class="time-scroll">
13
-                    <div class="time-tags">
14
-                        <div v-for="(tag, index) in timeTags" :key="index"
13
+                    <view class="time-tags">
14
+                        <view v-for="(tag, index) in timeTags" :key="index"
15 15
                             :class="['time-tag', { active: selectedTimeTag === index }]" @click="onTimeTagClick(index)">
16 16
                             {{ tag }}
17
-                        </div>
18
-                    </div>
17
+                        </view>
18
+                    </view>
19 19
                 </scroll-view>
20
-                <div v-if="selectedTimeTag === 4" class="date-range-picker">
20
+                <view v-if="selectedTimeTag === 4" class="date-range-picker">
21 21
                     <picker mode="date" :value="startDate" @change="onStartDateChange">
22
-                        <div class="date-input" :class="{ filled: startDate }">
22
+                        <view class="date-input" :class="{ filled: startDate }">
23 23
                             {{ startDate || '开始日期' }}
24
-                        </div>
24
+                        </view>
25 25
                     </picker>
26
-                    <span class="date-separator">至</span>
26
+                    <text class="date-separator">至</text>
27 27
                     <picker mode="date" :value="endDate" @change="onEndDateChange">
28
-                        <div class="date-input" :class="{ filled: endDate }">
28
+                        <view class="date-input" :class="{ filled: endDate }">
29 29
                             {{ endDate || '结束日期' }}
30
-                        </div>
30
+                        </view>
31 31
                     </picker>
32
-                </div>
33
-            </div>
34
-
35
-            <div class="tab-nav">
36
-                <div class="tab-item" :class="{ active: activeTab === 'profile' }" @click="activeTab = 'profile'">
32
+                </view>
33
+            </view>
34
+
35
+            <view class="search-selector">
36
+                <view class="search-select-trigger" :class="{ 'search-select-trigger-disabled': isSecurityCheck }"
37
+                    @click="!isSecurityCheck && (showEmployeePicker = true)">
38
+                    <u-icon name="list" color="#A78BFA" size="16"></u-icon>
39
+                    <text class="search-name-text">{{ selectedEmployeeName || '请选择员工' }}</text>
40
+                    <u-icon v-if="!isSecurityCheck" name="arrow-down" color="#A78BFA" size="14"></u-icon>
41
+                </view>
42
+            </view>
43
+
44
+            <view class="tab-nav">
45
+                <view class="tab-item" :class="{ active: activeTab === 'profile' }" @click="activeTab = 'profile'">
37 46
                     能力画像
38
-                </div>
39
-                <div class="tab-item" :class="{ active: activeTab === 'data' }" @click="activeTab = 'data'">
47
+                </view>
48
+                <view class="tab-item" :class="{ active: activeTab === 'data' }" @click="activeTab = 'data'">
40 49
                     运行数据
41
-                </div>
42
-            </div>
43
-        </div>
44
-
45
-        <div class="page-content">
46
-            <div class="search-section">
47
-                <div class="search-box">
48
-                    <input class="search-input" v-model="searchKeyword" placeholder="搜索员工姓名以查看画像" @confirm="onSearch" />
49
-                    <div class="search-btn" @click="onSearch">搜索</div>
50
-                </div>
51
-            </div>
52
-
53
-            <div v-if="!portrait.personName" class="empty-state">
54
-                <div class="empty-icon">📋</div>
55
-                <div class="empty-text">搜索员工姓名以查看画像</div>
56
-            </div>
57
-
58
-            <div v-if="portrait.personName">
59
-                <div class="info-card">
60
-                    <div class="card-title">
61
-                        <div class="title-bar"></div>
62
-                        <span>个人基本信息</span>
63
-                    </div>
64
-                    <div class="user-info">
65
-                        <div class="user-header">
66
-                            <div class="avatar-area">
67
-                                <img v-if="portrait.avatar" :src="portrait.avatar" class="avatar" alt="头像" />
68
-                                <div v-else class="avatar-placeholder">{{ portrait.personName ?
69
-                                    portrait.personName.charAt(0) : '' }}</div>
70
-                            </div>
71
-                            <div class="name-row">
72
-                                <div class="name-label">姓名:</div>
73
-                                <div class="name-value">{{ portrait.personName }}</div>
74
-                            </div>
75
-                        </div>
76
-
77
-                        <div class="info-grid">
78
-                            <div class="info-item">
79
-                                <div class="info-label">所属部门及队室:</div>
80
-                                <div class="info-value">{{ portrait.deptPath || '-' }}</div>
81
-                            </div>
82
-                            <div class="info-item">
83
-                                <div class="info-label">学历:</div>
84
-                                <div class="info-value">{{ schoolingText }}</div>
85
-                            </div>
86
-                            <div class="info-item">
87
-                                <div class="info-label">出生日期:</div>
88
-                                <div class="info-value">{{ portrait.birthday || '-' }}</div>
89
-                            </div>
90
-                            <div class="info-item">
91
-                                <div class="info-label">专业:</div>
92
-                                <div class="info-value">{{ portrait.major || '-' }}</div>
93
-                            </div>
94
-                            <div class="info-item">
95
-                                <div class="info-label">技能等级:</div>
96
-                                <div class="info-value">{{ portrait.qualificationLevelText || '-' }}</div>
97
-                            </div>
98
-                            <div class="info-item">
99
-                                <div class="info-label">标签:</div>
100
-                                <div class="info-value">
101
-                                    <span class="tag" v-if="portrait.roleNames">{{ portrait.roleNames }}</span>
102
-                                </div>
103
-                            </div>
104
-                        </div>
105
-
106
-                        <div class="score-section">
107
-                            <div class="score-circle" ref="scoreCircle"></div>
108
-                            <div class="score-box">
109
-                                <div class="score-row">
110
-                                    <span class="score-label">评分:</span>
111
-                                    <span class="score-val">{{ portrait.totalScore || 0 }}</span>
112
-                                </div>
113
-                                <div class="score-row">
114
-                                    <span class="score-label">标签得分:</span>
115
-                                    <span class="score-val">{{ tagScoreDisplay }}</span>
116
-                                </div>
117
-                            </div>
118
-                        </div>
119
-                    </div>
120
-                </div>
121
-
122
-                <div class="info-card" v-if="activeTab === 'profile'">
123
-                    <div class="card-title">
124
-                        <div class="title-bar"></div>
125
-                        <span>工作履历</span>
126
-                    </div>
127
-                    <div class="work-history">
128
-                        <span v-if="portrait.entryDate">
50
+                </view>
51
+            </view>
52
+        </view>
53
+
54
+        <view class="page-content">
55
+
56
+            <view v-if="!portrait.personName" class="empty-state">
57
+                <view class="empty-icon">📋</view>
58
+                <view class="empty-text">搜索员工姓名以查看画像</view>
59
+            </view>
60
+
61
+            <view v-if="portrait.personName">
62
+                <SectionTitle title="个人基本信息">
63
+                    <view class="user-info">
64
+                        <view class="user-header">
65
+                            <view class="avatar-area">
66
+                                <image v-if="portrait.avatar" :src="portrait.avatar" class="avatar" mode="aspectFill">
67
+                                </image>
68
+                                <view v-else class="avatar-placeholder">{{ portrait.personName ?
69
+                                    portrait.personName.charAt(0) : '' }}</view>
70
+                            </view>
71
+                            <view class="name-row">
72
+                                <view class="name-label">姓名:</view>
73
+                                <view class="name-value">{{ portrait.personName }}</view>
74
+                            </view>
75
+                        </view>
76
+
77
+                        <view class="info-grid">
78
+                            <view class="info-item">
79
+                                <view class="info-label">所属部门及队室:</view>
80
+                                <view class="info-value">{{ portrait.deptPath || '-' }}</view>
81
+                            </view>
82
+                            <view class="info-item">
83
+                                <view class="info-label">学历:</view>
84
+                                <view class="info-value">{{ schoolingText }}</view>
85
+                            </view>
86
+                            <view class="info-item">
87
+                                <view class="info-label">出生日期:</view>
88
+                                <view class="info-value">{{ portrait.birthday || '-' }}</view>
89
+                            </view>
90
+                            <view class="info-item">
91
+                                <view class="info-label">专业:</view>
92
+                                <view class="info-value">{{ portrait.major || '-' }}</view>
93
+                            </view>
94
+                            <view class="info-item">
95
+                                <view class="info-label">技能等级:</view>
96
+                                <view class="info-value">{{ portrait.qualificationLevelText || '-' }}</view>
97
+                            </view>
98
+                            <view class="info-item">
99
+                                <view class="info-label">标签:</view>
100
+                                <view class="info-value">
101
+                                    <text class="tag" v-if="portrait.roleNames">{{ portrait.roleNames }}</text>
102
+                                </view>
103
+                            </view>
104
+                        </view>
105
+
106
+                        <view class="score-section">
107
+                            <view class="score-circle" ref="scoreCircle"></view>
108
+                            <view class="score-box">
109
+                                <view class="score-row">
110
+                                    <text class="score-label">评分:</text>
111
+                                    <text class="score-val">{{ portrait.totalScore || 0 }}</text>
112
+                                </view>
113
+                                <view class="score-row">
114
+                                    <text class="score-label">标签得分:</text>
115
+                                    <text class="score-val">{{ tagScoreDisplay }}</text>
116
+                                </view>
117
+                            </view>
118
+                        </view>
119
+                    </view>
120
+                </SectionTitle>
121
+
122
+                <SectionTitle v-if="activeTab === 'profile'" title="工作履历">
123
+                    <view class="work-history">
124
+                        <text v-if="portrait.entryDate">
129 125
                             {{ formatWorkDate(portrait.entryDate) }}入职 | 司龄{{ portrait.companyYears != null ?
130 126
                                 portrait.companyYears : '-' }}年
131 127
                             | 开机年限{{ portrait.xrayOperatorYears != null ? portrait.xrayOperatorYears : '-' }}年
132 128
                             | 现任职{{ portrait.roleNames || '-' }}
133
-                        </span>
134
-                        <span v-else>暂无数据</span>
135
-                    </div>
136
-                </div>
137
-
138
-                <div class="info-card" v-if="activeTab === 'profile'">
139
-                    <div class="card-title">
140
-                        <div class="title-bar"></div>
141
-                        <span>获奖记录</span>
142
-                    </div>
143
-                    <div class="honor-list">
144
-                        <div class="honor-item" v-for="(item, index) in portrait.awards" :key="index">
145
-                            <div class="honor-name">
146
-                                <span class="honor-dot"
147
-                                    :style="{ background: item.color || honorColors[index % honorColors.length] }"></span>
129
+                        </text>
130
+                        <text v-else>暂无数据</text>
131
+                    </view>
132
+                </SectionTitle>
133
+
134
+                <SectionTitle v-if="activeTab === 'profile'" title="获奖记录">
135
+                    <view class="honor-list">
136
+                        <view class="honor-item" v-for="(item, index) in portrait.awards" :key="index">
137
+                            <view class="honor-name">
138
+                                <text class="honor-dot"
139
+                                    :style="{ background: item.color || honorColors[index % honorColors.length] }"></text>
148 140
                                 {{ item.level2Name }} {{ item.level4Name }}
149
-                            </div>
150
-                            <div class="honor-score">{{ item.score || '-' }}分</div>
151
-                        </div>
152
-                        <div v-if="!portrait.awards || portrait.awards.length === 0" class="no-data">暂无获奖记录</div>
153
-                    </div>
154
-                </div>
155
-
156
-                <div class="info-card" v-if="activeTab === 'profile'">
157
-                    <div class="card-title">
158
-                        <div class="title-bar"></div>
159
-                        <span>个人能力</span>
160
-                    </div>
161
-                    <div class="chart-container" ref="radarChart"></div>
162
-                </div>
163
-
164
-                <div class="info-card" v-if="activeTab === 'profile'">
165
-                    <div class="card-title">
166
-                        <div class="title-bar"></div>
167
-                        <span>补充信息</span>
168
-                    </div>
169
-                    <div class="supp-grid">
170
-                        <div class="supp-item">
171
-                            <span class="supp-label">政治面貌:</span>
172
-                            <span class="supp-value">{{ portrait.politicalStatusText || '-' }}</span>
173
-                        </div>
174
-                        <div class="supp-item">
175
-                            <span class="supp-label">性别:</span>
176
-                            <span class="supp-value">{{ portrait.sexText || '-' }}</span>
177
-                        </div>
178
-                        <div class="supp-item">
179
-                            <span class="supp-label">籍贯:</span>
180
-                            <span class="supp-value">{{ portrait.nativePlace || '-' }}</span>
181
-                        </div>
182
-                        <div class="supp-item">
183
-                            <span class="supp-label">民族:</span>
184
-                            <span class="supp-value">{{ portrait.nation || '-' }}</span>
185
-                        </div>
186
-                        <div class="supp-item">
187
-                            <span class="supp-label">年龄:</span>
188
-                            <span class="supp-value">{{ ageText }}</span>
189
-                        </div>
190
-                        <div class="supp-item">
191
-                            <span class="supp-label">司龄:</span>
192
-                            <span class="supp-value">{{ portrait.companyYears != null ? portrait.companyYears + '年' :
193
-                                '-' }}</span>
194
-                        </div>
195
-                        <div class="supp-item">
196
-                            <span class="supp-label">性格特征:</span>
197
-                            <span class="supp-value">{{ portrait.characterCharacteristics || '-' }}</span>
198
-                        </div>
199
-                        <div class="supp-item">
200
-                            <span class="supp-label">工作风格:</span>
201
-                            <span class="supp-value">{{ portrait.workingStyle || '-' }}</span>
202
-                        </div>
203
-                        <div class="supp-item">
204
-                            <span class="supp-label">业务岗位:</span>
205
-                            <span class="supp-value">{{ portrait.postNames || '-' }}</span>
206
-                        </div>
207
-                    </div>
208
-                </div>
209
-
210
-                <div class="info-card" v-if="activeTab === 'data'">
211
-                    <div class="card-title">
212
-                        <div class="title-bar"></div>
213
-                        <span>预警信息</span>
214
-                    </div>
215
-                    <div class="warning-content" @click="showWarning = true">
216
-                        <div class="warning-tip">点击查看详细预警信息</div>
217
-                    </div>
218
-                </div>
219
-            </div>
220
-        </div>
221
-
222
-        <div class="warning-overlay" v-if="showWarning" @click="showWarning = false">
223
-            <div class="warning-panel" @click.stop>
224
-                <div class="warning-header">
225
-                    <span class="warning-title">预警中枢</span>
226
-                    <span class="warning-close" @click="showWarning = false">✕</span>
227
-                </div>
228
-                <div class="warning-body">
229
-                    <div class="warning-desc">员工综合评估(<75分红色预警 | ≥90分优秀)</div>
230
-                    <div class="warning-score">
231
-                        <div class="warning-score-item">
232
-                            <span class="warning-score-label">综合得分</span>
233
-                            <span class="warning-score-value" :class="scoreLevelClass">{{ portrait.totalScore || 0
234
-                            }}</span>
235
-                        </div>
236
-                    </div>
237
-                    <div class="warning-detail" v-if="scoreDetails.length">
238
-                        <div class="warning-detail-title">评分明细</div>
239
-                        <div class="warning-detail-item" v-for="(item, index) in scoreDetails" :key="index">
240
-                            <div class="detail-left">
241
-                                <span class="detail-dim">{{ item.dimensionName }}</span>
242
-                                <span class="detail-name">{{ item.level3Name }}</span>
243
-                            </div>
244
-                            <div class="detail-score"
141
+                            </view>
142
+                            <view class="honor-score">{{ item.score || '-' }}分</view>
143
+                        </view>
144
+                        <view v-if="!portrait.awards || portrait.awards.length === 0" class="no-data">暂无获奖记录</view>
145
+                    </view>
146
+                </SectionTitle>
147
+
148
+                <SectionTitle v-if="activeTab === 'profile'" title="个人能力">
149
+                    <view class="chart-container" ref="radarChart"></view>
150
+                </SectionTitle>
151
+
152
+                <SectionTitle v-if="activeTab === 'profile'" title="补充信息">
153
+                    <view class="supp-grid">
154
+                        <view class="supp-item">
155
+                            <text class="supp-label">政治面貌:</text>
156
+                            <text class="supp-value">{{ portrait.politicalStatusText || '-' }}</text>
157
+                        </view>
158
+                        <view class="supp-item">
159
+                            <text class="supp-label">性别:</text>
160
+                            <text class="supp-value">{{ portrait.sexText || '-' }}</text>
161
+                        </view>
162
+                        <view class="supp-item">
163
+                            <text class="supp-label">籍贯:</text>
164
+                            <text class="supp-value">{{ portrait.nativePlace || '-' }}</text>
165
+                        </view>
166
+                        <view class="supp-item">
167
+                            <text class="supp-label">民族:</text>
168
+                            <text class="supp-value">{{ portrait.nation || '-' }}</text>
169
+                        </view>
170
+                        <view class="supp-item">
171
+                            <text class="supp-label">年龄:</text>
172
+                            <text class="supp-value">{{ ageText }}</text>
173
+                        </view>
174
+                        <view class="supp-item">
175
+                            <text class="supp-label">司龄:</text>
176
+                            <text class="supp-value">{{ portrait.companyYears != null ? portrait.companyYears + '年' :
177
+                                '-' }}</text>
178
+                        </view>
179
+                        <view class="supp-item">
180
+                            <text class="supp-label">性格特征:</text>
181
+                            <text class="supp-value">{{ portrait.characterCharacteristics || '-' }}</text>
182
+                        </view>
183
+                        <view class="supp-item">
184
+                            <text class="supp-label">工作风格:</text>
185
+                            <text class="supp-value">{{ portrait.workingStyle || '-' }}</text>
186
+                        </view>
187
+                        <view class="supp-item">
188
+                            <text class="supp-label">业务岗位:</text>
189
+                            <text class="supp-value">{{ portrait.postNames || '-' }}</text>
190
+                        </view>
191
+                    </view>
192
+                </SectionTitle>
193
+
194
+                <SectionTitle v-if="activeTab === 'data'" title="预警信息">
195
+                    <view class="warning-content" @click="showWarning = true">
196
+                        <view class="warning-tip">点击查看详细预警信息</view>
197
+                    </view>
198
+                </SectionTitle>
199
+            </view>
200
+        </view>
201
+
202
+        <view class="warning-overlay" v-if="showWarning" @click="showWarning = false">
203
+            <view class="warning-panel" @click.stop>
204
+                <view class="warning-header">
205
+                    <text class="warning-title">预警中枢</text>
206
+                    <text class="warning-close" @click="showWarning = false">✕</text>
207
+                </view>
208
+                <view class="warning-body">
209
+                    <view class="warning-desc">员工综合评估(<75分红色预警 | ≥90分优秀)</view>
210
+                    <view class="warning-score">
211
+                        <view class="warning-score-item">
212
+                            <text class="warning-score-label">综合得分</text>
213
+                            <text class="warning-score-value" :class="scoreLevelClass">{{ portrait.totalScore || 0
214
+                                }}</text>
215
+                        </view>
216
+                    </view>
217
+                    <view class="warning-detail" v-if="scoreDetails.length">
218
+                        <view class="warning-detail-title">评分明细</view>
219
+                        <view class="warning-detail-item" v-for="(item, index) in scoreDetails" :key="index">
220
+                            <view class="detail-left">
221
+                                <text class="detail-dim">{{ item.dimensionName }}</text>
222
+                                <text class="detail-name">{{ item.level3Name }}</text>
223
+                            </view>
224
+                            <view class="detail-score"
245 225
                                 :class="{ 'add-text': item.totalScore > 0, 'deduct-text': item.totalScore < 0 }">
246 226
                                 {{ item.totalScore > 0 ? '+' : '' }}{{ item.totalScore }}
247
-                            </div>
248
-                        </div>
249
-                    </div>
250
-                </div>
251
-            </div>
252
-        </div>
227
+                            </view>
228
+                        </view>
229
+                    </view>
230
+                </view>
231
+            </view>
232
+        </view>
233
+
234
+        <u-popup :show="showEmployeePicker" mode="bottom" :round="16" :mask-close-able="true"
235
+            :safe-area-inset-bottom="true" @close="onEmployeePickerClose">
236
+            <view class="employee-picker">
237
+                <view class="picker-header">
238
+                    <text class="picker-title">选择员工</text>
239
+                </view>
240
+                <view class="search-box">
241
+                    <u-input v-model="employeeSearchKeyword" placeholder="搜索员工" @confirm="onEmployeeSearch"
242
+                        @input="onEmployeeSearch" />
243
+                </view>
244
+                <scroll-view scroll-y class="employee-list">
245
+                    <view class="employee-item" v-for="item in filteredEmployeeList" :key="item.userId"
246
+                        @click="onEmployeeSelect(item)">
247
+                        <text class="employee-item-name">{{ item.nickName }}</text>
248
+                        <u-icon v-if="item.userId === selectedEmployeeId" name="checkmark" color="#34D399"
249
+                            size="18"></u-icon>
250
+                    </view>
251
+                    <view v-if="filteredEmployeeList.length === 0 && !employeeLoading" class="empty-state">
252
+                        <text>暂无数据</text>
253
+                    </view>
254
+                    <view v-if="employeeLoading" class="loading-state">
255
+                        <u-loading-icon text="加载中" textSize="14"></u-loading-icon>
256
+                    </view>
257
+                </scroll-view>
258
+            </view>
259
+        </u-popup>
253 260
     </view>
254 261
 </template>
255 262
 
256 263
 <script>
257 264
 import * as echarts from 'echarts'
258 265
 import { getEmployeePortrait, countTagScore } from '@/api/portraitManagement/portraitManagement'
266
+import { listAllUser, getDeptUserTree } from '@/api/system/user'
267
+import SectionTitle from '@/components/SectionTitle.vue'
259 268
 
260 269
 const honorColors = ['#60A5FA', '#34D399', '#FBBF24', '#EF4444', '#A78BFA', '#F472B6', '#6EE7B7', '#FB923C']
261 270
 
@@ -276,6 +285,9 @@ function getRandomHexColor() {
276 285
 
277 286
 export default {
278 287
     name: 'EmployeeProfile',
288
+    components: {
289
+        SectionTitle
290
+    },
279 291
     data() {
280 292
         return {
281 293
             activeTab: 'profile',
@@ -291,7 +303,17 @@ export default {
291 303
             scoreDetails: [],
292 304
             showWarning: false,
293 305
             radarChartInstance: null,
294
-            scoreChartInstance: null
306
+            scoreChartInstance: null,
307
+            // 员工选择相关
308
+            showEmployeePicker: false,
309
+            selectedEmployeeId: null,
310
+            selectedEmployeeName: '',
311
+            employeeList: [],
312
+            filteredEmployeeList: [],
313
+            employeeSearchKeyword: '',
314
+            employeeLoading: false,
315
+            isSecurityCheck: false,
316
+            userInfo: null
295 317
         }
296 318
     },
297 319
     computed: {
@@ -338,6 +360,7 @@ export default {
338 360
         this.timer = setInterval(() => {
339 361
             this.updateTime()
340 362
         }, 1000)
363
+        this.fetchEmployeeList()
341 364
     },
342 365
     beforeDestroy() {
343 366
         if (this.timer) {
@@ -358,6 +381,97 @@ export default {
358 381
             const seconds = String(now.getSeconds()).padStart(2, '0')
359 382
             this.currentTime = `${hours}:${minutes}:${seconds}`
360 383
         },
384
+        // 员工相关方法
385
+        fetchEmployeeList() {
386
+            this.employeeLoading = true
387
+            const user = this.$store.state.user?.userInfo || this.$store.state.user?.user
388
+            this.userInfo = user
389
+
390
+            // 检查角色是否包含 SecurityCheck
391
+            const roles = this.$store.state.user?.roles || []
392
+            this.isSecurityCheck = roles.some(role => role.includes('SecurityCheck'))
393
+
394
+            if (this.isSecurityCheck && user) {
395
+                // SecurityCheck 角色,默认显示登录人,不可编辑
396
+                this.selectedEmployeeId = user.userId || user.id
397
+                this.selectedEmployeeName = user.nickName || user.userName || ''
398
+                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]
404
+                this.employeeLoading = false
405
+                // 自动加载画像数据
406
+                this.fetchEmployeePortrait()
407
+            } else {
408
+                // 其他角色,使用 getDeptUserTree 接口
409
+                if (user && user.deptId) {
410
+                    getDeptUserTree({ deptId: user.deptId }).then(res => {
411
+                        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
420
+                                this.searchKeyword = this.selectedEmployeeName
421
+                                this.fetchEmployeePortrait()
422
+                            }
423
+                        }
424
+                    }).catch(() => {
425
+                    }).finally(() => {
426
+                        this.employeeLoading = false
427
+                    })
428
+                } else {
429
+                    this.employeeLoading = false
430
+                }
431
+            }
432
+        },
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
+        onEmployeeSearch() {
456
+            const keyword = this.employeeSearchKeyword.trim()
457
+            if (!keyword) {
458
+                this.filteredEmployeeList = [...this.employeeList]
459
+            } else {
460
+                this.filteredEmployeeList = this.employeeList.filter(item =>
461
+                    (item.nickName || '').includes(keyword)
462
+                )
463
+            }
464
+        },
465
+        onEmployeeSelect(item) {
466
+            this.selectedEmployeeId = item.userId
467
+            this.selectedEmployeeName = item.nickName
468
+            this.searchKeyword = item.nickName
469
+            this.showEmployeePicker = false
470
+            this.fetchEmployeePortrait()
471
+        },
472
+        onEmployeePickerClose() {
473
+            this.showEmployeePicker = false
474
+        },
361 475
         buildTimeParams() {
362 476
             const now = new Date()
363 477
             const today = this.formatDate(now)
@@ -516,7 +630,7 @@ export default {
516 630
                                     fontSize: 14,
517 631
                                     color: '#fff',
518 632
                                     lineHeight: 35,
519
-                                    
633
+
520 634
                                 }
521 635
                             }
522 636
                         }
@@ -681,64 +795,104 @@ export default {
681 795
     flex-shrink: 0;
682 796
 }
683 797
 
684
-.tab-nav {
798
+.search-selector {
685 799
     padding: 16rpx 32rpx;
800
+    background: rgba(30, 27, 75, 0.8);
801
+}
802
+
803
+.search-select-trigger {
686 804
     display: flex;
805
+    align-items: center;
687 806
     justify-content: center;
688
-    gap: 64rpx;
807
+    gap: 12rpx;
808
+    padding: 16rpx 24rpx;
809
+    background: rgba(45, 42, 85, 0.8);
810
+    border: 1rpx solid rgba(167, 139, 250, 0.3);
811
+    border-radius: 50rpx;
689 812
 }
690 813
 
691
-.tab-item {
692
-    font-size: 28rpx;
693
-    color: rgba(255, 255, 255, 0.5);
694
-    padding-bottom: 8rpx;
695
-    border-bottom: 2rpx solid transparent;
696
-    transition: all 0.3s;
814
+.search-select-trigger-disabled {
815
+    opacity: 0.5;
816
+    pointer-events: none;
817
+}
697 818
 
698
-    &.active {
699
-        color: #A78BFA;
700
-        border-bottom-color: #A78BFA;
701
-    }
819
+.search-name-text {
820
+    font-size: 26rpx;
821
+    color: rgba(255, 255, 255, 0.9);
702 822
 }
703 823
 
704
-.page-content {
705
-    padding: 32rpx;
824
+.employee-picker {
825
+    background: #1E1B4B;
706 826
     display: flex;
707 827
     flex-direction: column;
828
+    border-radius: 16rpx 16rpx 0 0;
829
+    width: 100%;
830
+    height: 70vh;
708 831
 }
709 832
 
710
-.search-section {
711
-    margin-bottom: 32rpx;
833
+.picker-header {
834
+    padding: 24rpx 32rpx;
835
+    border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
836
+    display: flex;
837
+    justify-content: center;
838
+    flex-shrink: 0;
839
+}
840
+
841
+.picker-title {
842
+    font-size: 32rpx;
843
+    font-weight: 600;
844
+    color: #fff;
712 845
 }
713 846
 
714 847
 .search-box {
848
+    padding: 16rpx 32rpx;
849
+    flex-shrink: 0;
850
+}
851
+
852
+.employee-list {
853
+    flex: 1;
854
+    padding: 0 32rpx;
855
+    height: 0;
856
+    overflow: hidden;
857
+}
858
+
859
+.employee-item {
715 860
     display: flex;
716
-    gap: 16rpx;
717 861
     align-items: center;
862
+    justify-content: space-between;
863
+    padding: 24rpx 0;
864
+    border-bottom: 1rpx solid rgba(255, 255, 255, 0.08);
718 865
 }
719 866
 
720
-.search-input {
721
-    flex: 1;
722
-    // padding: 16rpx 24rpx;
723
-    border-radius: 16rpx;
724
-    background: rgba(45, 42, 85, 0.8);
867
+.employee-item-name {
725 868
     font-size: 28rpx;
726
-    color: #fff;
727
-    border: 1rpx solid rgba(167, 139, 250, 0.3);
728
-
729
-    &::placeholder {
730
-        color: rgba(255, 255, 255, 0.4);
731
-    }
869
+    color: rgba(255, 255, 255, 0.9);
732 870
 }
733 871
 
734
-.search-btn {
872
+.tab-nav {
735 873
     padding: 16rpx 32rpx;
736
-    border-radius: 16rpx;
737
-    background: #A78BFA;
874
+    display: flex;
875
+    justify-content: center;
876
+    gap: 64rpx;
877
+}
878
+
879
+.tab-item {
738 880
     font-size: 28rpx;
739
-    color: #1E1B4B;
740
-    font-weight: 500;
741
-    flex-shrink: 0;
881
+    color: rgba(255, 255, 255, 0.5);
882
+    padding-bottom: 8rpx;
883
+    border-bottom: 2rpx solid transparent;
884
+    transition: all 0.3s;
885
+
886
+    &.active {
887
+        color: #A78BFA;
888
+        border-bottom-color: #A78BFA;
889
+    }
890
+}
891
+
892
+.page-content {
893
+    padding: 32rpx;
894
+    display: flex;
895
+    flex-direction: column;
742 896
 }
743 897
 
744 898
 .empty-state {
@@ -759,34 +913,6 @@ export default {
759 913
     }
760 914
 }
761 915
 
762
-.info-card {
763
-    background: rgba(45, 42, 85, 0.6);
764
-    border-radius: 20rpx;
765
-    padding: 32rpx;
766
-    margin-bottom: 24rpx;
767
-    border: 1rpx solid rgba(167, 139, 250, 0.2);
768
-}
769
-
770
-.card-title {
771
-    display: flex;
772
-    align-items: center;
773
-    gap: 12rpx;
774
-    margin-bottom: 24rpx;
775
-
776
-    .title-bar {
777
-        width: 4rpx;
778
-        height: 28rpx;
779
-        background: #A78BFA;
780
-        border-radius: 2rpx;
781
-    }
782
-
783
-    span {
784
-        font-size: 30rpx;
785
-        font-weight: 600;
786
-        color: #A78BFA;
787
-    }
788
-}
789
-
790 916
 .user-info {
791 917
     .user-header {
792 918
         display: flex;