Kaynağa Gözat

Merge branch 'employeeScreen'

huoyi 2 hafta önce
ebeveyn
işleme
33711a19f9

+ 3 - 0
src/api/ledger/index.js

@@ -17,6 +17,9 @@ export function downloadLedgerTemplate(params) {
17 17
 }
18 18
 
19 19
 // ===== 台账一键导入 =====
20
+export function importLeaveSpecial(data) {
21
+  return request({ url: '/ledger/import/leaveSpecial', method: 'post', data })
22
+}
20 23
 export function importSupervisionProblem(data) {
21 24
   return request({ url: '/ledger/import/supervisionProblem', method: 'post', data })
22 25
 }

+ 17 - 0
src/api/system/user.js

@@ -152,3 +152,20 @@ export function getDeptUserTree(params) {
152 152
     params: params
153 153
   })
154 154
 }
155
+//通过用户ID获取用户组织信息
156
+export function getUserOrgInfo(params) {
157
+  return request({
158
+    url: '/system/dept/userOrgInfo',
159
+    method: 'get',
160
+    params: params
161
+  })
162
+}
163
+
164
+//通过部门ID获取部门组织信息及上级链
165
+export function getDeptOrgInfo(params) {
166
+  return request({
167
+    url: '/system/dept/deptOrgInfo',
168
+    method: 'get',
169
+    params: params
170
+  })
171
+}

+ 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
+}

+ 3 - 3
src/router/index.js

