Bläddra i källkod

refactor(warningPage): 重构预警页面,对接后端接口并优化功能

1. 新增预警页面API接口封装
2. 替换旧级联选择器为可搜索弹窗树形选择器
3. 重构表格字段映射,对接后端真实返回字段
4. 新增日期范围筛选、组织架构筛选逻辑
5. 修复员工标签展示为逗号分隔的多标签样式
6. 调整员工详情页跳转路由路径
7. 新增数据拉取、格式化和监听更新逻辑
huoyi 2 veckor sedan
förälder
incheckning
0a5dea8b61

+ 10 - 0
src/api/warningPage/warningPage.js

@@ -0,0 +1,10 @@
1
+import request from '@/utils/request'
2
+
3
+//台账数据
4
+export function getWarningPageData(data) {
5
+  return request({ url: '/ledger/warning/ledger', method: 'post', data })
6
+}
7
+//员工综合评估预警看板
8
+export function getEmployeeWarningPageData(data) {
9
+  return request({ url: '/ledger/warning/ledgerDetail', method: 'post', data })
10
+}

+ 5 - 4
src/views/portraitManagement/employeeProfile/index.vue

@@ -74,8 +74,8 @@
74 74
                   </div>
75 75
                   <div class="info-item-content">
76 76
                     <div class="info-item-label">标签:</div>
77
-                    <div class="info-item-value">
78
-                      <span class="info-item-tag" v-if="portrait.userTags">{{ portrait.userTags }}</span>
77
+                    <div class="info-item-value" v-if="portrait.userTags">
78
+                      <span class="info-item-tag" v-for="tag in portrait.userTags.split(',')" :key="tag">{{ tag }}</span>
79 79
                     </div>
80 80
                   </div>
81 81
                 </div>
