huoyi недель назад: 2
Родитель
Сommit
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
 export function importSupervisionProblem(data) {
23
 export function importSupervisionProblem(data) {
21
   return request({ url: '/ledger/import/supervisionProblem', method: 'post', data })
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
     params: params
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
     children: [
92
     children: [
93
       {
93
       {
94
         path: '/index',
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
       //   path: '/dashboard-work',
100
       //   path: '/dashboard-work',

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

@@ -142,6 +142,7 @@ import {
142
   importTerminalBonus,
142
   importTerminalBonus,
143
   importExamScore,
143
   importExamScore,
144
   importRewardApproval,
144
   importRewardApproval,
145
+  importLeaveSpecial,
145
   importDailyTraining,
146
   importDailyTraining,
146
   importLeaderDuty,
147
   importLeaderDuty,
147
   importHealthSoldier,
148
   importHealthSoldier,
@@ -251,15 +252,15 @@ const importItems = reactive([
251
     loading: false,
252
     loading: false,
252
     lastResult: null
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
     key: 'realtimeInterception',
265
     key: 'realtimeInterception',
265
     title: '实时质控拦截记录',
266
     title: '实时质控拦截记录',
@@ -315,6 +316,15 @@ const importItems = reactive([
315
     lastResult: null
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
     key: 'seizureStats',
328
     key: 'seizureStats',
319
     title: '2026查获违规品统计',
329
     title: '2026查获违规品统计',
320
     desc: '查获违规品统计数据',
330
     desc: '查获违规品统计数据',
@@ -350,51 +360,52 @@ const importItems = reactive([
350
     loading: false,
360
     loading: false,
351
     lastResult: null
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
 function beforeUpload(file) {
411
 function beforeUpload(file) {

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

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

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

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

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

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

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

@@ -1,7 +1,7 @@
1
 <template>
1
 <template>
2
   <div class="group-profile-page">
2
   <div class="group-profile-page">
3
     <Page title="安检人事管理可视化大屏" :tabs="['能力画像', '运行数据']" @tab-change="handleTabChange">
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
         :deptType="'BRIGADE'" />
5
         :deptType="'BRIGADE'" />
6
       <PentagonGroup :items="pentagonItems" />
6
       <PentagonGroup :items="pentagonItems" />
7
       <Profile v-if="activeTab === 0" :query-params="queryParams" />
7
       <Profile v-if="activeTab === 0" :query-params="queryParams" />

+ 35 - 10
src/views/portraitManagement/employeeProfile/index.vue

@@ -74,8 +74,8 @@
74
                   </div>
74
                   </div>
75
                   <div class="info-item-content">
75
                   <div class="info-item-content">
76
                     <div class="info-item-label">标签:</div>
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
                     </div>
79
                     </div>
80
                   </div>
80
                   </div>
81
                 </div>
81
                 </div>
@@ -110,7 +110,6 @@
110
                 <span v-if="portrait.entryDate">
110
                 <span v-if="portrait.entryDate">
111
                   {{ formatWorkDate(portrait.entryDate)  }}入职 | 司龄{{ portrait.companyYears != null ? portrait.companyYears : '-' }}年 | 开机年限{{ portrait.xrayOperatorYears != null ? portrait.xrayOperatorYears : '-'
111
                   {{ formatWorkDate(portrait.entryDate)  }}入职 | 司龄{{ portrait.companyYears != null ? portrait.companyYears : '-' }}年 | 开机年限{{ portrait.xrayOperatorYears != null ? portrait.xrayOperatorYears : '-'
112
 
112
 
113
-
114
                   }}年 | 现任职{{ portrait.roleNames || '-' }}
113
                   }}年 | 现任职{{ portrait.roleNames || '-' }}
115
                 </span>
114
                 </span>
116
                 <span v-else>暂无数据</span>
115
                 <span v-else>暂无数据</span>
@@ -130,7 +129,7 @@
130
           </div>
129
           </div>
131
           <div class="content-bottom-center">
130
           <div class="content-bottom-center">
132
             <Card title="个人能力">
131
             <Card title="个人能力">
133
-              <div ref="abilityChart" class="chart-box" />
132
+              <div ref="abilityChart" class="chart-box" @click="goToWarningPage" />
134
             </Card>
133
             </Card>
135
           </div>
134
           </div>
136
           <div class="content-bottom-right">
135
           <div class="content-bottom-right">
@@ -224,11 +223,14 @@ import Card from '../components/card.vue'
224
 import SearchBar from '../components/SearchBar.vue'
223
 import SearchBar from '../components/SearchBar.vue'
225
 import { getEmployeePortrait } from '@/api/score/index'
224
 import { getEmployeePortrait } from '@/api/score/index'
226
 import { countTagScore } from '@/api/portraitManagement/portraitManagement'
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
 import { useDict } from '@/utils/dict'
227
 import { useDict } from '@/utils/dict'
229
 import { useECharts } from '@/hooks/useEcharts'
228
 import { useECharts } from '@/hooks/useEcharts'
230
 import useUserStore from '@/store/modules/user'
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
 const { sys_user_schooling } = useDict('sys_user_schooling')
235
 const { sys_user_schooling } = useDict('sys_user_schooling')
234
 const userStore = useUserStore()
236
 const userStore = useUserStore()
@@ -238,6 +240,7 @@ const portrait = ref({ dimensions: [] })
238
 const abilityChart = ref(null)
240
 const abilityChart = ref(null)
239
 const activeDimName = ref(null)
241
 const activeDimName = ref(null)
240
 const tagScoreData = ref(null)
242
 const tagScoreData = ref(null)
243
+const currentQuery = ref(null)
241
 
244
 
242
 const scoreDetails = computed(() => portrait.value?.scoreDetails || [])
245
 const scoreDetails = computed(() => portrait.value?.scoreDetails || [])
243
 
246
 
@@ -331,9 +334,22 @@ const invokerEmployeePortrait = (query) => {
331
 
334
 
332
 const searchHandler = (query) => {
335
 const searchHandler = (query) => {
333
   visible.value = false
336
   visible.value = false
337
+  currentQuery.value = query
334
   invokerEmployeePortrait(query)
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
 const radarData = reactive({
353
 const radarData = reactive({
338
   grounp: [],
354
   grounp: [],
339
   data: []
355
   data: []
@@ -424,7 +440,7 @@ let pisition = {
424
 }
440
 }
425
 const tipRef = ref(null)
441
 const tipRef = ref(null)
426
 
442
 
427
-useECharts(abilityChart, setRadarOptions).bindEvent('mousemove', (e) => {
443
+radarChartInstance = useECharts(abilityChart, setRadarOptions).bindEvent('mousemove', (e) => {
428
   if (!Array.isArray(portrait.value.dimensions) || !portrait.value.dimensions.length) return
444
   if (!Array.isArray(portrait.value.dimensions) || !portrait.value.dimensions.length) return
429
   const rect = abilityChart.value.getBoundingClientRect()
445
   const rect = abilityChart.value.getBoundingClientRect()
430
   if (!tipRef.value) return
446
   if (!tipRef.value) return
@@ -455,11 +471,19 @@ useECharts(abilityChart, setRadarOptions).bindEvent('mousemove', (e) => {
455
   if (tipRef.value) tipRef.value.style.display = 'none'
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
 onMounted(() => {
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
 </script>
488
 </script>
465
 
489
 
@@ -582,6 +606,7 @@ onMounted(() => {
582
               display: flex;
606
               display: flex;
583
               align-items: center;
607
               align-items: center;
584
               flex-wrap: wrap;
608
               flex-wrap: wrap;
609
+              gap: 2px;
585
             }
610
             }
586
             .info-item-tag {
611
             .info-item-tag {
587
               font-size: 14px;
612
               font-size: 14px;

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

@@ -69,11 +69,7 @@ const skillData = ref([])
69
 
69
 
70
 const operateData = ref([])
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
 const fetchTeamMembers = async (params) => {
73
 const fetchTeamMembers = async (params) => {
78
   try {
74
   try {
79
     const res = await getDeptMembers(params)
75
     const res = await getDeptMembers(params)

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

@@ -426,15 +426,24 @@ const initDailySeizedArea = () => {
426
   }
426
   }
427
   const rawData = countSeizureSingleQuantityData.value
427
   const rawData = countSeizureSingleQuantityData.value
428
   if (!rawData || rawData.length === 0) return
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
   const groupMap = {}
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
   const countryColors = ['#4da6ff', '#7eff7e', '#bd03fb', '#ffd93d', '#ff6b6b', '#00f5ff', '#ffa500', '#71B138']
447
   const countryColors = ['#4da6ff', '#7eff7e', '#bd03fb', '#ffd93d', '#ff6b6b', '#00f5ff', '#ffa500', '#71B138']
439
   const series = groupNames.map((name, idx) => {
448
   const series = groupNames.map((name, idx) => {
440
     const color = countryColors[idx % countryColors.length]
449
     const color = countryColors[idx % countryColors.length]

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

@@ -188,7 +188,7 @@
188
           <el-col :span="12">
188
           <el-col :span="12">
189
             <el-form-item label="部门名称" prop="deptId" :rules="rules.deptId">
189
             <el-form-item label="部门名称" prop="deptId" :rules="rules.deptId">
190
               <el-select v-model="form.deptId" placeholder="请选择部门" style="width:100%" clearable filterable
190
               <el-select v-model="form.deptId" placeholder="请选择部门" style="width:100%" clearable filterable
191
-                @change="handleDeptChange">
191
+                 @change="handleBrigadeChange">
192
                 <el-option v-for="d in deptOptions" :key="d.deptId" :label="d.deptName" :value="d.deptId" />
192
                 <el-option v-for="d in deptOptions" :key="d.deptId" :label="d.deptName" :value="d.deptId" />
193
               </el-select>
193
               </el-select>
194
             </el-form-item>
194
             </el-form-item>
@@ -196,7 +196,7 @@
196
           <el-col :span="12">
196
           <el-col :span="12">
197
             <el-form-item label="队室/班组" prop="teamId" :rules="rules.teamId">
197
             <el-form-item label="队室/班组" prop="teamId" :rules="rules.teamId">
198
               <el-select v-model="form.teamId" placeholder="请选择队室/班组" style="width:100%" clearable filterable
198
               <el-select v-model="form.teamId" placeholder="请选择队室/班组" style="width:100%" clearable filterable
199
-                :disabled="form.org < 2" @change="handleTeamChange">
199
+                 @change="handleDepartmentChange">
200
                 <el-option v-for="t in teamOptions" :key="t.id" :label="t.label" :value="t.id" />
200
                 <el-option v-for="t in teamOptions" :key="t.id" :label="t.label" :value="t.id" />
201
               </el-select>
201
               </el-select>
202
             </el-form-item>
202
             </el-form-item>
@@ -204,7 +204,7 @@
204
           <el-col :span="12">
204
           <el-col :span="12">
205
             <el-form-item label="通道/小组" prop="groupId" :rules="rules.groupId">
205
             <el-form-item label="通道/小组" prop="groupId" :rules="rules.groupId">
206
               <el-select v-model="form.groupId" placeholder="请选择通道/小组" style="width:100%" clearable filterable
206
               <el-select v-model="form.groupId" placeholder="请选择通道/小组" style="width:100%" clearable filterable
207
-                :disabled="form.org < 3" @change="handleGroupChange">
207
+                @change="handleGroupChange">
208
                 <el-option v-for="g in groupOptions" :key="g.id" :label="g.label" :value="g.id" />
208
                 <el-option v-for="g in groupOptions" :key="g.id" :label="g.label" :value="g.id" />
209
               </el-select>
209
               </el-select>
210
             </el-form-item>
210
             </el-form-item>
@@ -212,7 +212,7 @@
212
           <el-col :span="12">
212
           <el-col :span="12">
213
             <el-form-item label="责任人" prop="personId" :rules="rules.personId">
213
             <el-form-item label="责任人" prop="personId" :rules="rules.personId">
214
               <el-select v-model="form.personId" placeholder="请选择责任人" style="width:100%" clearable filterable
214
               <el-select v-model="form.personId" placeholder="请选择责任人" style="width:100%" clearable filterable
215
-                :disabled="form.org < 4">
215
+                 @change="handlePersonChange">
216
                 <el-option v-for="p in personOptions" :key="p.userId" :label="p.nickName" :value="p.userId" />
216
                 <el-option v-for="p in personOptions" :key="p.userId" :label="p.nickName" :value="p.userId" />
217
               </el-select>
217
               </el-select>
218
             </el-form-item>
218
             </el-form-item>
@@ -250,7 +250,7 @@
250
 <script setup>
250
 <script setup>
251
 import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
251
 import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
252
 import { ElMessage, ElMessageBox } from 'element-plus'
252
 import { ElMessage, ElMessageBox } from 'element-plus'
253
-import { listUser } from '@/api/system/user'
253
+import { listUser, getDeptOrgInfo, getUserOrgInfo } from '@/api/system/user'
254
 import {
254
 import {
255
   listScoreEvent, addScoreEvent, updateScoreEvent, delScoreEvent,
255
   listScoreEvent, addScoreEvent, updateScoreEvent, delScoreEvent,
256
   exportScoreEvent, importScoreEvent
256
   exportScoreEvent, importScoreEvent
@@ -289,13 +289,12 @@ const rules = computed(() => {
289
   return {
289
   return {
290
     org: [{ required: true, message: '请选择配分层级', trigger: 'submit' }],
290
     org: [{ required: true, message: '请选择配分层级', trigger: 'submit' }],
291
     dimensionId: [{ required: true, message: '请选择维度', trigger: 'submit' }],
291
     dimensionId: [{ required: true, message: '请选择维度', trigger: 'submit' }],
292
-    personName: [{ required: true, message: '请输入责任人', trigger: 'submit' }],
293
     eventTime: [{ required: true, message: '请选择事件时间', trigger: 'submit' }],
292
     eventTime: [{ required: true, message: '请选择事件时间', trigger: 'submit' }],
294
     scoreValue: [{ required: true, message: '请输入分值', trigger: 'submit' }],
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
   indicatorTree.value = []
320
   indicatorTree.value = []
322
   level2Options.value = []; level3Options.value = []; level4Options.value = []
321
   level2Options.value = []; level3Options.value = []; level4Options.value = []
323
   teamOptions.value = []; groupOptions.value = []; personOptions.value = []
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
   dimensionOptions.value = r.data || []
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
 async function loadDepts() {
342
 async function loadDepts() {
329
   const r = await listDept()
343
   const r = await listDept()
330
   deptOptions.value = (r.data || []).filter(d => d.deptType === 'BRIGADE')
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
   const record = row?.id ? row : list.value.find(r => r.id === ids.value[0])
456
   const record = row?.id ? row : list.value.find(r => r.id === ids.value[0])
441
   if (record) {
457
   if (record) {
442
     Object.assign(form, record)
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
     if (record.dimensionId) onDimensionChange(record.dimensionId, true)
479
     if (record.dimensionId) onDimensionChange(record.dimensionId, true)
444
     if (record.level2Id) onLevel2Change(record.level2Id, true)
480
     if (record.level2Id) onLevel2Change(record.level2Id, true)
445
     if (record.level3Id) onLevel3Change(record.level3Id, true)
481
     if (record.level3Id) onLevel3Change(record.level3Id, true)
446
     if (record.level4Id) onLevel4Change(record.level4Id, true)
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
   dialogTitle.value = '修改配分事项'; dialogVisible.value = true
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
 getList()
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
 </script>
321
 </script>

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

@@ -90,6 +90,18 @@
90
               </el-select>
90
               </el-select>
91
             </el-form-item>
91
             </el-form-item>
92
           </el-col>
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
         </el-row>
105
         </el-row>
94
       </el-card>
106
       </el-card>
95
       <el-card header="状态管理信息" style="margin-bottom: 10px;">
107
       <el-card header="状态管理信息" style="margin-bottom: 10px;">
@@ -426,6 +438,42 @@ defineProps({
426
 const userRef = ref(null)
438
 const userRef = ref(null)
427
 const form = defineModel('form', ref({}))
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
 const {
478
 const {
431
   sys_user_political_status,
479
   sys_user_political_status,
@@ -541,5 +589,13 @@ defineExpose({
541
       color: #000;
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
 </style>
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>