@@ -92,9 +92,9 @@ export const constantRoutes = [
92 92
     children: [
93 93
       {
94 94
         path: '/index',
95
-        component: () => import('@/views/portraitManagement/employeeProfile'),
96
-        name: 'Index',
97
-        meta: { title: '员工画像', icon: 'dashboard' }
95
+        component: () => import('@/views/warningPage'),
96
+        name: 'WarningPage',
97
+        meta: { title: '预警页面', icon: 'dashboard' }
98 98
       },
99 99
       // {
100 100
       //   path: '/dashboard-work',

+ 65 - 54
src/views/ledger/import/index.vue

@@ -142,6 +142,7 @@ import {
142 142
   importTerminalBonus,
143 143
   importExamScore,
144 144
   importRewardApproval,
145
+  importLeaveSpecial,
145 146
   importDailyTraining,
146 147
   importLeaderDuty,
147 148
   importHealthSoldier,
@@ -251,15 +252,15 @@ const importItems = reactive([
251 252
     loading: false,
252 253
     lastResult: null
253 254
   },
254
-  {
255
-    key: 'patrolInspection',
256
-    title: '队室三级质控巡查记录',
257
-    desc: '队室层级巡查问题记录',
258
-    icon: 'DocumentChecked',
259
-    api: importPatrolInspection,
260
-    loading: false,
261
-    lastResult: null
262
-  },
255
+  // {
256
+  //   key: 'patrolInspection',
257
+  //   title: '队室三级质控巡查记录',
258
+  //   desc: '队室层级巡查问题记录',
259
+  //   icon: 'DocumentChecked',
260
+  //   api: importPatrolInspection,
261
+  //   loading: false,
262
+  //   lastResult: null
263
+  // },
263 264
   {
264 265
     key: 'realtimeInterception',
265 266
     title: '实时质控拦截记录',
@@ -315,6 +316,15 @@ const importItems = reactive([
315 316
     lastResult: null
316 317
   },
317 318
   {
319
+    key: 'leaveSpecial',
320
+    title: '请、休假记录表(特殊)',
321
+    desc: '请、休假记录表(特殊)',
322
+    icon: 'Calendar',
323
+    api: importLeaveSpecial,
324
+    loading: false,
325
+    lastResult: null
326
+  },
327
+  {
318 328
     key: 'seizureStats',
319 329
     title: '2026查获违规品统计',
320 330
     desc: '查获违规品统计数据',
@@ -350,51 +360,52 @@ const importItems = reactive([
350 360
     loading: false,
351 361
     lastResult: null
352 362
   },
353
-  {
354
-    key: 'dailyTraining',
355
-    title: '日常培训记录',
356
-    desc: '月度培训任务及完成情况记录',
357
-    icon: 'Reading',
358
-    api: importDailyTraining,
359
-    loading: false,
360
-    lastResult: null
361
-  },
362
-  {
363
-    key: 'leaderDuty',
364
-    title: '组长履职情况记录',
365
-    desc: '队室组长本班点评及问题处置',
366
-    icon: 'Management',
367
-    api: importLeaderDuty,
368
-    loading: false,
369
-    lastResult: null
370
-  },
371
-  {
372
-    key: 'healthSoldier',
373
-    title: '健康锐兵',
374
-    desc: '员工身心健康状况台账记录',
375
-    icon: 'FirstAidKit',
376
-    api: importHealthSoldier,
377
-    loading: false,
378
-    lastResult: null
379
-  },
380
-  {
381
-    key: 'dormFireSafety',
382
-    title: '宿舍消防安全专项自查',
383
-    desc: '宿舍消防隐患自查情况记录',
384
-    icon: 'House',
385
-    api: importDormFireSafety,
386
-    loading: false,
387
-    lastResult: null
388
-  },
389
-  {
390
-    key: 'trainingIssue',
391
-    title: '培训台账问题通报',
392
-    desc: '培训台账发现问题及整改通报',
393
-    icon: 'Bell',
394
-    api: importTrainingIssue,
395
-    loading: false,
396
-    lastResult: null
397
-  }
363
+
364
+  // {
365
+  //   key: 'dailyTraining',
366
+  //   title: '日常培训记录',
367
+  //   desc: '月度培训任务及完成情况记录',
368
+  //   icon: 'Reading',
369
+  //   api: importDailyTraining,
370
+  //   loading: false,
371
+  //   lastResult: null
372
+  // },
373
+  // {
374
+  //   key: 'leaderDuty',
375
+  //   title: '组长履职情况记录',
376
+  //   desc: '队室组长本班点评及问题处置',
377
+  //   icon: 'Management',
378
+  //   api: importLeaderDuty,
379
+  //   loading: false,
380
+  //   lastResult: null
381
+  // },
382
+  // {
383
+  //   key: 'healthSoldier',
384
+  //   title: '健康锐兵',
385
+  //   desc: '员工身心健康状况台账记录',
386
+  //   icon: 'FirstAidKit',
387
+  //   api: importHealthSoldier,
388
+  //   loading: false,
389
+  //   lastResult: null
390
+  // },
391
+  // {
392
+  //   key: 'dormFireSafety',
393
+  //   title: '宿舍消防安全专项自查',
394
+  //   desc: '宿舍消防隐患自查情况记录',
395
+  //   icon: 'House',
396
+  //   api: importDormFireSafety,
397
+  //   loading: false,
398
+  //   lastResult: null
399
+  // },
400
+  // {
401
+  //   key: 'trainingIssue',
402
+  //   title: '培训台账问题通报',
403
+  //   desc: '培训台账发现问题及整改通报',
404
+  //   icon: 'Bell',
405
+  //   api: importTrainingIssue,
406
+  //   loading: false,
407
+  //   lastResult: null
408
+  // }
398 409
 ])
399 410
 
400 411
 function beforeUpload(file) {

+ 5 - 1
src/views/portraitManagement/components/ProfileRadar.vue

@@ -1,5 +1,5 @@
1 1
 <template>
2
-  <InfoCard title="七维得分一览">
2
+  <InfoCard :title="title">
3 3
     <div class="radar-section">
4 4
       <div ref="radarChartRef" class="radar-chart"></div>
5 5
       <div class="radar-list">
@@ -26,6 +26,10 @@ import * as echarts from 'echarts'
26 26
 import InfoCard from './card.vue'
27 27
 
28 28
 const props = defineProps({
29
+  title: {
30
+    type: String,
31
+    default: '七维得分一览'
32
+  },
29 33
   chartData: {
30 34
     type: Array,
31 35
     default: () => []

+ 1 - 1
src/views/portraitManagement/components/SearchBar.vue

@@ -308,7 +308,7 @@ onMounted(async () => {
308 308
         const path = buildPathForNode(departments.value, defaultNode.deptId || defaultNode.id)
309 309
         const name = defaultNode.label
310 310
         personName.value = path.length > 0 ? `${path.join(' / ')} / ${name}` : name
311
-        curQuery.value = { personName: defaultNode.label }
311
+        curQuery.value = { personName: defaultNode.label,id:defaultNode.id }
312 312
         searchHandler(curQuery.value)
313 313
       } else {
314 314
         const path = buildPathForNode(departments.value, defaultNode.deptId || defaultNode.id)

+ 1 - 1
src/views/portraitManagement/deptProfile/component/profile.vue

@@ -2,7 +2,7 @@
2 2
   <div class="main-content-wrapper">
3 3
     <div class="main-content">
4 4
       <div class="content-row">
5
-        <ProfileRadar :chartData="radarData" />
5
+        <ProfileRadar :chartData="radarData" :title="'五维得分一览'" />
6 6
         <ProfileMembers :columns="teamColumns" :data="teamMembers" />
7 7
       </div>
8 8
 

+ 1 - 1
src/views/portraitManagement/deptProfile/index.vue

@@ -1,7 +1,7 @@
1 1
 <template>
2 2
   <div class="group-profile-page">
3 3
     <Page title="安检人事管理可视化大屏" :tabs="['能力画像', '运行数据']" @tab-change="handleTabChange">
4
-      <SearchBar v-model:visible="searchVisible" ref="searchBar" @search="handleSearch" @init-done="handleInitDone"
4
+      <SearchBar v-model:visible="searchVisible" ref="searchBar" @search="handleSearch" 
5 5
         :deptType="'BRIGADE'" />
6 6
       <PentagonGroup :items="pentagonItems" />
7 7
       <Profile v-if="activeTab === 0" :query-params="queryParams" />

+ 35 - 10
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.roleNames">{{ portrait.roleNames }}</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>
@@ -110,7 +110,6 @@
110 110
                 <span v-if="portrait.entryDate">
111 111
                   {{ formatWorkDate(portrait.entryDate)  }}入职 | 司龄{{ portrait.companyYears != null ? portrait.companyYears : '-' }}年 | 开机年限{{ portrait.xrayOperatorYears != null ? portrait.xrayOperatorYears : '-'
112 112
 
113
-
114 113
                   }}年 | 现任职{{ portrait.roleNames || '-' }}
115 114
                 </span>
116 115
                 <span v-else>暂无数据</span>
@@ -130,7 +129,7 @@
130 129
           </div>
131 130
           <div class="content-bottom-center">
132 131
             <Card title="个人能力">
133
-              <div ref="abilityChart" class="chart-box" />
132
+              <div ref="abilityChart" class="chart-box" @click="goToWarningPage" />
134 133
             </Card>
135 134
           </div>
136 135
           <div class="content-bottom-right">
@@ -224,11 +223,14 @@ import Card from '../components/card.vue'
224 223
 import SearchBar from '../components/SearchBar.vue'
225 224
 import { getEmployeePortrait } from '@/api/score/index'
226 225
 import { countTagScore } from '@/api/portraitManagement/portraitManagement'
227
-import { onMounted, reactive, ref, computed } from 'vue'
226
+import { onMounted, onUnmounted, reactive, ref, computed, watch } from 'vue'
228 227
 import { useDict } from '@/utils/dict'
229 228
 import { useECharts } from '@/hooks/useEcharts'
230 229
 import useUserStore from '@/store/modules/user'
230
+import { useRouter } from 'vue-router'
231 231
 
232
+const router = useRouter()
233
+let radarChartInstance = null
232 234
 
233 235
 const { sys_user_schooling } = useDict('sys_user_schooling')
234 236
 const userStore = useUserStore()
@@ -238,6 +240,7 @@ const portrait = ref({ dimensions: [] })
238 240
 const abilityChart = ref(null)
239 241
 const activeDimName = ref(null)
240 242
 const tagScoreData = ref(null)
243
+const currentQuery = ref(null)
241 244
 
242 245
 const scoreDetails = computed(() => portrait.value?.scoreDetails || [])
243 246
 
@@ -331,9 +334,22 @@ const invokerEmployeePortrait = (query) => {
331 334
 
332 335
 const searchHandler = (query) => {
333 336
   visible.value = false
337
+  currentQuery.value = query
334 338
   invokerEmployeePortrait(query)
335 339
 }
336 340
 
341
+const goToWarningPage = () => {
342
+  if (currentQuery.value) {
343
+    router.push({
344
+      path: '/index',
345
+      query: { id: currentQuery.value.id },
346
+   
347
+    })
348
+  } else {
349
+    router.push('/index')
350
+  }
351
+}
352
+
337 353
 const radarData = reactive({
338 354
   grounp: [],
339 355
   data: []
@@ -424,7 +440,7 @@ let pisition = {
424 440
 }
425 441
 const tipRef = ref(null)
426 442
 
427
-useECharts(abilityChart, setRadarOptions).bindEvent('mousemove', (e) => {
443
+radarChartInstance = useECharts(abilityChart, setRadarOptions).bindEvent('mousemove', (e) => {
428 444
   if (!Array.isArray(portrait.value.dimensions) || !portrait.value.dimensions.length) return
429 445
   const rect = abilityChart.value.getBoundingClientRect()
430 446
   if (!tipRef.value) return
@@ -455,11 +471,19 @@ useECharts(abilityChart, setRadarOptions).bindEvent('mousemove', (e) => {
455 471
   if (tipRef.value) tipRef.value.style.display = 'none'
456 472
 })
457 473
 
474
+
475
+
476
+const handleMouseMove = (eve) => {
477
+  pisition.x = eve.pageX
478
+  pisition.y = eve.pageY
479
+}
480
+
458 481
 onMounted(() => {
459
-  window.addEventListener('mousemove', (eve) => {
460
-    pisition.x = eve.pageX
461
-    pisition.y = eve.pageY
462
-  })
482
+  window.addEventListener('mousemove', handleMouseMove)
483
+})
484
+
485
+onUnmounted(() => {
486
+  window.removeEventListener('mousemove', handleMouseMove)
463 487
 })
464 488
 </script>
465 489
 
@@ -582,6 +606,7 @@ onMounted(() => {
582 606
               display: flex;
583 607
               align-items: center;
584 608
               flex-wrap: wrap;
609
+              gap: 2px;
585 610
             }
586 611
             .info-item-tag {
587 612
               font-size: 14px;

+ 1 - 5
src/views/portraitManagement/groupProfile/component/profile.vue

@@ -69,11 +69,7 @@ const skillData = ref([])
69 69
 
70 70
 const operateData = ref([])
71 71
 
72
-const postData = ref({
73
-  categories: ['前传', '人身', '验证', '开包', '开机'],
74
-  values: [4, 5, 6, 7, 8],
75
-  colors: ['#ff6b6b', '#ee5a24']
76
-})
72
+const postData = ref([])
77 73
 const fetchTeamMembers = async (params) => {
78 74
   try {
79 75
     const res = await getDeptMembers(params)

+ 16 - 7
src/views/portraitManagement/stationProfile/component/runData.vue

@@ -426,15 +426,24 @@ const initDailySeizedArea = () => {
426 426
   }
427 427
   const rawData = countSeizureSingleQuantityData.value
428 428
   if (!rawData || rawData.length === 0) return
429
+
430
+  const dates = [...new Set(rawData.map(item => item.recordDate))].sort()
431
+  const groupNames = [...new Set(rawData.flatMap(d => d.items.map(i => i.groupName)))]
432
+
429 433
   const groupMap = {}
430
-  rawData.forEach(item => {
431
-    if (!groupMap[item.groupName]) {
432
-      groupMap[item.groupName] = []
433
-    }
434
-    groupMap[item.groupName].push(item.seizeQuantity)
434
+  groupNames.forEach(name => {
435
+    groupMap[name] = new Array(dates.length).fill(0)
435 436
   })
436
-  const groupNames = Object.keys(groupMap)
437
-  const dates = [...new Set(rawData.map(item => item.recordDate))]
437
+
438
+  rawData.forEach(record => {
439
+    const dateIndex = dates.indexOf(record.recordDate)
440
+    record.items.forEach(item => {
441
+      if (groupMap[item.groupName] && dateIndex >= 0) {
442
+        groupMap[item.groupName][dateIndex] = item.seizeQuantity
443
+      }
444
+    })
445
+  })
446
+
438 447
   const countryColors = ['#4da6ff', '#7eff7e', '#bd03fb', '#ffd93d', '#ff6b6b', '#00f5ff', '#ffa500', '#71B138']
439 448
   const series = groupNames.map((name, idx) => {
440 449
     const color = countryColors[idx % countryColors.length]

+ 80 - 48
src/views/score/event/index.vue

@@ -188,7 +188,7 @@
188 188
           <el-col :span="12">
189 189
             <el-form-item label="部门名称" prop="deptId" :rules="rules.deptId">
190 190
               <el-select v-model="form.deptId" placeholder="请选择部门" style="width:100%" clearable filterable
191
-                @change="handleDeptChange">
191
+                 @change="handleBrigadeChange">
192 192
                 <el-option v-for="d in deptOptions" :key="d.deptId" :label="d.deptName" :value="d.deptId" />
193 193
               </el-select>
194 194
             </el-form-item>
@@ -196,7 +196,7 @@
196 196
           <el-col :span="12">
197 197
             <el-form-item label="队室/班组" prop="teamId" :rules="rules.teamId">
198 198
               <el-select v-model="form.teamId" placeholder="请选择队室/班组" style="width:100%" clearable filterable
199
-                :disabled="form.org < 2" @change="handleTeamChange">
199
+                 @change="handleDepartmentChange">
200 200
                 <el-option v-for="t in teamOptions" :key="t.id" :label="t.label" :value="t.id" />
201 201
               </el-select>
202 202
             </el-form-item>
@@ -204,7 +204,7 @@
204 204
           <el-col :span="12">
205 205
             <el-form-item label="通道/小组" prop="groupId" :rules="rules.groupId">
206 206
               <el-select v-model="form.groupId" placeholder="请选择通道/小组" style="width:100%" clearable filterable
207
-                :disabled="form.org < 3" @change="handleGroupChange">
207
+                @change="handleGroupChange">
208 208
                 <el-option v-for="g in groupOptions" :key="g.id" :label="g.label" :value="g.id" />
209 209
               </el-select>
210 210
             </el-form-item>
@@ -212,7 +212,7 @@
212 212
           <el-col :span="12">
213 213
             <el-form-item label="责任人" prop="personId" :rules="rules.personId">
214 214
               <el-select v-model="form.personId" placeholder="请选择责任人" style="width:100%" clearable filterable
215
-                :disabled="form.org < 4">
215
+                 @change="handlePersonChange">
216 216
                 <el-option v-for="p in personOptions" :key="p.userId" :label="p.nickName" :value="p.userId" />
217 217
               </el-select>
218 218
             </el-form-item>
@@ -250,7 +250,7 @@
250 250
 <script setup>
251 251
 import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
252 252
 import { ElMessage, ElMessageBox } from 'element-plus'
253
-import { listUser } from '@/api/system/user'
253
+import { listUser, getDeptOrgInfo, getUserOrgInfo } from '@/api/system/user'
254 254
 import {
255 255
   listScoreEvent, addScoreEvent, updateScoreEvent, delScoreEvent,
256 256
   exportScoreEvent, importScoreEvent
@@ -289,13 +289,12 @@ const rules = computed(() => {
289 289
   return {
290 290
     org: [{ required: true, message: '请选择配分层级', trigger: 'submit' }],
291 291
     dimensionId: [{ required: true, message: '请选择维度', trigger: 'submit' }],
292
-    personName: [{ required: true, message: '请输入责任人', trigger: 'submit' }],
293 292
     eventTime: [{ required: true, message: '请选择事件时间', trigger: 'submit' }],
294 293
     scoreValue: [{ required: true, message: '请输入分值', trigger: 'submit' }],
295
-    deptId: ['1', '2', '3', '4'].includes(form.org) ? [{ required: true, message: '请选择部门', trigger: 'submit' }] : [],
296
-    teamId: ['2', '3', '4'].includes(form.org) ? [{ required: true, message: '请选择队室/班组', trigger: 'submit' }] : [],
297
-    groupId: ['3', '4'].includes(form.org) ? [{ required: true, message: '请选择通道/小组', trigger: 'submit' }] : [],
298
-    personId: ['4'].includes(form.org) ? [{ required: true, message: '请选择责任人', trigger: 'submit' }] : [],
294
+    deptId: form.org === '1' ? [{ required: true, message: '请选择部门', trigger: 'submit' }] : [],
295
+    teamId: form.org === '2' ? [{ required: true, message: '请选择队室/班组', trigger: 'submit' }] : [],
296
+    groupId: form.org === '3' ? [{ required: true, message: '请选择通道/小组', trigger: 'submit' }] : [],
297
+    personId: form.org === '4' ? [{ required: true, message: '请选择责任人', trigger: 'submit' }] : [],
299 298
   }
300 299
 })
301 300
 
@@ -321,56 +320,73 @@ async function handleFormLevelChange() {
321 320
   indicatorTree.value = []
322 321
   level2Options.value = []; level3Options.value = []; level4Options.value = []
323 322
   teamOptions.value = []; groupOptions.value = []; personOptions.value = []
324
-  const r = await getDimensionAll({ pageNum: 1, pageSize: 100, org: form.org })
323
+
324
+  const org = form.org
325
+  // if (org === '2') {
326
+  await loadAllTeams()
327
+  // } else if (org === '3') {
328
+  await loadAllGroups()
329
+  // } else if (org === '4') {
330
+  await loadAllPersons()
331
+  // }
332
+
333
+  const r = await getDimensionAll({ pageNum: 1, pageSize: 100, org })
325 334
   dimensionOptions.value = r.data || []
326 335
 }
327 336
 
337
+async function loadAllTeams() {
338
+  const r = await listDept({ deptType: 'MANAGER' })
339
+  teamOptions.value = (r.data || []).map(d => ({ id: d.deptId, label: d.deptName }))
340
+}
341
+
328 342
 async function loadDepts() {
329 343
   const r = await listDept()
330 344
   deptOptions.value = (r.data || []).filter(d => d.deptType === 'BRIGADE')
331 345
 }
332 346
 
333
-async function handleDeptChange(deptId, preserve) {
334
-  if (!preserve) {
335
-    form.teamId = null; form.groupId = null; form.personId = null
336
-    groupOptions.value = []; personOptions.value = []
337
-  }
338
-  if (deptId) {
339
-    const r = await deptTreeSelect({ parentId: deptId })
340
-    teamOptions.value = r.data || []
341
-    if (preserve && form.teamId) {
342
-      await handleTeamChange(form.teamId, true)
347
+async function loadAllGroups() {
348
+  const r = await listDept({ deptType: 'TEAMS' })
349
+  groupOptions.value = (r.data || []).map(d => ({ id: d.deptId, label: d.deptName }))
350
+}
351
+
352
+async function loadAllPersons() {
353
+  const r = await listUser({ pageSize: 9999 })
354
+  personOptions.value = r.rows || []
355
+}
356
+
357
+function handleBrigadeChange() {
358
+}
359
+
360
+async function handleDepartmentChange(val) {
361
+  form.deptId = null
362
+  if (val) {
363
+    const r = await getDeptOrgInfo({ deptId: val })
364
+    if (r.data) {
365
+      form.deptId = r.data.brigadeId
343 366
     }
344
-  } else {
345
-    teamOptions.value = []
346 367
   }
347 368
 }
348 369
 
349
-async function handleTeamChange(val, preserve) {
350
-  if (!preserve) {
351
-    form.groupId = null; form.personId = null
352
-    personOptions.value = []
353
-  }
354
-  const team = teamOptions.value.find(t => t.id === val)
355
-  if (team) {
356
-    const r = await deptTreeSelect({ parentId: team.id })
357
-    groupOptions.value = r.data || []
358
-    if (preserve && form.groupId) {
359
-      await handleGroupChange(form.groupId, true)
370
+async function handleGroupChange(val) {
371
+  form.deptId = null; form.teamId = null
372
+  if (val) {
373
+    const r = await getDeptOrgInfo({ deptId: val })
374
+    if (r.data) {
375
+      form.deptId = r.data.brigadeId
376
+      form.teamId = r.data.departmentId
360 377
     }
361
-  } else {
362
-    groupOptions.value = []
363 378
   }
364 379
 }
365 380
 
366
-async function handleGroupChange(val, preserve) {
367
-  if (!preserve) form.personId = null
368
-  const group = groupOptions.value.find(g => g.id === val)
369
-  if (group) {
370
-    const r = await listUser({ deptId: group.id })
371
-    personOptions.value = r.rows || []
372
-  } else {
373
-    personOptions.value = []
381
+async function handlePersonChange(val) {
382
+  form.deptId = null; form.teamId = null; form.groupId = null
383
+  if (val) {
384
+    const r = await getUserOrgInfo({ userId: val })
385
+    if (r.data) {
386
+      form.deptId = r.data.deptInfo.brigadeId
387
+      form.teamId = r.data.deptInfo.departmentId
388
+      form.groupId = r.data.deptInfo.teamId
389
+    }
374 390
   }
375 391
 }
376 392
 
@@ -440,14 +456,30 @@ function handleUpdate(row) {
440 456
   const record = row?.id ? row : list.value.find(r => r.id === ids.value[0])
441 457
   if (record) {
442 458
     Object.assign(form, record)
459
+    const org = String(record.org)
460
+    form.org = org
461
+    if (org === '1') {
462
+      form.deptId = record.deptId || null
463
+    } else if (org === '2') {
464
+      loadAllTeams().then(() => {
465
+        form.teamId = record.teamId || null
466
+        if (form.teamId) handleDepartmentChange(form.teamId)
467
+      })
468
+    } else if (org === '3') {
469
+      loadAllGroups().then(() => {
470
+        form.groupId = record.groupId || null
471
+        if (form.groupId) handleGroupChange(form.groupId)
472
+      })
473
+    } else if (org === '4') {
474
+      loadAllPersons().then(() => {
475
+        form.personId = record.personId || null
476
+        if (form.personId) handlePersonChange(form.personId)
477
+      })
478
+    }
443 479
     if (record.dimensionId) onDimensionChange(record.dimensionId, true)
444 480
     if (record.level2Id) onLevel2Change(record.level2Id, true)
445 481
     if (record.level3Id) onLevel3Change(record.level3Id, true)
446 482
     if (record.level4Id) onLevel4Change(record.level4Id, true)
447
-    if (record.deptId) handleDeptChange(record.deptId, true)
448
-    if (record.teamId) handleTeamChange(record.teamId, true)
449
-    if (record.groupId) handleGroupChange(record.groupId, true)
450
-
451 483
   }
452 484
   dialogTitle.value = '修改配分事项'; dialogVisible.value = true
453 485
 }

+ 7 - 0
src/views/system/dept/index.vue

@@ -311,4 +311,11 @@ function handleDelete(row) {
311 311
 }
312 312
 
313 313
 getList()
314
+onMounted(() => {
315
+    listDept().then(response => {
316
+    const data = { deptId: 0, deptName: '顶级节点', children: [] }
317
+    data.children = proxy.handleTree(response.data, "deptId")
318
+    deptOptions.value.push(data)
319
+  })
320
+})
314 321
 </script>

+ 56 - 0
src/views/system/user/components/UserInfoEdit.vue

@@ -90,6 +90,18 @@
90 90
               </el-select>
91 91
             </el-form-item>
92 92
           </el-col>
93
+          <el-col :span="12">
94
+            <el-form-item label="标签" prop="tags">
95
+              <div class="tag-input-wrap">
96
+                <el-tag v-for="tag in tagsArray" :key="tag" closable :disable-transitions="false" @close="handleClose(tag)">
97
+                  {{ tag }}
98
+                </el-tag>
99
+                <el-input v-if="inputVisible" ref="tagInputRef" v-model="inputValue" size="small" style="width: 100px"
100
+                  @keyup.enter="handleInputConfirm" @blur="handleInputConfirm" />
101
+                <el-button v-else size="small" @click="showInput">+ 新增标签</el-button>
102
+              </div>
103
+            </el-form-item>
104
+          </el-col>
93 105
         </el-row>
94 106
       </el-card>
95 107
       <el-card header="状态管理信息" style="margin-bottom: 10px;">
@@ -426,6 +438,42 @@ defineProps({
426 438
 const userRef = ref(null)
427 439
 const form = defineModel('form', ref({}))
428 440
 
441
+const tagsArray = ref([])
442
+const inputVisible = ref(false)
443
+const inputValue = ref('')
444
+const tagInputRef = ref(null)
445
+
446
+watch(() => form.value.tags, (val) => {
447
+  if (val) {
448
+    tagsArray.value = val.split(',').filter(t => t.trim())
449
+  } else {
450
+    tagsArray.value = []
451
+  }
452
+}, { immediate: true })
453
+
454
+watch(tagsArray, (arr) => {
455
+  form.value.tags = arr.join(',')
456
+}, { deep: true })
457
+
458
+const handleClose = (tag) => {
459
+  tagsArray.value = tagsArray.value.filter(t => t !== tag)
460
+}
461
+
462
+const showInput = () => {
463
+  inputVisible.value = true
464
+  nextTick(() => {
465
+    tagInputRef.value?.focus()
466
+  })
467
+}
468
+
469
+const handleInputConfirm = () => {
470
+  if (inputValue.value) {
471
+    tagsArray.value = [...tagsArray.value, inputValue.value]
472
+  }
473
+  inputVisible.value = false
474
+  inputValue.value = ''
475
+}
476
+
429 477
 // 政治面貌、资质等级、紧急联系人关系、生肖、星座、血型、是否通过政审、学历、性格特征、工作风格、团队配合、关键词、能力表现、人际互动、成长潜力
430 478
 const {
431 479
   sys_user_political_status,
@@ -541,5 +589,13 @@ defineExpose({
541 589
       color: #000;
542 590
     }
543 591
   }
592
+
593
+  .tag-input-wrap {
594
+    display: flex;
595
+    flex-wrap: wrap;
596
+    gap: 6px;
597
+    align-items: center;
598
+    width: 100%;
599
+  }
544 600
 }
545 601
 </style>

+ 837 - 0
src/views/warningPage/index.vue

@@ -0,0 +1,837 @@
1
+<template>
2
+    <div class="dashboard">
3
+        <div class="header">
4
+            <div class="title-section">
5
+                <h1><i class="fas fa-shield-alt" style="color:#2c5282; margin-right: 8px;"></i>预警中枢</h1>
6
+                <p>员工综合评估(<75分红色预警 | ≥90分优秀)</p>
7
+            </div>
8
+            <div class="badge-group">
9
+                <div class="alert-badge"><i class="fas fa-exclamation-triangle"></i> 实时预警</div>
10
+                <div class="alert-badge" style="border-left-color:#f97316;"><i class="fas fa-chart-line"></i> 动态月度数据
11
+                </div>
12
+            </div>
13
+        </div>
14
+
15
+        <div class="filter-bar">
16
+            <div class="time-range">
17
+                <button class="time-btn" :class="{ active: activeRange === 'week' }"
18
+                    @click="setActiveRange('week')">近一周</button>
19
+                <button class="time-btn" :class="{ active: activeRange === 'month' }"
20
+                    @click="setActiveRange('month')">近一月</button>
21
+                <button class="time-btn" :class="{ active: activeRange === 'quarter' }"
22
+                    @click="setActiveRange('quarter')">近三月</button>
23
+                <button class="time-btn" :class="{ active: activeRange === 'year' }"
24
+                    @click="setActiveRange('year')">近一年</button>
25
+                <div class="custom-date">
26
+                    <el-date-picker v-model="startDate" type="date" placeholder="开始日期" style="width: 150px;" />
27
+                    <span style="margin: 0 8px;">至</span>
28
+                    <el-date-picker v-model="endDate" type="date" placeholder="结束日期" style="width: 150px;" />
29
+
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>
45
+            </div>
46
+            <div class="filter-group">
47
+                <el-select v-model="selectedAlertLevel" placeholder="预警等级" clearable style="width: 250px;">
48
+                    <el-option label="全部" value=""></el-option>
49
+                    <el-option v-for="item in alert_level" :key="item.value" :label="item.label" :value="item.value" />
50
+                </el-select>
51
+
52
+            </div>
53
+        </div>
54
+
55
+        <div class="cards-container">
56
+            <div class="cards-grid" style="overflow-x: auto; white-space: nowrap;">
57
+                <div class="card" v-for="(item, index) in summaryCards" :key="index"
58
+                    style="display: inline-block; width: 200px; flex-shrink: 0;">
59
+                    <div class="card-header">
60
+                        <i :class="item.icon"></i>
61
+                        <h3>{{ item.title }}</h3>
62
+                        <span class="card-badge">{{ item.badge }}</span>
63
+                    </div>
64
+                    <div class="value-large" :style="{ color: item.color }">{{ item.value }}</div>
65
+                </div>
66
+            </div>
67
+        </div>
68
+
69
+        <div class="employee-section">
70
+            <div class="section-title">
71
+                <i class="fas fa-users" style="color:#dc2626;"></i> 员工综合评估预警看板
72
+                <span
73
+                    style="font-size:0.7rem; background:#eef2ff; padding:4px 12px; border-radius:30px; margin-left:12px;">
74
+                    评分依据:员工配分表
75
+                </span>
76
+            </div>
77
+            <div class="employee-card">
78
+                <div style="overflow-x: auto;">
79
+                    <table class="data-table" style="width:100%;">
80
+                        <thead>
81
+                            <tr>
82
+                                <th>员工ID</th>
83
+                                <th>姓名</th>
84
+                                <th>所属部门</th>
85
+                                <th>综合评估得分</th>
86
+                                <th>预警等级</th>
87
+                                <th>核心风险/优秀事迹</th>
88
+                                <th>状态标签</th>
89
+                            </tr>
90
+                        </thead>
91
+                        <tbody>
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>
96
+                                <td>
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
+                                        }}
101
+                                        分</span>
102
+                                    <span v-else style="font-weight:600;">{{ emp.overallScore }} 分</span>
103
+                                </td>
104
+                                <td>
105
+                                    <span v-if="emp.overallScore < 75" class="status-badge"
106
+                                        style="animation: subtlePulse 1s infinite;"><i
107
+                                            class="fas fa-exclamation-triangle"></i> 红色预警</span>
108
+                                    <span v-else-if="emp.overallScore >= 90" class="status-excellent"
109
+                                        style="background:#d1fae5; color:#065f46;"><i class="fas fa-star"></i>
110
+                                        优秀标杆</span>
111
+                                    <span v-else class="status-warning">正常范围</span>
112
+                                </td>
113
+                                <td style="font-size:0.75rem;">{{ emp.coreRisksOrOutstandingAchievements }}</td>
114
+                                <td>
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>
121
+                                </td>
122
+                            </tr>
123
+                        </tbody>
124
+                    </table>
125
+                </div>
126
+                <div class="warning-summary">
127
+                    <div><i class="fas fa-circle" style="color:#ef4444;"></i> <strong>红色预警</strong> (综合评估 &lt; 75分) 共计
128
+                        {{ redAlertCount }} 人 → 立即约谈/培训</div>
129
+                    <div><i class="fas fa-circle" style="color:#22c55e;"></i> <strong>优秀标杆</strong> (综合评估 ≥ 90分) 共计 {{
130
+                        excellentCount }} 人 → 表彰激励</div>
131
+                    <div><i class="fas fa-chart-simple"></i> 全员平均综合得分: <strong>{{ avgScore }}</strong> 分</div>
132
+                </div>
133
+            </div>
134
+        </div>
135
+
136
+        <footer>
137
+            <i class="far fa-bell"></i> 员工部门:旅检一/二/三部 | 阈值:<75红色预警 | ≥90优秀
138
+        </footer>
139
+    </div>
140
+</template>
141
+
142
+<script setup>
143
+import { ref, computed, onMounted, watch } from 'vue'
144
+import { getDeptUserTree } from '@/api/item/items'
145
+import { getWarningPageData, getEmployeeWarningPageData } from '@/api/warningPage/warningPage'
146
+import { useDict } from '@/utils/dict'
147
+import { useRoute } from 'vue-router'
148
+
149
+const route = useRoute()
150
+
151
+const { alert_level } = useDict('alert_level')
152
+
153
+const dateRangeInput = ref(null)
154
+const activeRange = ref('month')
155
+
156
+const startDate = ref(null)
157
+const endDate = ref(null)
158
+const selectedAlertLevel = ref('')
159
+const selectedOrg = ref('')
160
+const selectedOrgLabel = ref('')
161
+const treePopoverRef = ref(null)
162
+const treeProps = { label: 'label', children: 'children' }
163
+const cascadeOptions = ref([])
164
+
165
+const summaryCards = ref([
166
+    { icon: 'fas fa-clipboard-list', title: '部门监察问题', badge: '部门级', value: '13项', color: '#b45309' },
167
+    { icon: 'fas fa-microchip', title: '实时质控拦截', badge: '部门级', value: '347次', color: '#2563eb' },
168
+    { icon: 'fas fa-bug', title: '不安全事件', badge: '一级预警', value: '18起', color: '#dc2626' },
169
+    { icon: 'fas fa-shield-virus', title: '安保测试记录', badge: '部门级', value: '4项', color: '#e67e22' },
170
+    { icon: 'fas fa-comment-dots', title: '旅客服务投诉', badge: '服务响应', value: '11件', color: '#e67e22' },
171
+    { icon: 'fas fa-clipboard-check', title: '服务巡查', badge: '部门级', value: '5项', color: '#333' },
172
+    { icon: 'fas fa-graduation-cap', title: '培训及考试成绩', badge: '平均分数', value: '92.4分', color: '#333' },
173
+    { icon: 'fas fa-plane-departure', title: '航站楼', badge: '吞吐量', value: '2.8万', color: '#059669' },
174
+    { icon: 'fas fa-gift', title: '小额奖励', badge: '奖励次数', value: '156次', color: '#7c3aed' }
175
+])
176
+
177
+const newNames = [
178
+    "刘孟", "王苡衡", "周海涵", "何欣怡", "王崎",
179
+    "黄政", "刘韵儿", "汪安霖", "张诗敏", "张维", "陈凯琳"
180
+]
181
+const deptList = ["旅检一部", "旅检二部", "旅检三部"]
182
+const scores = [68, 92, 55, 96, 72, 88, 48, 94, 81, 63, 91]
183
+const coreRisksOrOutstandingAchievementsList = [
184
+    "违规操作2次+投诉1起,考试成绩62分",
185
+    "优秀服务案例,安保测试满分,无违规",
186
+    "安保测试未通过,不安全事件责任人",
187
+    "典型服务案例主导者,考试成绩98",
188
+    "质控拦截违规2次,考试成绩74分",
189
+    "考试成绩89分,服务巡查良好",
190
+    "严重不规范操作,安保测试未过",
191
+    "质控拦截贡献突出,考试成绩96",
192
+    "表现良好,无安全事故,考试86分",
193
+    "服务巡查扣分,投诉关联2件",
194
+    "临近预警线,服务巡查扣分1次"
195
+]
196
+
197
+const employeesData = ref([])
198
+
199
+for (let i = 0; i < newNames.length; i++) {
200
+    employeesData.value.push({
201
+        id: String(10021 + i),
202
+        name: newNames[i],
203
+        dept: deptList[i % deptList.length],
204
+        overallScore: scores[i % scores.length],
205
+        coreRisksOrOutstandingAchievements: coreRisksOrOutstandingAchievementsList[i % coreRisksOrOutstandingAchievementsList.length]
206
+    })
207
+}
208
+
209
+// 将组织架构数据转换为树形数据
210
+const transformCascadeData = (nodes) => {
211
+    if (!nodes) return []
212
+    return nodes.map(node => {
213
+        const label = node.nickName || node.deptName || node.name || node.label
214
+        const deptType = node.deptType || node.nodeType
215
+    //    console.log(node,"node")
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 ?? '')
225
+    //     }
226
+
227
+        const children = node.children && node.children.length > 0 ? transformCascadeData(node.children) : undefined
228
+
229
+        return {
230
+            label,
231
+            value:node.id,
232
+            deptType,
233
+            children
234
+        }
235
+    })
236
+}
237
+
238
+// 获取选中的部门或用户信息
239
+const getSelectedInfo = (selectedValue) => {
240
+    if (!selectedValue) return null
241
+
242
+    const findNodeByValue = (nodes, value) => {
243
+        for (const node of nodes) {
244
+            if (node.value === value) {
245
+                return node
246
+            }
247
+            if (node.children) {
248
+                const found = findNodeByValue(node.children, value)
249
+                if (found) return found
250
+            }
251
+        }
252
+        return null
253
+    }
254
+
255
+    return findNodeByValue(cascadeOptions.value, selectedValue)
256
+}
257
+
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
+        }
279
+    }
280
+    return ''
281
+}
282
+
283
+const filteredEmployees = computed(() => {
284
+    let result = employeesData.value.ledgerWarningDetailItemList
285
+
286
+
287
+
288
+    // 预警等级筛选
289
+    if (selectedAlertLevel.value) {
290
+        // 根据字典值判断筛选条件(红色预警 <75,优秀标杆 >=90,正常范围 75-89)
291
+        // 可以根据字典的 value 或 label 来判断
292
+        const alertItem = alert_level.value.find(item => item.value === selectedAlertLevel.value)
293
+        if (alertItem) {
294
+            const label = alertItem.label
295
+            if (label.includes('红色') || label.includes('预警')) {
296
+                result = result.filter(emp => emp.overallScore < 75)
297
+            } else if (label.includes('优秀') || label.includes('标杆')) {
298
+                result = result.filter(emp => emp.overallScore >= 90)
299
+            } else if (label.includes('正常')) {
300
+                result = result.filter(emp => emp.overallScore >= 75 && emp.overallScore < 90)
301
+            }
302
+        }
303
+    }
304
+
305
+
306
+
307
+    return result
308
+})
309
+
310
+const redAlertCount = computed(() => {
311
+    return employeesData.value?.redAlertNum || 0
312
+})
313
+
314
+const excellentCount = computed(() => {
315
+    return employeesData.value?.excellentBenchmarkNum || 0
316
+})
317
+
318
+const avgScore = computed(() => {
319
+    return employeesData.value?.averageComprehensiveScore || 0
320
+})
321
+
322
+const getRowClass = (score) => {
323
+    if (score < 75) return "employee-warning-row"
324
+    if (score >= 90) return "employee-excellent-row"
325
+    return ""
326
+}
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
+
334
+const setActiveRange = (range) => {
335
+    activeRange.value = range
336
+    // 清除自定义日期
337
+    if (range !== 'custom') {
338
+        startDate.value = null
339
+        endDate.value = null
340
+    }
341
+}
342
+
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
+    }
422
+}
423
+
424
+onMounted(async () => {
425
+    try {
426
+        const res = await getDeptUserTree()
427
+        if (res.data) {
428
+            cascadeOptions.value = transformCascadeData(res.data)
429
+        }
430
+    } catch (error) {
431
+        console.error('获取组织架构数据失败:', error)
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
+    }
450
+})
451
+
452
+// 监听路由参数变化,回显到级联选择器
453
+watch(() => route.query, (query) => {
454
+    const { id } = query
455
+    if (id) {
456
+        selectedOrg.value = id
457
+        selectedOrgLabel.value = findNodeLabelByValue(cascadeOptions.value, id)
458
+    } else {
459
+        selectedOrg.value = ''
460
+        selectedOrgLabel.value = ''
461
+    }
462
+    fetchWarningData()
463
+}, { immediate: true })
464
+</script>
465
+
466
+<style scoped>
467
+@keyframes subtlePulse {
468
+    0% {
469
+        opacity: 0.7;
470
+    }
471
+
472
+    50% {
473
+        opacity: 1;
474
+        background: #ffb3b3;
475
+    }
476
+
477
+    100% {
478
+        opacity: 0.7;
479
+    }
480
+}
481
+
482
+
483
+
484
+.dashboard {
485
+    max-width: 100%;
486
+    padding: 10px;
487
+}
488
+
489
+.header {
490
+    margin-bottom: 28px;
491
+    display: flex;
492
+    justify-content: space-between;
493
+    align-items: flex-end;
494
+    flex-wrap: wrap;
495
+    gap: 16px;
496
+}
497
+
498
+.title-section h1 {
499
+    font-size: 1.7rem;
500
+    font-weight: 700;
501
+    background: linear-gradient(135deg, #1e3c72, #2a5298);
502
+    -webkit-background-clip: text;
503
+    background-clip: text;
504
+    color: transparent;
505
+    letter-spacing: -0.3px;
506
+}
507
+
508
+.title-section p {
509
+    color: #475569;
510
+    margin-top: 6px;
511
+    font-size: 0.85rem;
512
+}
513
+
514
+.badge-group {
515
+    display: flex;
516
+    gap: 12px;
517
+}
518
+
519
+.alert-badge {
520
+    background: #fff1f0;
521
+    border-left: 5px solid #ef4444;
522
+    padding: 6px 16px;
523
+    border-radius: 40px;
524
+    font-weight: 600;
525
+    font-size: 0.85rem;
526
+}
527
+
528
+.alert-badge i {
529
+    color: #ef4444;
530
+    margin-right: 6px;
531
+}
532
+
533
+.filter-bar {
534
+    background: white;
535
+    border-radius: 60px;
536
+    padding: 8px 20px;
537
+    margin-bottom: 28px;
538
+    display: flex;
539
+    flex-wrap: wrap;
540
+    align-items: center;
541
+    justify-content: space-between;
542
+    gap: 16px;
543
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02), 0 1px 2px rgba(0, 0, 0, 0.03);
544
+    border: 1px solid #eef2f6;
545
+}
546
+
547
+.time-range {
548
+    display: flex;
549
+    gap: 8px;
550
+    align-items: center;
551
+    flex-wrap: wrap;
552
+}
553
+
554
+.time-btn {
555
+    background: #f8fafc;
556
+    border: 1px solid #e2e8f0;
557
+    padding: 6px 18px;
558
+    border-radius: 40px;
559
+    font-size: 0.8rem;
560
+    font-weight: 500;
561
+    cursor: pointer;
562
+    transition: all 0.2s;
563
+    color: #1e293b;
564
+}
565
+
566
+.time-btn.active {
567
+    background: #2563eb;
568
+    border-color: #2563eb;
569
+    color: white;
570
+}
571
+
572
+.time-btn:hover {
573
+    background: #e2e8f0;
574
+}
575
+
576
+.custom-date {
577
+    display: flex;
578
+    align-items: center;
579
+    gap: 8px;
580
+}
581
+
582
+.clear-btn {
583
+    background: transparent;
584
+    border: 1px solid #e2e8f0;
585
+    padding: 4px 8px;
586
+    border-radius: 40px;
587
+    cursor: pointer;
588
+    color: #64748b;
589
+}
590
+
591
+.clear-btn:hover {
592
+    background: #f1f5f9;
593
+}
594
+
595
+.custom-date :deep(.el-date-picker) {
596
+    background: transparent;
597
+}
598
+
599
+.custom-date :deep(.el-input__wrapper) {
600
+    background: transparent;
601
+
602
+
603
+}
604
+
605
+.filter-group {
606
+    display: flex;
607
+    gap: 12px;
608
+    align-items: center;
609
+}
610
+
611
+.search-wrapper {
612
+    display: flex;
613
+    align-items: center;
614
+    gap: 8px;
615
+    background: #f8fafc;
616
+    padding: 4px 12px;
617
+    border-radius: 40px;
618
+    border: 1px solid #e2e8f0;
619
+}
620
+
621
+.search-btn {
622
+    background: #2563eb;
623
+    border: none;
624
+    color: white;
625
+    padding: 4px 14px;
626
+    border-radius: 30px;
627
+    font-size: 0.75rem;
628
+    font-weight: 500;
629
+    cursor: pointer;
630
+    transition: 0.2s;
631
+}
632
+
633
+.search-btn:hover {
634
+    background: #1d4ed8;
635
+}
636
+
637
+.cards-container {
638
+    width: 100%;
639
+    margin-bottom: 36px;
640
+    overflow-x: auto;
641
+}
642
+
643
+.cards-grid {
644
+    display: flex;
645
+
646
+    gap: 16px;
647
+    flex-wrap: nowrap;
648
+    min-width: max-content;
649
+}
650
+
651
+.card {
652
+    background: #ffffff;
653
+    border-radius: 20px;
654
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03), 0 1px 2px rgba(0, 0, 0, 0.05);
655
+    transition: all 0.2s;
656
+    border: 1px solid #eef2f6;
657
+    padding: 1rem 0.5rem;
658
+    display: flex;
659
+    flex-direction: column;
660
+    text-align: center;
661
+    min-width: 280px;
662
+    flex-shrink: 0;
663
+}
664
+
665
+.card:hover {
666
+    transform: translateY(-3px);
667
+    box-shadow: 0 12px 20px -10px rgba(0, 0, 0, 0.1);
668
+    border-color: #cbd5e1;
669
+}
670
+
671
+.card-header {
672
+    display: flex;
673
+    flex-direction: column;
674
+    align-items: center;
675
+    gap: 6px;
676
+    border-bottom: none;
677
+    padding-bottom: 0;
678
+    margin-bottom: 12px;
679
+}
680
+
681
+.card-header i {
682
+    font-size: 1.5rem;
683
+    color: #2563eb;
684
+}
685
+
686
+.card-header h3 {
687
+    font-size: 0.85rem;
688
+    font-weight: 700;
689
+    margin: 0;
690
+    white-space: nowrap;
691
+}
692
+
693
+.card-badge {
694
+    font-size: 0.6rem;
695
+    background: #f1f5f9;
696
+    padding: 2px 8px;
697
+    border-radius: 30px;
698
+    margin-top: 4px;
699
+    display: inline-block;
700
+}
701
+
702
+.value-large {
703
+    font-size: 1.8rem;
704
+    font-weight: 800;
705
+    line-height: 1.2;
706
+    margin: 8px 0 4px 0;
707
+}
708
+
709
+.employee-section {
710
+    margin-top: 20px;
711
+    width: 100%;
712
+}
713
+
714
+.section-title {
715
+    font-size: 1.2rem;
716
+    font-weight: 700;
717
+    margin-bottom: 1rem;
718
+    display: flex;
719
+    align-items: center;
720
+    gap: 8px;
721
+    border-left: 5px solid #ef4444;
722
+    padding-left: 14px;
723
+}
724
+
725
+.employee-card {
726
+    background: #ffffff;
727
+    border-radius: 24px;
728
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03), 0 1px 2px rgba(0, 0, 0, 0.05);
729
+    border: 1px solid #eef2f6;
730
+    padding: 1rem 1.5rem;
731
+    width: 100%;
732
+}
733
+
734
+.data-table {
735
+    width: 100%;
736
+    border-collapse: collapse;
737
+    font-size: 0.8rem;
738
+}
739
+
740
+.data-table th {
741
+    text-align: left;
742
+    padding: 10px 6px 8px 0;
743
+    font-weight: 600;
744
+    color: #334155;
745
+    border-bottom: 2px solid #e2e8f0;
746
+}
747
+
748
+.data-table td {
749
+    padding: 8px 6px 8px 0;
750
+    border-bottom: 1px solid #f1f5f9;
751
+    vertical-align: middle;
752
+}
753
+
754
+.employee-warning-row {
755
+    background-color: #fff5f5;
756
+    border-left: 4px solid #ef4444;
757
+}
758
+
759
+.employee-excellent-row {
760
+    background-color: #f0fdf4;
761
+    border-left: 4px solid #22c55e;
762
+}
763
+
764
+.score-danger {
765
+    font-weight: 800;
766
+    color: #dc2626;
767
+    background: #fee2e2;
768
+    padding: 2px 8px;
769
+    border-radius: 30px;
770
+    display: inline-block;
771
+    font-size: 0.8rem;
772
+}
773
+
774
+.score-excellent {
775
+    font-weight: 800;
776
+    color: #15803d;
777
+    background: #dcfce7;
778
+    padding: 2px 8px;
779
+    border-radius: 30px;
780
+    display: inline-block;
781
+    font-size: 0.8rem;
782
+}
783
+
784
+.warning-summary {
785
+    background: #fff9f0;
786
+    border-radius: 18px;
787
+    padding: 12px 20px;
788
+    margin-top: 18px;
789
+    display: flex;
790
+    gap: 28px;
791
+    flex-wrap: wrap;
792
+    font-weight: 500;
793
+    font-size: 0.85rem;
794
+}
795
+
796
+footer {
797
+    text-align: center;
798
+    margin-top: 32px;
799
+    font-size: 0.7rem;
800
+    color: #7e8b9c;
801
+    border-top: 1px solid #e2edf7;
802
+    padding-top: 18px;
803
+}
804
+
805
+@media (max-width: 1200px) {
806
+    .cards-grid {
807
+        grid-template-columns: repeat(4, 1fr);
808
+        gap: 14px;
809
+    }
810
+}
811
+
812
+@media (max-width: 800px) {
813
+    .cards-grid {
814
+        grid-template-columns: repeat(2, 1fr);
815
+    }
816
+
817
+    body {
818
+        padding: 20px;
819
+    }
820
+
821
+    .filter-bar {
822
+        border-radius: 24px;
823
+        flex-direction: column;
824
+        align-items: stretch;
825
+    }
826
+
827
+    .org-search {
828
+        justify-content: space-between;
829
+    }
830
+}
831
+
832
+@media (max-width: 550px) {
833
+    .cards-grid {
834
+        grid-template-columns: 1fr;
835
+    }
836
+}
837
+</style>