@@ -341,12 +341,12 @@ const searchHandler = (query) => {
341 341
 const goToWarningPage = () => {
342 342
   if (currentQuery.value) {
343 343
     router.push({
344
-      path: '/warningPage',
344
+      path: '/index',
345 345
       query: { id: currentQuery.value.id },
346 346
    
347 347
     })
348 348
   } else {
349
-    router.push('/warningPage')
349
+    router.push('/index')
350 350
   }
351 351
 }
352 352
 
@@ -606,6 +606,7 @@ onUnmounted(() => {
606 606
               display: flex;
607 607
               align-items: center;
608 608
               flex-wrap: wrap;
609
+              gap: 2px;
609 610
             }
610 611
             .info-item-tag {
611 612
               font-size: 14px;

+ 197 - 75
src/views/warningPage/index.vue

@@ -28,15 +28,27 @@
28 28
                     <el-date-picker v-model="endDate" type="date" placeholder="结束日期" style="width: 150px;" />
29 29
 
30 30
                 </div>
31
+                <el-popover trigger="click" placement="bottom" :width="480" ref="treePopoverRef">
32
+                    <template #reference>
33
+                        <el-input v-model="selectedOrgLabel" placeholder="组织架构/员工" clearable style="width: 480px;"
34
+                            @clear="handleTreeClear" />
35
+                    </template>
36
+                    <el-tree
37
+                        :data="cascadeOptions"
38
+                        :props="treeProps"
39
+                        node-key="value"
40
+                        highlight-current
41
+                        :current-node-key="selectedOrg"
42
+                        @node-click="handleTreeNodeClick"
43
+                    />
44
+                </el-popover>
31 45
             </div>
32 46
             <div class="filter-group">
33 47
                 <el-select v-model="selectedAlertLevel" placeholder="预警等级" clearable style="width: 250px;">
34 48
                     <el-option label="全部" value=""></el-option>
35 49
                     <el-option v-for="item in alert_level" :key="item.value" :label="item.label" :value="item.value" />
36 50
                 </el-select>
37
-                <el-cascader v-model="selectedOrg" :options="cascadeOptions"
38
-                    :props="{ expandTrigger: 'hover', label: 'label', value: 'value', children: 'children', emitPath: false }"
39
-                    placeholder="组织架构/员工" clearable style="width: 280px;" />
51
+
40 52
             </div>
41 53
         </div>
42 54
 
@@ -77,32 +89,35 @@
77 89
                             </tr>
78 90
                         </thead>
79 91
                         <tbody>
80
-                            <tr v-for="emp in filteredEmployees" :key="emp.id" :class="getRowClass(emp.baseScore)">
81
-                                <td>{{ emp.id }}</td>
82
-                                <td><strong>{{ emp.name }}</strong></td>
83
-                                <td>{{ emp.dept }}</td>
92
+                            <tr v-for="emp in filteredEmployees" :key="emp.id" :class="getRowClass(emp.overallScore)">
93
+                                <td>{{ emp.userId }}</td>
94
+                                <td><strong>{{ emp.nickName }}</strong></td>
95
+                                <td>{{ emp.deptName }}</td>
84 96
                                 <td>
85
-                                    <span v-if="emp.baseScore < 75" class="score-danger">{{ emp.baseScore }} 分</span>
86
-                                    <span v-else-if="emp.baseScore >= 90" class="score-excellent">{{ emp.baseScore }}
97
+                                    <span v-if="emp.overallScore < 75" class="score-danger">{{ emp.overallScore }}
98
+                                        分</span>
99
+                                    <span v-else-if="emp.overallScore >= 90" class="score-excellent">{{ emp.overallScore
100
+                                        }}
87 101
                                         分</span>
88
-                                    <span v-else style="font-weight:600;">{{ emp.baseScore }} 分</span>
102
+                                    <span v-else style="font-weight:600;">{{ emp.overallScore }} 分</span>
89 103
                                 </td>
90 104
                                 <td>
91
-                                    <span v-if="emp.baseScore < 75" class="status-badge"
105
+                                    <span v-if="emp.overallScore < 75" class="status-badge"
92 106
                                         style="animation: subtlePulse 1s infinite;"><i
93 107
                                             class="fas fa-exclamation-triangle"></i> 红色预警</span>
94
-                                    <span v-else-if="emp.baseScore >= 90" class="status-excellent"
108
+                                    <span v-else-if="emp.overallScore >= 90" class="status-excellent"
95 109
                                         style="background:#d1fae5; color:#065f46;"><i class="fas fa-star"></i>
96 110
                                         优秀标杆</span>
97 111
                                     <span v-else class="status-warning">正常范围</span>
98 112
                                 </td>
99
-                                <td style="font-size:0.75rem;">{{ emp.riskDesc }}</td>
113
+                                <td style="font-size:0.75rem;">{{ emp.coreRisksOrOutstandingAchievements }}</td>
100 114
                                 <td>
101
-                                    <span v-if="emp.baseScore < 75" style="color:#b91c1c;"><i class="fas fa-bell"></i>
102
-                                        紧急干预</span>
103
-                                    <span v-else-if="emp.baseScore >= 90" style="color:#15803d;"><i
104
-                                            class="fas fa-crown"></i> 表彰激励</span>
105
-                                    <span v-else>常规辅导</span>
115
+                                    <span v-if="emp.overallScore < 75" style="color:#b91c1c;"><i
116
+                                            class="fas fa-bell"></i>
117
+                                        {{ getAlertLabel(emp.statusLabel) }}</span>
118
+                                    <span v-else-if="emp.overallScore >= 90" style="color:#15803d;"><i
119
+                                            class="fas fa-crown"></i> {{ getAlertLabel(emp.statusLabel) }}</span>
120
+                                    <span v-else>{{ getAlertLabel(emp.statusLabel) }}</span>
106 121
                                 </td>
107 122
                             </tr>
108 123
                         </tbody>
@@ -127,6 +142,7 @@
127 142
 <script setup>
128 143
 import { ref, computed, onMounted, watch } from 'vue'
129 144
 import { getDeptUserTree } from '@/api/item/items'
145
+import { getWarningPageData, getEmployeeWarningPageData } from '@/api/warningPage/warningPage'
130 146
 import { useDict } from '@/utils/dict'
131 147
 import { useRoute } from 'vue-router'
132 148
 
@@ -136,11 +152,14 @@ const { alert_level } = useDict('alert_level')
136 152
 
137 153
 const dateRangeInput = ref(null)
138 154
 const activeRange = ref('month')
139
-const currentFilterText = ref('')
155
+
140 156
 const startDate = ref(null)
141 157
 const endDate = ref(null)
142 158
 const selectedAlertLevel = ref('')
143 159
 const selectedOrg = ref('')
160
+const selectedOrgLabel = ref('')
161
+const treePopoverRef = ref(null)
162
+const treeProps = { label: 'label', children: 'children' }
144 163
 const cascadeOptions = ref([])
145 164
 
146 165
 const summaryCards = ref([
@@ -161,7 +180,7 @@ const newNames = [
161 180
 ]
162 181
 const deptList = ["旅检一部", "旅检二部", "旅检三部"]
163 182
 const scores = [68, 92, 55, 96, 72, 88, 48, 94, 81, 63, 91]
164
-const riskDescList = [
183
+const coreRisksOrOutstandingAchievementsList = [
165 184
     "违规操作2次+投诉1起,考试成绩62分",
166 185
     "优秀服务案例,安保测试满分,无违规",
167 186
     "安保测试未通过,不安全事件责任人",
@@ -182,32 +201,36 @@ for (let i = 0; i < newNames.length; i++) {
182 201
         id: String(10021 + i),
183 202
         name: newNames[i],
184 203
         dept: deptList[i % deptList.length],
185
-        baseScore: scores[i % scores.length],
186
-        riskDesc: riskDescList[i % riskDescList.length]
204
+        overallScore: scores[i % scores.length],
205
+        coreRisksOrOutstandingAchievements: coreRisksOrOutstandingAchievementsList[i % coreRisksOrOutstandingAchievementsList.length]
187 206
     })
188 207
 }
189 208
 
190
-// 将组织架构数据转换为级联选择器格式
209
+// 将组织架构数据转换为树形数据
191 210
 const transformCascadeData = (nodes) => {
192 211
     if (!nodes) return []
193 212
     return nodes.map(node => {
194 213
         const label = node.nickName || node.deptName || node.name || node.label
195
-        const value = node.id || node.deptId
196
-
197
-        let children = []
198
-        if (node.children && node.children.length > 0) {
199
-            children = transformCascadeData(node.children)
200
-        }
201
-        // 如果是用户节点,没有子节点
202
-        if (node.nodeType === 'user' || node.deptType === 'user') {
203
-            children = []
214
+        const deptType = node.deptType || node.nodeType
215
+
216
+        let value
217
+        if (deptType === 'BRIGADE') {
218
+            value = String(node.deptId)
219
+        } else if (deptType === 'MANAGER') {
220
+            value = String(node.teamId)
221
+        } else if (deptType === 'TEAMS') {
222
+            value = String(node.groupId)
223
+        } else {
224
+            value = String(node.userId ?? node.id ?? node.deptId ?? '')
204 225
         }
205 226
 
227
+        const children = node.children && node.children.length > 0 ? transformCascadeData(node.children) : undefined
228
+
206 229
         return {
207 230
             label,
208 231
             value,
209
-            nodeType: node.nodeType || node.deptType,
210
-            children: children.length > 0 ? children : undefined
232
+            deptType,
233
+            children
211 234
         }
212 235
     })
213 236
 }
@@ -232,18 +255,35 @@ const getSelectedInfo = (selectedValue) => {
232 255
     return findNodeByValue(cascadeOptions.value, selectedValue)
233 256
 }
234 257
 
235
-const filteredEmployees = computed(() => {
236
-    let result = [...employeesData.value]
237
-
238
-    // 模糊搜索筛选
239
-    if (currentFilterText.value.trim()) {
240
-        const keyword = currentFilterText.value.trim().toLowerCase()
241
-        result = result.filter(emp =>
242
-            emp.name.toLowerCase().includes(keyword) ||
243
-            emp.dept.toLowerCase().includes(keyword) ||
244
-            emp.id.toLowerCase().includes(keyword)
245
-        )
258
+const handleTreeNodeClick = (data) => {
259
+    selectedOrg.value = data.value
260
+    selectedOrgLabel.value = data.label
261
+    treePopoverRef.value?.hide()
262
+    fetchWarningData()
263
+}
264
+
265
+const handleTreeClear = () => {
266
+    selectedOrg.value = ''
267
+    selectedOrgLabel.value = ''
268
+    fetchWarningData()
269
+}
270
+
271
+const findNodeLabelByValue = (nodes, value) => {
272
+    if (!nodes) return ''
273
+    for (const node of nodes) {
274
+        if (node.value === value) return node.label
275
+        if (node.children) {
276
+            const found = findNodeLabelByValue(node.children, value)
277
+            if (found) return found
278
+        }
246 279
     }
280
+    return ''
281
+}
282
+
283
+const filteredEmployees = computed(() => {
284
+    let result = employeesData.value.ledgerWarningDetailItemList
285
+
286
+
247 287
 
248 288
     // 预警等级筛选
249 289
     if (selectedAlertLevel.value) {
@@ -253,44 +293,30 @@ const filteredEmployees = computed(() => {
253 293
         if (alertItem) {
254 294
             const label = alertItem.label
255 295
             if (label.includes('红色') || label.includes('预警')) {
256
-                result = result.filter(emp => emp.baseScore < 75)
296
+                result = result.filter(emp => emp.overallScore < 75)
257 297
             } else if (label.includes('优秀') || label.includes('标杆')) {
258
-                result = result.filter(emp => emp.baseScore >= 90)
298
+                result = result.filter(emp => emp.overallScore >= 90)
259 299
             } else if (label.includes('正常')) {
260
-                result = result.filter(emp => emp.baseScore >= 75 && emp.baseScore < 90)
300
+                result = result.filter(emp => emp.overallScore >= 75 && emp.overallScore < 90)
261 301
             }
262 302
         }
263 303
     }
264 304
 
265
-    // 组织架构筛选
266
-    if (selectedOrg.value) {
267
-        const selectedInfo = getSelectedInfo(selectedOrg.value)
268
-        if (selectedInfo) {
269
-            if (selectedInfo.nodeType === 'user') {
270
-                // 筛选特定用户
271
-                result = result.filter(emp => emp.name === selectedInfo.label)
272
-            } else {
273
-                // 筛选特定部门
274
-                result = result.filter(emp => emp.dept.includes(selectedInfo.label))
275
-            }
276
-        }
277
-    }
305
+
278 306
 
279 307
     return result
280 308
 })
281 309
 
282 310
 const redAlertCount = computed(() => {
283
-    return filteredEmployees.value.filter(emp => emp.baseScore < 75).length
311
+    return employeesData.value?.redAlertNum || 0
284 312
 })
285 313
 
286 314
 const excellentCount = computed(() => {
287
-    return filteredEmployees.value.filter(emp => emp.baseScore >= 90).length
315
+    return employeesData.value?.excellentBenchmarkNum || 0
288 316
 })
289 317
 
290 318
 const avgScore = computed(() => {
291
-    if (!filteredEmployees.value.length) return "0"
292
-    const total = filteredEmployees.value.reduce((sum, emp) => sum + emp.baseScore, 0)
293
-    return (total / filteredEmployees.value.length).toFixed(1)
319
+    return employeesData.value?.averageComprehensiveScore || 0
294 320
 })
295 321
 
296 322
 const getRowClass = (score) => {
@@ -299,6 +325,12 @@ const getRowClass = (score) => {
299 325
     return ""
300 326
 }
301 327
 
328
+const getAlertLabel = (value) => {
329
+    if (!value) return ''
330
+    const item = alert_level.value.find(d => d.value === value)
331
+    return item ? item.label : value
332
+}
333
+
302 334
 const setActiveRange = (range) => {
303 335
     activeRange.value = range
304 336
     // 清除自定义日期
@@ -308,19 +340,89 @@ const setActiveRange = (range) => {
308 340
     }
309 341
 }
310 342
 
311
-const handleSearch = () => {
312
-    console.log('执行筛选', {
313
-        activeRange: activeRange.value,
314
-        startDate: startDate.value,
315
-        endDate: endDate.value,
316
-        selectedAlertLevel: selectedAlertLevel.value,
317
-        selectedOrg: selectedOrg.value
318
-    })
343
+
344
+const formatDate = (d) => {
345
+    if (!d) return ''
346
+    const date = new Date(d)
347
+    const y = date.getFullYear()
348
+    const m = String(date.getMonth() + 1).padStart(2, '0')
349
+    const day = String(date.getDate()).padStart(2, '0')
350
+    return `${y}-${m}-${day}`
351
+}
352
+
353
+const getDateRangeFromActive = () => {
354
+    const now = new Date()
355
+    let start = new Date(now)
356
+    if (activeRange.value === 'week') {
357
+        start.setDate(now.getDate() - 7)
358
+    } else if (activeRange.value === 'month') {
359
+        start.setMonth(now.getMonth() - 1)
360
+    } else if (activeRange.value === 'quarter') {
361
+        start.setMonth(now.getMonth() - 3)
362
+    } else {
363
+        start.setFullYear(now.getFullYear() - 1)
364
+    }
365
+    return { startDate: formatDate(start), endDate: formatDate(now) }
366
+}
367
+
368
+const warningDataMap = {
369
+    ledgerSupervisionProblem: 0,
370
+    ledgerRealtimeInterception: 1,
371
+    ledgerUnsafeEvent: 2,
372
+    ledgerSecurityTest: 3,
373
+    ledgerComplaint: 4,
374
+    ledgerServicePatrol: 5,
375
+    ledgerExamScore: 6,
376
+    ledgerTerminalBonus: 7,
377
+    ledgerRewardApproval: 8
378
+}
379
+
380
+const fetchWarningData = async () => {
381
+    
382
+    let params = {}
383
+    if (startDate.value && endDate.value) {
384
+        params.startDate = formatDate(startDate.value)
385
+        params.endDate = formatDate(endDate.value)
386
+    } else {
387
+        const range = getDateRangeFromActive()
388
+        params.startDate = range.startDate
389
+        params.endDate = range.endDate
390
+    }
391
+
392
+    const selectedInfo = getSelectedInfo(selectedOrg.value)
393
+    if (selectedInfo) {
394
+        if (selectedInfo.deptType === 'BRIGADE') params.deptId = selectedInfo.value
395
+        else if (selectedInfo.deptType === 'MANAGER') params.teamId = selectedInfo.value
396
+        else if (selectedInfo.deptType === 'TEAMS') params.groupId = selectedInfo.value
397
+        else if (selectedInfo.deptType === 'user') params.userId = selectedInfo.value
398
+    }
399
+
400
+    try {
401
+        const [r1, r2] = await Promise.all([
402
+            getWarningPageData(params),
403
+            getEmployeeWarningPageData(params)
404
+        ])
405
+        if (r1.data) {
406
+            const d = r1.data
407
+            summaryCards.value.forEach((card, idx) => {
408
+                for (const [key, i] of Object.entries(warningDataMap)) {
409
+                    if (i === idx) {
410
+                        card.value = d[key] !== undefined ? String(d[key]) : card.value
411
+                        break
412
+                    }
413
+                }
414
+            })
415
+        }
416
+        if (r2.data) {
417
+            employeesData.value = r2.data
418
+        }
419
+    } catch (error) {
420
+        console.error('获取预警数据失败:', error)
421
+    }
319 422
 }
320 423
 
321 424
 onMounted(async () => {
322 425
     try {
323
-        // 获取组织架构数据
324 426
         const res = await getDeptUserTree()
325 427
         if (res.data) {
326 428
             cascadeOptions.value = transformCascadeData(res.data)
@@ -328,6 +430,23 @@ onMounted(async () => {
328 430
     } catch (error) {
329 431
         console.error('获取组织架构数据失败:', error)
330 432
     }
433
+    fetchWarningData()
434
+})
435
+
436
+watch(startDate, () => {
437
+    fetchWarningData()
438
+})
439
+watch(endDate, () => {
440
+    fetchWarningData()
441
+})
442
+watch(activeRange, () => {
443
+    fetchWarningData()
444
+})
445
+
446
+watch(cascadeOptions, (val) => {
447
+    if (val.length && selectedOrg.value) {
448
+        selectedOrgLabel.value = findNodeLabelByValue(val, selectedOrg.value)
449
+    }
331 450
 })
332 451
 
333 452
 // 监听路由参数变化,回显到级联选择器
@@ -335,9 +454,12 @@ watch(() => route.query, (query) => {
335 454
     const { id } = query
336 455
     if (id) {
337 456
         selectedOrg.value = id
457
+        selectedOrgLabel.value = findNodeLabelByValue(cascadeOptions.value, id)
338 458
     } else {
339 459
         selectedOrg.value = ''
460
+        selectedOrgLabel.value = ''
340 461
     }
462
+    fetchWarningData()
341 463
 }, { immediate: true })
342 464
 </script>
343 465