7 Incheckningar 1b9ec05127 ... cc1abebf97

Upphovsman SHA1 Meddelande Datum
  huoyi cc1abebf97 fix(employeeProfile): 修正X射线操作岗位显示逻辑 2 dagar sedan
  huoyi a68d6f6667 refactor(warningManage): 重构预警相关接口及页面逻辑 2 dagar sedan
  huoyi 3a89beab45 feat(organProfile): 优化画像页图表布局及排行榜样式 2 dagar sedan
  huoyi a31ceb834e feat(portraitManagement): 新增器官画像模块及白卡组件 2 dagar sedan
  huoyi 79f56a109c feat(user): 添加X射线操作员开始时间字段 5 dagar sedan
  huoyi 0adc281ad0 fix(employeeProfile): 修复附加分显示异常问题 5 dagar sedan
  huoyi cb1f0ce952 refactor(profile): 优化个人画像页面布局及样式,调整组织架构命名 5 dagar sedan

+ 5 - 0
src/api/warningManage/redLineWarning.js

@@ -0,0 +1,5 @@
1
+import request from '@/utils/request'
2
+//红线指标预警
3
+export function getRedLineWarningPageData(data) {
4
+    return request({ url: '/ledger/warning/ledgerReduceDetail', method: 'post', data })
5
+}

src/api/warningPage/warningPage.js → src/api/warningManage/warningPage.js


+ 7 - 20
src/views/assistant/components/dutyOrganization.vue

@@ -558,7 +558,7 @@ const attendanceBarOptions = {
558 558
     }
559 559
   },
560 560
   legend: {
561
-    data: ['安检一大队', '安检二大队', '安检三大队', '安检综合大队', '全站'],
561
+    data: ['旅检一部', '旅检二部', '旅检三部', '全站'],
562 562
     top: 0,
563 563
     show: true,
564 564
   },
@@ -611,7 +611,7 @@ const attendanceBarOptions = {
611 611
       data: []
612 612
     },
613 613
     {
614
-      name: '安检一大队',
614
+      name: '旅检一部',
615 615
       type: 'bar',
616 616
       barWidth: '10%',
617 617
       itemStyle: {
@@ -625,7 +625,7 @@ const attendanceBarOptions = {
625 625
       data: []
626 626
     },
627 627
     {
628
-      name: '安检二大队',
628
+      name: '旅检二部',
629 629
       type: 'bar',
630 630
       barWidth: '10%',
631 631
       itemStyle: {
@@ -639,7 +639,7 @@ const attendanceBarOptions = {
639 639
       data: []
640 640
     },
641 641
     {
642
-      name: '安检三大队',
642
+      name: '旅检三部',
643 643
       type: 'bar',
644 644
       barWidth: '10%',
645 645
       itemStyle: {
@@ -652,20 +652,7 @@ const attendanceBarOptions = {
652 652
       },
653 653
       data: []
654 654
     },
655
-    {
656
-      name: '安检综合大队',
657
-      type: 'bar',
658
-      barWidth: '10%',
659
-      itemStyle: {
660
-        color: '#FFD166'
661
-      },
662
-      label: {
663
-        show: true,
664
-        position: 'top',
665
-        formatter: '{c}人'
666
-      },
667
-      data: []
668
-    }
655
+    
669 656
   ]
670 657
 }
671 658
 // 出勤人次柱状图配置
@@ -902,7 +889,7 @@ const updateAttendanceBarChart = () => {
902 889
     const dept1Data = trendList.map(item => item.data1 || 0)
903 890
     const dept2Data = trendList.map(item => item.data2 || 0)
904 891
     const dept3Data = trendList.map(item => item.data3 || 0)
905
-    const dept4Data = trendList.map(item => item.data4 || 0)
892
+    // const dept4Data = trendList.map(item => item.data4 || 0)
906 893
 
907 894
     // 更新图表配置
908 895
     attendanceBarOptions.xAxis.data = xAxisData
@@ -910,7 +897,7 @@ const updateAttendanceBarChart = () => {
910 897
     attendanceBarOptions.series[1].data = dept1Data
911 898
     attendanceBarOptions.series[2].data = dept2Data
912 899
     attendanceBarOptions.series[3].data = dept3Data
913
-    attendanceBarOptions.series[4].data = dept4Data
900
+    // attendanceBarOptions.series[4].data = dept4Data
914 901
     attendanceBarOptions.legend.show = !!isStationType.value
915 902
 
916 903
     // 重新设置图表选项

+ 3 - 3
src/views/assistant/components/performanceAnalysis.vue

@@ -68,7 +68,7 @@
68 68
         <!-- 大队分析 -->
69 69
         <div class="panel-item" :class="{ 'loading-overlay': brigadeLoading }">
70 70
           <div class="panel-header">
71
-            <h3>大队</h3>
71
+            <h3>部门</h3>
72 72
           </div>
73 73
           <!-- 折线图 -->
74 74
           <div class="chart-container">
@@ -91,7 +91,7 @@
91 91
         <!-- 科室分析 -->
92 92
         <div class="panel-item" :class="{ 'loading-overlay': departmentLoading }">
93 93
           <div class="panel-header">
94
-            <h3>主管</h3>
94
+            <h3>班组</h3>
95 95
           </div>
96 96
           <!-- 折线图 -->
97 97
           <div class="chart-container">
@@ -117,7 +117,7 @@
117 117
         <!-- 班组分析 -->
118 118
         <div class="panel-item" :class="{ 'loading-overlay': teamLoading }">
119 119
           <div class="panel-header">
120
-            <h3>组</h3>
120
+            <h3>组</h3>
121 121
             <div class="panel-tabs">
122 122
               <div class="button-group small">
123 123
                 <div v-for="option in tabOptions" :key="option.value" class="custom-button"

+ 4 - 4
src/views/assistant/components/riskHazard.vue

@@ -86,7 +86,7 @@
86 86
           <div class="table-container">
87 87
             <el-table :data="captureRankData" style="width: 100%" size="small">
88 88
               <el-table-column prop="rank" label="排名" align="center" />
89
-              <el-table-column prop="department" label="大队" />
89
+              <el-table-column prop="department" label="部门" />
90 90
               <el-table-column prop="percentage" label="占比" align="center">
91 91
                 <template #default="{ row }">
92 92
                   <span>{{ row.percentage }}%</span>
@@ -397,7 +397,7 @@ const concealmentPositionStatsDescription = computed(() => {
397 397
 const departmentRankingDescription = computed(() => {
398 398
 
399 399
   if (!departmentRankingData.value || !Array.isArray(departmentRankingData.value)) {
400
-    return 'XXXX是违禁品查获的主力大队,查获违禁物品数量为XX,占比XX%。'
400
+    return 'XXXX是违禁品查获的主力部门,查获违禁物品数量为XX,占比XX%。'
401 401
   }
402 402
 
403 403
   const rankingData = departmentRankingData.value
@@ -408,7 +408,7 @@ const departmentRankingDescription = computed(() => {
408 408
   // 2. 获取查获量最高的大队
409 409
   const topDepartment = sortedData[0] || { departmentName: 'XXXX', seizureCount: 234 }
410 410
 
411
-  const str = isStationType.value ? '大队' : isBrigadeType.value ? '主管' : isDepartmentType.value ? '班组' : isTeamsType.value ? '成员' : ''
411
+  const str = isStationType.value ? '部门' : isBrigadeType.value ? '班组' : isDepartmentType.value ? '小组' : isTeamsType.value ? '成员' : ''
412 412
   // 5. 生成描述文字
413 413
   return `${topDepartment.brigadeName || 'XXXX'}是违禁品查获的主力${str},查获违禁物品数量为${topDepartment.seizureCount || 'XX'},占比${topDepartment.currentRatio}%。`
414 414
 })
@@ -962,7 +962,7 @@ const updateDepartmentRankingTable = () => {
962 962
     // 转换数据格式
963 963
     const formattedData = tableData.map((item, index) => ({
964 964
       rank: index + 1,
965
-      department: item.brigadeName || '未知大队',
965
+      department: item.brigadeName || '未知部门',
966 966
       percentage: item.currentRatio || 0,
967 967
       count: item.seizureCount || 0
968 968
     }))

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 4 - 4
src/views/assistant/components/useReports.vue


+ 2 - 2
src/views/ledger/import/index.vue

@@ -363,8 +363,8 @@ const importItems = reactive([
363 363
   },
364 364
   {
365 365
     key: 'qualificationLevel',
366
-    title: '职业资格等级',
367
-    desc: '职业资格等级流程记录',
366
+    title: '职业资格证书获取时间',
367
+    desc: '职业资格证书获取时间流程记录',
368 368
     icon: 'Medal',
369 369
     api: importQualificationLevel,
370 370
     loading: false,

+ 3 - 8
src/views/portraitManagement/components/ProfileRadar.vue

@@ -68,19 +68,13 @@ const computedRadarData = computed(() => {
68 68
 
69 69
 const computedIndicators = computed(() => {
70 70
   const data = computedRadarData.value
71
-  const allScores = data.map(item => item.finalScore || 0)
72
-  const maxValue = allScores.length > 0 ? Math.max(...allScores) : 100
73 71
   return data.map(item => ({
74 72
     name: item.name + '\n\n' + (item.finalScore || 0),
75
-    max: Math.max(maxValue, 1)
73
+    max: 100
76 74
   }))
77 75
 })
78 76
 
79
-const computedMaxScore = computed(() => {
80
-  const data = computedRadarData.value
81
-  const allScores = data.map(item => item.finalScore || 0)
82
-  return allScores.length > 0 ? Math.max(...allScores) : 100
83
-})
77
+const computedMaxScore = computed(() => 100)
84 78
 
85 79
 const computedSeries = computed(() => {
86 80
   return computedRadarData.value.map(item => item.finalScore || 0)
@@ -90,6 +84,7 @@ const radarChartRef = ref(null)
90 84
 let radarChart = null
91 85
 
92 86
 const updateRadarChart = () => {
87
+  
93 88
   if (!radarChartRef.value) return
94 89
 
95 90
   if (!radarChart) {

+ 58 - 0
src/views/portraitManagement/components/whiteCard.vue

@@ -0,0 +1,58 @@
1
+<template>
2
+  <div class="white-card" :style="{ background: bgColor }">
3
+    <div class="card-title" v-if="title">
4
+      <span class="title-dot"></span>
5
+      <span class="title-text">{{ title }}</span>
6
+    </div>
7
+    <div class="card-body">
8
+      <slot></slot>
9
+    </div>
10
+  </div>
11
+</template>
12
+
13
+<script setup>
14
+defineProps({
15
+  title: {
16
+    type: String,
17
+    default: ''
18
+  },
19
+  bgColor: {
20
+    type: String,
21
+    default: '#ffffff'
22
+  }
23
+})
24
+</script>
25
+
26
+<style lang="scss" scoped>
27
+.white-card {
28
+  border-radius: 12px;
29
+  padding: 12px 16px;
30
+  display: flex;
31
+  flex-direction: column;
32
+
33
+  .card-title {
34
+    display: flex;
35
+    align-items: center;
36
+    gap: 8px;
37
+    margin-bottom: 8px;
38
+
39
+    .title-dot {
40
+      width: 8px;
41
+      height: 8px;
42
+      border-radius: 50%;
43
+      background: #3b82f6;
44
+      flex-shrink: 0;
45
+    }
46
+
47
+    .title-text {
48
+      font-size: 14px;
49
+      font-weight: bold;
50
+      color: #1e293b;
51
+    }
52
+  }
53
+
54
+  .card-body {
55
+    flex: 1;
56
+  }
57
+}
58
+</style>

+ 300 - 230
src/views/portraitManagement/employeeProfile/index.vue

@@ -4,194 +4,198 @@
4 4
       <div class="content-search">
5 5
         <SearchBar v-model:visible="visible" @search="searchHandler" :deptType="'user'" />
6 6
       </div>
7
-      
8
-        <div v-show="portrait.personName" class="content-info">
9
-          <Card title="个人基本信息">
10
-            <div class="userInfo">
11
-              <div class="userInfo-name">
12
-                <div class="avatar-area">
13
-                  <img v-if="portrait.avatar" :src="portrait.avatar" class="avatar" alt="头像" />
14
-                  <div v-else class="avatar">{{ portrait.personName?.charAt(0) }}</div>
7
+
8
+      <div v-show="portrait.personName" class="content-info">
9
+        <Card title="个人基本信息">
10
+          <div class="userInfo">
11
+            <div class="userInfo-name">
12
+              <div class="avatar-area">
13
+                <img v-if="portrait.avatar" :src="portrait.avatar" class="avatar" alt="头像" />
14
+                <div v-else class="avatar">{{ portrait.personName?.charAt(0) }}</div>
15
+              </div>
16
+              <div class="basic-name">
17
+                <div class="basic-label">姓名:</div>
18
+                <div class="basic-value">{{ portrait.personName }}</div>
19
+              </div>
20
+            </div>
21
+            <div class="userInfo-info">
22
+              <div class="info-item">
23
+                <div class="info-item-icon">
24
+                  <img src="/src/assets/dataBigScreen/img01.png" alt="" />
15 25
                 </div>
16
-                <div class="basic-name">
17
-                  <div class="basic-label">姓名:</div>
18
-                  <div class="basic-value">{{ portrait.personName }}</div>
26
+                <div class="info-item-content">
27
+                  <div class="info-item-label">所属部门及队室:</div>
28
+                  <div class="info-item-value">{{ portrait.deptPath || '-' }}</div>
19 29
                 </div>
20 30
               </div>
21
-              <div class="userInfo-info">
22
-                <div class="info-item">
23
-                  <div class="info-item-icon">
24
-                    <img src="/src/assets/dataBigScreen/img01.png" alt="" />
25
-                  </div>
26
-                  <div class="info-item-content">
27
-                    <div class="info-item-label">所属部门及队室:</div>
28
-                    <div class="info-item-value">{{ portrait.deptPath || '-' }}</div>
29
-                  </div>
31
+              <div class="info-item">
32
+                <div class="info-item-icon">
33
+                  <img src="/src/assets/dataBigScreen/img04.png" alt="" />
30 34
                 </div>
31
-                <div class="info-item">
32
-                  <div class="info-item-icon">
33
-                    <img src="/src/assets/dataBigScreen/img04.png" alt="" />
34
-                  </div>
35
-                  <div class="info-item-content">
36
-                    <div class="info-item-label">学历:</div>
37
-                    <div class="info-item-value">{{ getSchooling(portrait.schooling) }}</div>
38
-                  </div>
35
+                <div class="info-item-content">
36
+                  <div class="info-item-label">学历:</div>
37
+                  <div class="info-item-value">{{ getSchooling(portrait.schooling) }}</div>
39 38
                 </div>
40 39
               </div>
41
-              <div class="userInfo-info">
42
-                <div class="info-item">
43
-                  <div class="info-item-icon">
44
-                    <img src="/src/assets/dataBigScreen/img02.png" alt="" />
45
-                  </div>
46
-                  <div class="info-item-content">
47
-                    <div class="info-item-label">出生日期:</div>
48
-                    <div class="info-item-value">{{ portrait.birthday || '-' }}</div>
49
-                  </div>
40
+            </div>
41
+            <div class="userInfo-info">
42
+              <div class="info-item">
43
+                <div class="info-item-icon">
44
+                  <img src="/src/assets/dataBigScreen/img02.png" alt="" />
50 45
                 </div>
51
-                <div class="info-item">
52
-                  <div class="info-item-icon">
53
-                    <img src="/src/assets/dataBigScreen/img05.png" alt="" />
54
-                  </div>
55
-                  <div class="info-item-content">
56
-                    <div class="info-item-label">专业:</div>
57
-                    <div class="info-item-value">{{ portrait.major || '-' }}</div>
58
-                  </div>
46
+                <div class="info-item-content">
47
+                  <div class="info-item-label">出生日期:</div>
48
+                  <div class="info-item-value">{{ portrait.birthday || '-' }}</div>
59 49
                 </div>
60 50
               </div>
61
-              <div class="userInfo-info">
62
-                <div class="info-item">
63
-                  <div class="info-item-icon">
64
-                    <img src="/src/assets/dataBigScreen/img03.png" alt="" />
65
-                  </div>
66
-                  <div class="info-item-content">
67
-                    <div class="info-item-label">技能等级:</div>
68
-                    <div class="info-item-value">
69
-                      {{ portrait.qualificationLevelText || '-' }}
70
-                      <span class="info-item-tag" style="margin-left: 10px;margin-top: 2px;" v-if="!!portrait.xrayOperatorStarttime">X射线安检仪操作岗位</span>
71
-                    </div>
72
-                  </div>
51
+              <div class="info-item">
52
+                <div class="info-item-icon">
53
+                  <img src="/src/assets/dataBigScreen/img05.png" alt="" />
73 54
                 </div>
74
-                <div class="info-item">
75
-                  <div class="info-item-icon">
76
-                    <img src="/src/assets/dataBigScreen/img06.png" alt="" />
77
-                  </div>
78
-                  <div class="info-item-content">
79
-                    <div class="info-item-label">标签:</div>
80
-                    <div class="info-item-value" v-if="portrait.userTags">
81
-                      <span class="info-item-tag" v-for="tag in portrait.userTags.split(',')" :key="tag">{{ tag }}</span>
82
-                    </div>
55
+                <div class="info-item-content">
56
+                  <div class="info-item-label">专业:</div>
57
+                  <div class="info-item-value">{{ portrait.major || '-' }}</div>
58
+                </div>
59
+              </div>
60
+            </div>
61
+            <div class="userInfo-info">
62
+              <div class="info-item">
63
+                <div class="info-item-icon">
64
+                  <img src="/src/assets/dataBigScreen/img03.png" alt="" />
65
+                </div>
66
+                <div class="info-item-content">
67
+                  <div class="info-item-label">技能等级:</div>
68
+                  <div class="info-item-value">
69
+                    {{ portrait.qualificationLevelText || '-' }}
70
+                    <span class="info-item-tag" style="margin-left: 10px;margin-top: 2px;"
71
+                      v-if="!!portrait.xrayOperatorStarttime || portrait.postNames.includes('X射线安检仪操作岗位')">X射线安检仪操作岗位</span>
83 72
                   </div>
84 73
                 </div>
85 74
               </div>
86
-              <div class="userInfo-score">
87
-                <div class="score-progress">
88
-                  <el-progress type="circle" :width="160" :stroke-width="18" color="#5BE39E" :percentage="(portrait.totalScore || -2) + 2">
89
-                    <div class="percentage-content">
90
-                      <span class="percentage-value">{{ (portrait.totalScore || 0)  }}</span>
91
-                      <span class="percentage-text">综合得分</span>
92
-                    </div>
93
-                  </el-progress>
75
+              <div class="info-item">
76
+                <div class="info-item-icon">
77
+                  <img src="/src/assets/dataBigScreen/img06.png" alt="" />
78
+                </div>
79
+                <div class="info-item-content">
80
+                  <div class="info-item-label">标签:</div>
81
+                  <div class="info-item-value" v-if="portrait.userTags">
82
+                    <span class="info-item-tag" v-for="tag in portrait.userTags.split(',')" :key="tag">{{ tag }}</span>
83
+                  </div>
94 84
                 </div>
95
-                <div class="score-box">
96
-                  <!-- <div class="score-row">
85
+              </div>
86
+            </div>
87
+            <div class="userInfo-score">
88
+              <div class="score-progress">
89
+                <el-progress type="circle" :width="160" :stroke-width="18" color="#5BE39E"
90
+                  :percentage="(portrait.totalScore || -2) + 2">
91
+                  <div class="percentage-content">
92
+                    <span class="percentage-value">{{ (portrait.totalScore || 0) }}</span>
93
+                    <span class="percentage-text">综合得分</span>
94
+                  </div>
95
+                </el-progress>
96
+              </div>
97
+              <div class="score-box">
98
+                <!-- <div class="score-row">
97 99
                     <span class="score-col">评分:</span>
98 100
                     <span class="score-col-2">{{ portrait.totalScore || 0 }}</span>
99 101
                   </div> -->
100
-                  <div class="score-row">
101
-                    <span class="score-col">附加分:</span>
102
-                    <span class="score-col-2">{{ portrait?.userTags?.split(',').length || 0 }}分</span>
103
-                    <!-- <span class="score-col-2">{{ tagScoreData != null ? (typeof tagScoreData === 'object' ? (tagScoreData.totalScore ?? tagScoreData.score ?? tagScoreData) : tagScoreData) : 0 }}</span> -->
104
-                  </div>
102
+                <div class="score-row">
103
+                  <span class="score-col">附加分:</span>
104
+                  <span class="score-col-2">{{ portrait?.userTags ? portrait?.userTags?.split(',').length || 0 : 0 }}分</span>
105
+                  <!-- <span class="score-col-2">{{ tagScoreData != null ? (typeof tagScoreData === 'object' ? (tagScoreData.totalScore ?? tagScoreData.score ?? tagScoreData) : tagScoreData) : 0 }}</span> -->
106
+                </div>
107
+              </div>
108
+            </div>
109
+          </div>
110
+        </Card>
111
+      </div>
112
+      <div v-show="portrait.personName" class="content-bottom">
113
+        <div class="content-bottom-left">
114
+          <Card title="工作履历">
115
+            <div class="work-history">
116
+              <span v-if="portrait.entryDate">
117
+                {{ formatWorkDate(portrait.entryDate) }}入职 | 司龄{{ portrait.companyYears != null ? portrait.companyYears
118
+                : '-' }}年 | 开机年限{{ portrait.xrayOperatorYears != null ? portrait.xrayOperatorYears : '-'
119
+
120
+                }}年 | 现任职{{ portrait.roleNames || '-' }}
121
+              </span>
122
+              <span v-else>暂无数据</span>
123
+            </div>
124
+          </Card>
125
+          <Card title="获奖记录">
126
+            <div class="card-content">
127
+              <div class="honor-item" v-for="(value, index) in portrait.awards" :key="index">
128
+                <div :style="{ '--indexBgColor': value.color }">
129
+                  <div :data-index="index + 1"></div>
130
+                  {{ value.level2Name }} {{ value.level4Name }}
105 131
                 </div>
132
+                <div>{{ value.score || '-' }}分</div>
133
+              </div>
134
+            </div>
135
+          </Card>
136
+          <Card title="职业资格证书获取时间">
137
+            <div class="cert-info">
138
+              <div class="cert-item" v-for="level in displayQualificationLevels" :key="level.field">
139
+                <span class="cert-name">{{ level.label }}:{{ qualificationData?.[level.field] || '-' }}</span>
106 140
               </div>
107 141
             </div>
108 142
           </Card>
109 143
         </div>
110
-        <div v-show="portrait.personName" class="content-bottom">
111
-          <div class="content-bottom-left">
112
-            <Card title="工作履历">
113
-              <div class="work-history">
114
-                <span v-if="portrait.entryDate">
115
-                  {{ formatWorkDate(portrait.entryDate)  }}入职 | 司龄{{ portrait.companyYears != null ? portrait.companyYears : '-' }}年 | 开机年限{{ portrait.xrayOperatorYears != null ? portrait.xrayOperatorYears : '-'
116
-
117
-                  }}年 | 现任职{{ portrait.roleNames || '-' }}
118
-                </span>
119
-                <span v-else>暂无数据</span>
144
+        <div class="content-bottom-center">
145
+          <Card title="个人能力">
146
+            <div class="chart-legend">
147
+              <div class="legend-item legend-warning"><span></span>预警线(低于75分)</div>
148
+              <!-- <div class="legend-item legend-normal"><span></span>正常线(75~90分)</div> -->
149
+              <div class="legend-item legend-excellent"><span></span>优秀线(高于90分)</div>
150
+              <div class="legend-item legend-current"><span></span>当前员工分值</div>
151
+            </div>
152
+            <div ref="abilityChart" class="chart-box" @click="goToWarningPage" />
153
+          </Card>
154
+        </div>
155
+        <div class="content-bottom-right">
156
+          <Card title="补充信息">
157
+            <div class="card-content">
158
+              <div class="supp-item">
159
+                <span class="s-lbl">政治面貌:</span>
160
+                <span class="s-val">{{ portrait.politicalStatusText || '-' }}</span>
120 161
               </div>
121
-            </Card>
122
-            <Card title="获奖记录">
123
-              <div class="card-content">
124
-                <div class="honor-item" v-for="(value, index) in portrait.awards" :key="index">
125
-                  <div :style="{'--indexBgColor': value.color}">
126
-                    <div :data-index="index + 1"></div>
127
-                    {{ value.level2Name }} {{ value.level4Name }}
128
-                  </div>
129
-                  <div>{{ value.score || '-' }}分</div>
130
-                </div>
162
+              <div class="supp-item supp-item-2">
163
+                <span class="s-lbl">性别:</span>
164
+                <span class="s-val">{{ portrait.sexText || '-' }}</span>
131 165
               </div>
132
-            </Card>
133
-            <Card title="职业资格证书情况">
134
-              <div class="cert-info">
135
-                <div class="cert-item" v-for="level in displayQualificationLevels" :key="level.field">
136
-                  <span class="cert-name">{{ level.label }}:{{ qualificationData?.[level.field] || '-' }}</span>
137
-                </div>
166
+              <div class="supp-item">
167
+                <span class="s-lbl">籍贯:</span>
168
+                <span class="s-val">{{ portrait.nativePlace || '-' }}</span>
138 169
               </div>
139
-            </Card>
140
-          </div>
141
-          <div class="content-bottom-center">
142
-            <Card title="个人能力">
143
-              <div class="chart-legend">
144
-                <div class="legend-item legend-warning"><span></span>预警线(低于75分)</div>
145
-                <!-- <div class="legend-item legend-normal"><span></span>正常线(75~90分)</div> -->
146
-                <div class="legend-item legend-excellent"><span></span>优秀线(高于90分)</div>
147
-                <div class="legend-item legend-current"><span></span>当前员工分值</div>
170
+              <div class="supp-item supp-item-2">
171
+                <span class="s-lbl">民族:</span>
172
+                <span class="s-val">{{ portrait.nation || '-' }}</span>
148 173
               </div>
149
-              <div ref="abilityChart" class="chart-box" @click="goToWarningPage" />
150
-            </Card>
151
-          </div>
152
-          <div class="content-bottom-right">
153
-            <Card title="补充信息">
154
-              <div class="card-content">
155
-                <div class="supp-item">
156
-                  <span class="s-lbl">政治面貌:</span>
157
-                  <span class="s-val">{{ portrait.politicalStatusText || '-' }}</span>
158
-                </div>
159
-                <div class="supp-item supp-item-2">
160
-                  <span class="s-lbl">性别:</span>
161
-                  <span class="s-val">{{ portrait.sexText || '-' }}</span>
162
-                </div>
163
-                <div class="supp-item">
164
-                  <span class="s-lbl">籍贯:</span>
165
-                  <span class="s-val">{{ portrait.nativePlace || '-' }}</span>
166
-                </div>
167
-                <div class="supp-item supp-item-2">
168
-                  <span class="s-lbl">民族:</span>
169
-                  <span class="s-val">{{ portrait.nation || '-' }}</span>
170
-                </div>
171
-                <div class="supp-item">
172
-                  <span class="s-lbl">年龄:</span>
173
-                  <span class="s-val">{{ getAge(portrait.birthday) }}</span>
174
-                </div>
175
-                <div class="supp-item supp-item-2">
176
-                  <span class="s-lbl">司龄:</span>
177
-                  <span class="s-val">{{ portrait.companyYears != null ? portrait.companyYears+'年' : '-' }}</span>
178
-                </div>
179
-                <div class="supp-item">
180
-                  <span class="s-lbl">性格特征:</span>
181
-                  <span class="s-val">{{ portrait.characterCharacteristics || '-' }}</span>
182
-                </div>
183
-                <div class="supp-item supp-item-2">
184
-                  <span class="s-lbl">工作风格:</span>
185
-                  <span class="s-val">{{ portrait.workingStyle || '-' }}</span>
186
-                </div>
187
-                <div :class="i % 2 ? 'supp-item' : 'supp-item supp-item-2'" v-for="(p, i) in (portrait.postNames || '').split('、')" :key="i">
188
-                  <span class="s-lbl">{{ i === 0 ? '业务岗位:' : ''}}</span>
189
-                  <span class="s-val">{{ p }}</span>
190
-                </div>
174
+              <div class="supp-item">
175
+                <span class="s-lbl">年龄:</span>
176
+                <span class="s-val">{{ getAge(portrait.birthday) }}</span>
191 177
               </div>
192
-            </Card>
193
-          </div>
178
+              <div class="supp-item supp-item-2">
179
+                <span class="s-lbl">司龄:</span>
180
+                <span class="s-val">{{ portrait.companyYears != null ? portrait.companyYears + '年' : '-' }}</span>
181
+              </div>
182
+              <div class="supp-item">
183
+                <span class="s-lbl">性格特征:</span>
184
+                <span class="s-val">{{ portrait.characterCharacteristics || '-' }}</span>
185
+              </div>
186
+              <div class="supp-item supp-item-2">
187
+                <span class="s-lbl">工作风格:</span>
188
+                <span class="s-val">{{ portrait.workingStyle || '-' }}</span>
189
+              </div>
190
+              <div :class="i % 2 ? 'supp-item' : 'supp-item supp-item-2'"
191
+                v-for="(p, i) in (portrait.postNames || '').split('、')" :key="i">
192
+                <span class="s-lbl">{{ i === 0 ? '业务岗位:' : '' }}</span>
193
+                <span class="s-val">{{ p }}</span>
194
+              </div>
195
+            </div>
196
+          </Card>
194 197
         </div>
198
+      </div>
195 199
       <div v-show="!portrait.personName" class="ep-empty">
196 200
         <el-empty description="搜索员工姓名以查看画像" />
197 201
       </div>
@@ -300,11 +304,11 @@ const deductTotal = computed(() => {
300 304
 
301 305
 // 职业资格等级枚举
302 306
 const qualificationLevelMap = [
303
-  { label: '一级', field: 'levelOneTime' },
304
-  { label: '二级', field: 'levelTwoTime' },
305
-  { label: '三级', field: 'levelThreeTime' },
307
+  { label: '五级', field: 'levelFiveTime' },
306 308
   { label: '四级', field: 'levelFourTime' },
307
-  { label: '五级', field: 'levelFiveTime' }
309
+  { label: '三级', field: 'levelThreeTime' },
310
+  { label: '二级', field: 'levelTwoTime' },
311
+  { label: '一级', field: 'levelOneTime' },
308 312
 ]
309 313
 
310 314
 const displayQualificationLevels = computed(() => {
@@ -312,7 +316,7 @@ const displayQualificationLevels = computed(() => {
312 316
   if (!currentLevel) return []
313 317
   const currentIndex = qualificationLevelMap.findIndex(l => l.label === currentLevel)
314 318
   if (currentIndex === -1) return []
315
-  return qualificationLevelMap.slice(0, currentIndex + 1)
319
+  return qualificationLevelMap.slice(currentIndex)
316 320
 })
317 321
 
318 322
 
@@ -367,12 +371,12 @@ const invokerEmployeePortrait = (query) => {
367 371
   const tagPromise = countTagScore(queryData).then(res => {
368 372
     const data = res.data
369 373
     if (Array.isArray(data)) {
370
-   
371
-   
372
-      const found = data.find(item => (item.userId == userId) )
373
-      
374
+
375
+
376
+      const found = data.find(item => (item.userId == userId))
377
+
374 378
       if (found) {
375
-        
379
+
376 380
         tagScoreData.value = found.totalScore ?? found.score
377 381
       } else {
378 382
         tagScoreData.value = null
@@ -414,50 +418,50 @@ const radarData = reactive({
414 418
 })
415 419
 const permutationRadarDataHandler = (result = []) => {
416 420
   radarData.grounp = result.map(d => ({ name: d.name + '\n\n' + d.score, max: 100 })),
417
-  radarData.data = [{
418
-    name: '个人能力',
419
-    value: result.map(attr => attr.score),
420
-    symbolSize: 10,
421
-    areaStyle: {
422
-      show: false,
423
-      opacity: 0,
424
-     
425
-      // color: {
426
-      //   type: 'linear',
427
-      //   x: 0,
428
-      //   y: 0,
429
-      //   x2: 0,
430
-      //   y2: 1,
431
-      //   colorStops: [{
432
-      //     offset: 0, color: '#4DC8FE'
433
-      //   }, {
434
-      //     offset: 1, color: '#6C26F3'
435
-      //   }],
436
-      //   global: false
421
+    radarData.data = [{
422
+      name: '个人能力',
423
+      value: result.map(attr => attr.score),
424
+      symbolSize: 10,
425
+      areaStyle: {
426
+        show: false,
427
+        opacity: 0,
428
+
429
+        // color: {
430
+        //   type: 'linear',
431
+        //   x: 0,
432
+        //   y: 0,
433
+        //   x2: 0,
434
+        //   y2: 1,
435
+        //   colorStops: [{
436
+        //     offset: 0, color: '#4DC8FE'
437
+        //   }, {
438
+        //     offset: 1, color: '#6C26F3'
439
+        //   }],
440
+        //   global: false
441
+        // },
442
+      },
443
+      lineStyle: {
444
+        color: '#4DC8FE',
445
+        width: 2
446
+      },
447
+      // lineStyle: {
448
+      //   color: {
449
+      //     type: 'linear',
450
+      //     x: 0,
451
+      //     y: 0,
452
+      //     x2: 0,
453
+      //     y2: 1,
454
+      //     colorStops: [{
455
+      //       offset: 0, color: '#4DC8FE'
456
+      //     }, {
457
+      //       offset: 1, color: '#6C26F3'
458
+      //     }],
459
+      //     global: false
460
+      //   },
461
+      //   width: 5
437 462
       // },
438
-    },
439
-    lineStyle: {
440
-      color: '#4DC8FE',
441
-      width: 2
442
-    },
443
-    // lineStyle: {
444
-    //   color: {
445
-    //     type: 'linear',
446
-    //     x: 0,
447
-    //     y: 0,
448
-    //     x2: 0,
449
-    //     y2: 1,
450
-    //     colorStops: [{
451
-    //       offset: 0, color: '#4DC8FE'
452
-    //     }, {
453
-    //       offset: 1, color: '#6C26F3'
454
-    //     }],
455
-    //     global: false
456
-    //   },
457
-    //   width: 5
458
-    // },
459
-    itemStyle: { color: '#fff', borderWidth: 1,  borderColor: '#00C8DA', borderJoin: 'round' }
460
-  }]
463
+      itemStyle: { color: '#fff', borderWidth: 1, borderColor: '#00C8DA', borderJoin: 'round' }
464
+    }]
461 465
 }
462 466
 const setRadarOptions = computed(() => {
463 467
   return {
@@ -467,7 +471,7 @@ const setRadarOptions = computed(() => {
467 471
       radius: '65%',
468 472
       splitNumber: 8,
469 473
       axisLine: { lineStyle: { color: '#ccc' } },
470
-      splitLine: { lineStyle: { color: ['#ccc', '#ccc', '#ccc', '#ccc', '#ccc', '#ccc', '#fe4322', '#8EC742', '#ccc'], width: 3  } },
474
+      splitLine: { lineStyle: { color: ['#ccc', '#ccc', '#ccc', '#ccc', '#ccc', '#ccc', '#fe4322', '#8EC742', '#ccc'], width: 3 } },
471 475
       splitArea: { show: false },
472 476
       axisName: {
473 477
         color: '#fff',
@@ -547,13 +551,14 @@ onUnmounted(() => {
547 551
 
548 552
 <style lang="scss" scoped>
549 553
 .content {
550
-  height:  calc(100vh - 90px);
554
+  height: calc(100vh - 90px);
551 555
   display: flex;
552 556
   flex-direction: column;
553 557
   row-gap: 15px;
554 558
   padding: 15px;
555 559
   box-sizing: border-box;
556 560
   overflow: hidden;
561
+
557 562
   .ep-empty {
558 563
     background: #fff;
559 564
     height: 100%;
@@ -564,17 +569,21 @@ onUnmounted(() => {
564 569
     opacity: 0.8;
565 570
     border-radius: 30px;
566 571
   }
572
+
567 573
   .content-search {}
574
+
568 575
   .content-info {
569
-    & > * {
576
+    &>* {
570 577
       height: 100%;
571 578
     }
579
+
572 580
     .userInfo {
573 581
       display: flex;
574 582
       width: 100%;
575 583
       height: 180px;
576 584
       box-sizing: border-box;
577 585
       column-gap: 5px;
586
+
578 587
       .userInfo-name {
579 588
         flex: 1;
580 589
         position: relative;
@@ -582,16 +591,18 @@ onUnmounted(() => {
582 591
         align-items: center;
583 592
         justify-content: center;
584 593
         column-gap: 30px;
594
+
585 595
         &::after {
586 596
           content: '';
587 597
           display: block;
588 598
           width: 1px;
589
-          background:linear-gradient(180deg, transparent, #6f6c98, transparent);
599
+          background: linear-gradient(180deg, transparent, #6f6c98, transparent);
590 600
           height: 100%;
591 601
           position: absolute;
592 602
           top: 0;
593 603
           right: -2px;
594 604
         }
605
+
595 606
         .avatar-area {
596 607
           height: 140px;
597 608
           width: 140px;
@@ -600,6 +611,7 @@ onUnmounted(() => {
600 611
           display: flex;
601 612
           align-items: center;
602 613
           justify-content: center;
614
+
603 615
           .avatar {
604 616
             width: 100%;
605 617
             height: 100%;
@@ -611,22 +623,26 @@ onUnmounted(() => {
611 623
             font-weight: 900;
612 624
           }
613 625
         }
626
+
614 627
         .basic-name {
615 628
           display: flex;
616 629
           flex-direction: column;
617 630
           justify-content: center;
618 631
           row-gap: 10px;
619 632
           font-weight: 500;
633
+
620 634
           .basic-label {
621 635
             font-size: 18px;
622 636
             color: #8675AE;
623 637
           }
638
+
624 639
           .basic-value {
625 640
             font-size: 26px;
626 641
             color: #fff;
627 642
           }
628 643
         }
629 644
       }
645
+
630 646
       .userInfo-info {
631 647
         flex: 1;
632 648
         position: relative;
@@ -636,28 +652,33 @@ onUnmounted(() => {
636 652
         padding: 10px 15px;
637 653
         padding-right: 0px;
638 654
         box-sizing: border-box;
655
+
639 656
         &::after {
640 657
           content: '';
641 658
           display: block;
642 659
           width: 1px;
643
-          background:linear-gradient(180deg, transparent, #6f6c98, transparent);
660
+          background: linear-gradient(180deg, transparent, #6f6c98, transparent);
644 661
           height: 100%;
645 662
           position: absolute;
646 663
           top: 0;
647 664
           right: -2px;
648 665
         }
666
+
649 667
         .info-item {
650 668
           display: flex;
651 669
           column-gap: 15px;
670
+
652 671
           .info-item-content {
653 672
             display: flex;
654 673
             flex-direction: column;
655 674
             justify-content: space-between;
656 675
             font-weight: 500;
676
+
657 677
             .info-item-label {
658 678
               font-size: 18px;
659 679
               color: #8675AE;
660 680
             }
681
+
661 682
             .info-item-value {
662 683
               font-size: 18px;
663 684
               color: #fff;
@@ -666,6 +687,7 @@ onUnmounted(() => {
666 687
               flex-wrap: wrap;
667 688
               gap: 2px;
668 689
             }
690
+
669 691
             .info-item-tag {
670 692
               font-size: 14px;
671 693
               display: block;
@@ -676,6 +698,7 @@ onUnmounted(() => {
676 698
           }
677 699
         }
678 700
       }
701
+
679 702
       .userInfo-score {
680 703
         :deep(.el-progress-circle) {
681 704
           width: 160px;
@@ -683,11 +706,13 @@ onUnmounted(() => {
683 706
           --el-fill-color-light: #004970;
684 707
           transform: rotate(0.5turn);
685 708
         }
709
+
686 710
         flex: 1;
687 711
         display: flex;
688 712
         align-items: center;
689 713
         padding-left: 15px;
690 714
         column-gap: 25px;
715
+
691 716
         .score-progress {
692 717
           .percentage-content {
693 718
             display: flex;
@@ -695,26 +720,31 @@ onUnmounted(() => {
695 720
             align-items: center;
696 721
             row-gap: 6px;
697 722
             font-weight: 500;
723
+
698 724
             .percentage-value {
699 725
               font-size: 48px;
700 726
               color: #02E5CC;
701 727
             }
728
+
702 729
             .percentage-text {
703 730
               font-size: 16px;
704 731
               color: #fff;
705 732
             }
706 733
           }
707 734
         }
735
+
708 736
         .score-box {
709 737
           display: flex;
710 738
           flex-direction: column;
711 739
           font-weight: 500;
712 740
           row-gap: 15px;
713 741
           font-size: 18px;
742
+
714 743
           .score-col {
715 744
             display: inline-block;
716 745
             width: 5em;
717 746
           }
747
+
718 748
           .score-col-2 {
719 749
             font-size: 24px;
720 750
           }
@@ -722,24 +752,30 @@ onUnmounted(() => {
722 752
       }
723 753
     }
724 754
   }
755
+
725 756
   .content-bottom {
726 757
     flex: 1;
727 758
     display: flex;
728 759
     column-gap: 15px;
729 760
     overflow: hidden;
761
+
730 762
     .content-bottom-left {
731 763
       width: 380px;
732 764
       display: flex;
733 765
       flex-direction: column;
734 766
       row-gap: 15px;
735
-      & > *:nth-child(1) {
767
+
768
+      &>*:nth-child(1) {
736 769
         height: 160px;
770
+
737 771
         .work-history {
738 772
           font-size: 18px;
739 773
         }
740 774
       }
741
-      & > *:nth-child(2) {
775
+
776
+      &>*:nth-child(2) {
742 777
         flex: 1.6;
778
+
743 779
         .honor-item {
744 780
           height: 48px;
745 781
           padding-right: 15px;
@@ -750,12 +786,14 @@ onUnmounted(() => {
750 786
           align-items: center;
751 787
           justify-content: space-between;
752 788
           position: relative;
753
-          & > div:nth-child(1) {
789
+
790
+          &>div:nth-child(1) {
754 791
             display: flex;
755 792
             align-items: center;
756 793
             column-gap: 15px;
757 794
             margin-left: 10px;
758
-            & > div {
795
+
796
+            &>div {
759 797
               width: 26px;
760 798
               height: 20px;
761 799
               position: relative;
@@ -763,6 +801,7 @@ onUnmounted(() => {
763 801
               text-align: center;
764 802
               line-height: 20px;
765 803
               color: #222;
804
+
766 805
               &::before {
767 806
                 position: absolute;
768 807
                 inset: 0;
@@ -773,6 +812,7 @@ onUnmounted(() => {
773 812
                 height: 100%;
774 813
                 transform: skew(-25deg);
775 814
               }
815
+
776 816
               &::after {
777 817
                 position: absolute;
778 818
                 inset: 0;
@@ -784,9 +824,11 @@ onUnmounted(() => {
784 824
               }
785 825
             }
786 826
           }
787
-          & > div:nth-child(2) {
827
+
828
+          &>div:nth-child(2) {
788 829
             color: #02E5CC;
789 830
           }
831
+
790 832
           &::after {
791 833
             content: '';
792 834
             display: block;
@@ -795,16 +837,19 @@ onUnmounted(() => {
795 837
             left: 0;
796 838
             height: 1px;
797 839
             width: 100%;
798
-            background:linear-gradient(45deg, transparent 0%, transparent 30%, #6f6c98 100%);
840
+            background: linear-gradient(45deg, transparent 0%, transparent 30%, #6f6c98 100%);
799 841
           }
800 842
         }
801 843
       }
802 844
     }
845
+
803 846
     .content-bottom-center {
804 847
       flex: 1;
805
-      & > * {
848
+
849
+      &>* {
806 850
         height: 100%;
807 851
       }
852
+
808 853
       .chart-legend {
809 854
         display: flex;
810 855
         justify-content: center;
@@ -815,10 +860,12 @@ onUnmounted(() => {
815 860
         color: #fff;
816 861
         font-size: 14px;
817 862
       }
863
+
818 864
       .legend-item {
819 865
         display: flex;
820 866
         align-items: center;
821 867
         gap: 6px;
868
+
822 869
         span {
823 870
           width: 24px;
824 871
           height: 10px;
@@ -826,41 +873,53 @@ onUnmounted(() => {
826 873
           border-radius: 2px;
827 874
         }
828 875
       }
876
+
829 877
       .legend-warning {
830 878
         color: #fe4322;
831
-        span { 
879
+
880
+        span {
832 881
           background-color: #fe4322;
833 882
         }
834 883
       }
884
+
835 885
       .legend-normal {
836 886
         color: #fff;
837
-        span { 
887
+
888
+        span {
838 889
           background-color: #fff;
839 890
         }
840 891
       }
892
+
841 893
       .legend-excellent {
842 894
         color: #8EC742;
843
-        span { 
895
+
896
+        span {
844 897
           background-color: #8EC742;
845 898
         }
846 899
       }
900
+
847 901
       .legend-current {
848 902
         color: #1890ff;
849
-        span { 
903
+
904
+        span {
850 905
           background-color: #1890ff;
851 906
         }
852 907
       }
908
+
853 909
       .chart-box {
854 910
         width: 100%;
855 911
         height: calc(100% - 34px);
856 912
         overflow: hidden;
857 913
       }
858 914
     }
915
+
859 916
     .content-bottom-right {
860 917
       width: 380px;
861
-      & > * {
918
+
919
+      &>* {
862 920
         height: 100%;
863 921
       }
922
+
864 923
       .supp-item {
865 924
         display: flex;
866 925
         justify-content: space-between;
@@ -869,24 +928,30 @@ onUnmounted(() => {
869 928
         padding: 0 15px;
870 929
         font-weight: 500;
871 930
         font-size: 18px;
931
+
872 932
         .s-lbl {
873 933
           color: #8675AE;
874 934
         }
935
+
875 936
         .s-val {
876 937
           color: #fff;
877 938
         }
878 939
       }
940
+
879 941
       .supp-item-2 {
880 942
         background: #261C48;
881 943
       }
882 944
     }
945
+
883 946
     .card-content {
884 947
       height: 100%;
885 948
       overflow-x: hidden;
886 949
       overflow-y: auto;
950
+
887 951
       &::-webkit-scrollbar-track {
888 952
         background: transparent;
889 953
       }
954
+
890 955
       &::-webkit-scrollbar-thumb {
891 956
         background: linear-gradient(225deg, #4355cb, #873dc3);
892 957
         border-radius: 4px;
@@ -903,6 +968,7 @@ onUnmounted(() => {
903 968
   pointer-events: none;
904 969
   transform: translateY(-80%);
905 970
   display: none;
971
+
906 972
   .p-empty {
907 973
     text-align: center;
908 974
     margin-top: 5px;
@@ -926,9 +992,11 @@ onUnmounted(() => {
926 992
     align-items: center;
927 993
     justify-content: space-between;
928 994
     color: #fff;
995
+
929 996
     .deduct-text {
930 997
       color: #ff4d4f;
931 998
     }
999
+
932 1000
     .add-text {
933 1001
       color: #00b42a;
934 1002
     }
@@ -941,9 +1009,11 @@ onUnmounted(() => {
941 1009
     justify-content: space-between;
942 1010
     font-weight: 500;
943 1011
     font-size: 16px;
1012
+
944 1013
     &.deduct {
945 1014
       color: #ff4d4f;
946 1015
     }
1016
+
947 1017
     &.add {
948 1018
       color: #00b42a;
949 1019
     }

+ 807 - 0
src/views/portraitManagement/organProfile/index.vue

@@ -0,0 +1,807 @@
1
+<template>
2
+
3
+  <div class="org-profile">
4
+    <!-- 顶部指标卡片 -->
5
+    <div class="metric-row">
6
+      <div class="metric-card" v-for="(m, i) in metricCards" :key="i" :style="{ background: m.bg }">
7
+        <div class="metric-title">{{ m.title }}</div>
8
+        <div class="metric-value" :style="{ color: m.color }">{{ m.value }}</div>
9
+        <div class="metric-change-row">
10
+          <div class="metric-change-info">
11
+            <span class="metric-change-label">环比</span>
12
+            <span class="metric-change" :class="'change-' + m.changeType" :style="{ color: m.changeColor }">
13
+              {{ m.change }}
14
+            </span>
15
+          </div>
16
+          <div class="metric-chart" :ref="el => m._sparkRef = el" />
17
+        </div>
18
+      </div>
19
+      <div class="metric-side-cards">
20
+        <div class="metric-card metric-warning-top">
21
+          <div class="metric-title">锐甲安语—自愿报告系统</div>
22
+          <div class="metric-value" style="color:#fff">3</div>
23
+        </div>
24
+        <div class="metric-card metric-warning-bottom">
25
+          <div class="metric-title">部门亚健康人员</div>
26
+          <div class="metric-value" style="color:#fff">147</div>
27
+        </div>
28
+      </div>
29
+    </div>
30
+
31
+    <!-- 中部图表区 -->
32
+    <div class="charts-row">
33
+      <div class="charts-col charts-col-main">
34
+        <!-- 第一行 -->
35
+        <div class="charts-grid charts-grid-2">
36
+          <WhiteCard title="监察问题统计" bgColor="#dbeafe">
37
+            <div ref="supervisionLineRef" class="chart-box" />
38
+          </WhiteCard>
39
+          <WhiteCard title="问题类型分布" bgColor="#dbeafe">
40
+            <div ref="problemTypeRef" class="chart-box" />
41
+          </WhiteCard>
42
+        </div>
43
+        <!-- 第二行 -->
44
+        <div class="charts-grid charts-grid-2">
45
+          <WhiteCard title="班组问题统计(监察)" bgColor="#dcfce7">
46
+            <div ref="teamSupervisionRef" class="chart-box" />
47
+          </WhiteCard>
48
+          <WhiteCard title="区域问题占比(监察)" bgColor="#fef3c7">
49
+            <div ref="areaPieRef" class="chart-box" />
50
+          </WhiteCard>
51
+        </div>
52
+        <!-- 第三行 -->
53
+        <div class="charts-grid charts-grid-2">
54
+          <WhiteCard title="班组问题统计(实时)" bgColor="#dcfce7">
55
+            <div ref="teamRealtimeRef" class="chart-box" />
56
+          </WhiteCard>
57
+          <WhiteCard title="实时质控拦截情况" bgColor="#dcfce7">
58
+            <div ref="realtimeLineRef" class="chart-box" />
59
+          </WhiteCard>
60
+        </div>
61
+        <!-- 第四行 -->
62
+        <div class="charts-grid charts-grid-2">
63
+          <WhiteCard title="实时质控开机年龄分布" bgColor="#dbeafe">
64
+            <div ref="yearRingRef" class="chart-box" />
65
+          </WhiteCard>
66
+          <WhiteCard title="实时质控围难易度" bgColor="#dbeafe">
67
+            <div ref="difficultyRef" class="chart-box" />
68
+          </WhiteCard>
69
+        </div>
70
+        <!-- 实时质控开机年限分布 -->
71
+        <div class="charts-grid charts-grid-2">
72
+          <WhiteCard title="实时质控开机年限分布" bgColor="#dcfce7">
73
+            <div ref="yearLimitRef" class="chart-box" />
74
+          </WhiteCard>
75
+          <WhiteCard title="实时拦截物品汇总" bgColor="#dbeafe">
76
+            <div ref="interceptItemsRef" class="chart-box" />
77
+          </WhiteCard>
78
+        </div>
79
+        <!-- 第五行 -->
80
+        <div class="charts-grid charts-grid-2">
81
+
82
+          <WhiteCard title="服务巡查" bgColor="#dbeafe">
83
+            <div ref="servicePatrolRef" class="chart-box" />
84
+          </WhiteCard>
85
+          <WhiteCard title="服务巡查" bgColor="#e8eaf6">
86
+            <div ref="servicePatrolLineRef" class="chart-box" />
87
+          </WhiteCard>
88
+        </div>
89
+        <!-- 第六行 -->
90
+        <div class="charts-grid charts-grid-2">
91
+          <WhiteCard title="投诉涉及班组情况" bgColor="#fce4ec">
92
+            <div ref="complaintTeamRef" class="chart-box" />
93
+          </WhiteCard>
94
+          <WhiteCard title="不安全事件发生对比" bgColor="#dbeafe">
95
+            <div ref="unsafeCompareRef" class="chart-box" />
96
+          </WhiteCard>
97
+        </div>
98
+        <!-- 第七行 -->
99
+        <div class="charts-grid charts-grid-2">
100
+          <WhiteCard title="亚健康人数占比" bgColor="#fef3c7">
101
+            <div ref="subhealthPieRef" class="chart-box" />
102
+          </WhiteCard>
103
+          <WhiteCard title="各班组健康与亚健康比例" bgColor="#fef3c7">
104
+            <div ref="healthRatioRef" class="chart-box" />
105
+          </WhiteCard>
106
+        </div>
107
+        <!-- 第八行 -->
108
+        <div class="charts-grid charts-grid-1">
109
+          <WhiteCard title="旅检三部人员年龄分布表" bgColor="#dbeafe">
110
+            <div ref="ageDistRef" class="chart-box-wide" />
111
+          </WhiteCard>
112
+        </div>
113
+      </div>
114
+
115
+      <!-- 右侧排行榜 -->
116
+      <div class="charts-col-side">
117
+        <WhiteCard title="监察问题(总)" bgColor="#f0f4ff">
118
+          <div class="rank-list">
119
+            <div class="rank-avatar-row">
120
+              <div class="rank-avatar-item" v-for="(item, i) in superVisionTop3" :key="i">
121
+                <div class="rank-avatar-wrapper" :class="'rank-pos-' + (i + 1)">
122
+                  <div class="rank-avatar" :style="{ background: item.color }">{{ item.name.slice(-2) }}</div>
123
+                  <span class="rank-badge">{{ i + 1 }}</span>
124
+                </div>
125
+                <div class="rank-name">{{ item.name }}</div>
126
+                <div class="rank-num">{{ item.num }}</div>
127
+              </div>
128
+            </div>
129
+            <div class="rank-table">
130
+              <div class="rank-tr rank-th">
131
+                <span>排名</span><span>姓名</span><span>问题数</span>
132
+              </div>
133
+              <div class="rank-tr" v-for="(item, i) in superVisionList" :key="i">
134
+                <span>{{ i + 1 }}</span>
135
+                <span>{{ item.name }}</span>
136
+                <span>{{ item.num }}</span>
137
+              </div>
138
+            </div>
139
+          </div>
140
+        </WhiteCard>
141
+        <WhiteCard title="实时漏洞检情况(总)" bgColor="#f0fdf4">
142
+          <div class="rank-list">
143
+            <div class="rank-avatar-row">
144
+              <div class="rank-avatar-item" v-for="(item, i) in vulnTop3" :key="i">
145
+                <div class="rank-avatar-wrapper" :class="'rank-pos-' + (i + 1)">
146
+                  <div class="rank-avatar" :style="{ background: item.color }">{{ item.name.slice(-2) }}</div>
147
+                  <span class="rank-badge">{{ i + 1 }}</span>
148
+                </div>
149
+                <div class="rank-name">{{ item.name }}</div>
150
+                <div class="rank-num">{{ item.num }}</div>
151
+              </div>
152
+            </div>
153
+            <div class="rank-table">
154
+              <div class="rank-tr rank-th">
155
+                <span>排名</span><span>姓名</span><span>问题数</span>
156
+              </div>
157
+              <div class="rank-tr" v-for="(item, i) in vulnList" :key="i">
158
+                <span>{{ i + 1 }}</span>
159
+                <span>{{ item.name }}</span>
160
+                <span>{{ item.num }}</span>
161
+              </div>
162
+            </div>
163
+          </div>
164
+        </WhiteCard>
165
+        <WhiteCard title="航站楼加分" bgColor="#eff6ff">
166
+          <div class="rank-list">
167
+            <div class="rank-avatar-row">
168
+              <div class="rank-avatar-item" v-for="(item, i) in bonusTop3" :key="i">
169
+                <div class="rank-avatar-wrapper" :class="'rank-pos-' + (i + 1)">
170
+                  <div class="rank-avatar" :style="{ background: item.color }">{{ item.name.slice(-2) }}</div>
171
+                  <span class="rank-badge">{{ i + 1 }}</span>
172
+                </div>
173
+                <div class="rank-name">{{ item.name }}</div>
174
+                <div class="rank-num">{{ item.num }}</div>
175
+              </div>
176
+            </div>
177
+            <div class="rank-table">
178
+              <div class="rank-tr rank-th">
179
+                <span>排名</span><span>姓名</span><span>加分</span>
180
+              </div>
181
+              <div class="rank-tr" v-for="(item, i) in bonusList" :key="i">
182
+                <span>{{ i + 1 }}</span>
183
+                <span>{{ item.name }}</span>
184
+                <span>{{ item.num }}</span>
185
+              </div>
186
+            </div>
187
+          </div>
188
+        </WhiteCard>
189
+        <WhiteCard title="查获数量" bgColor="#f0f4ff">
190
+          <div class="rank-list">
191
+            <div class="rank-avatar-row">
192
+              <div class="rank-avatar-item" v-for="(item, i) in seizureTop3" :key="i">
193
+                <div class="rank-avatar-wrapper" :class="'rank-pos-' + (i + 1)">
194
+                  <div class="rank-avatar" :style="{ background: item.color }">{{ item.name.slice(-2) }}</div>
195
+                  <span class="rank-badge">{{ i + 1 }}</span>
196
+                </div>
197
+                <div class="rank-name">{{ item.name }}</div>
198
+                <div class="rank-num">{{ item.num }}</div>
199
+              </div>
200
+            </div>
201
+            <div class="rank-table">
202
+              <div class="rank-tr rank-th">
203
+                <span>排名</span><span>姓名</span><span>查获数</span>
204
+              </div>
205
+              <div class="rank-tr" v-for="(item, i) in seizureList" :key="i">
206
+                <span>{{ i + 1 }}</span>
207
+                <span>{{ item.name }}</span>
208
+                <span>{{ item.num }}</span>
209
+              </div>
210
+            </div>
211
+          </div>
212
+        </WhiteCard>
213
+      </div>
214
+    </div>
215
+  </div>
216
+
217
+</template>
218
+
219
+<script setup>
220
+import { ref, computed, onMounted } from 'vue'
221
+import { useECharts } from '@/hooks/useEcharts'
222
+import WhiteCard from '../components/whiteCard.vue'
223
+import Page from '../components/page.vue'
224
+import * as echarts from 'echarts'
225
+
226
+// ─── 顶部指标卡片 ───────────────────────────────
227
+const metricCards = ref([
228
+  { title: '监察问题数(本月)', value: '11', change: '57.69%', changeType: 'down', changeColor: '#ef4444', color: '#f59e0b', bg: '#dbeafe', sparkColor: '#3b82f6' },
229
+  { title: '实时质控数(本月)', value: '21', change: '56.25%', changeType: 'down', changeColor: '#ef4444', color: '#22c55e', bg: '#dcfce7', sparkColor: '#22c55e' },
230
+  { title: '服务巡查(本月)', value: '4', change: '0%', changeType: 'flat', changeColor: '#6b7280', color: '#3b82f6', bg: '#dbeafe', sparkColor: '#3b82f6' },
231
+  { title: '投诉情况(本月)', value: '1', change: '88.89%', changeType: 'down', changeColor: '#ef4444', color: '#ef4444', bg: '#fce4ec', sparkColor: '#ef4444' },
232
+  { title: '不安全事件发生次数(今年)', value: '3', change: '57.14%', changeType: 'down', changeColor: '#ef4444', color: '#ffffff', bg: '#6b7280', sparkColor: '#9ca3af' },
233
+])
234
+
235
+// ─── 图表 ref ────────────────────────────────────
236
+const supervisionLineRef = ref(null)
237
+const problemTypeRef = ref(null)
238
+const teamSupervisionRef = ref(null)
239
+const areaPieRef = ref(null)
240
+const teamRealtimeRef = ref(null)
241
+const realtimeLineRef = ref(null)
242
+const yearRingRef = ref(null)
243
+const yearLimitRef = ref(null)
244
+const difficultyRef = ref(null)
245
+const interceptItemsRef = ref(null)
246
+const servicePatrolRef = ref(null)
247
+const servicePatrolLineRef = ref(null)
248
+const complaintTeamRef = ref(null)
249
+const unsafeCompareRef = ref(null)
250
+const subhealthPieRef = ref(null)
251
+const healthRatioRef = ref(null)
252
+const ageDistRef = ref(null)
253
+
254
+// ─── 图表配置(浅色主题)──────────────────────────
255
+
256
+const axisLabelColor = '#475569'
257
+const axisLineColor = '#cbd5e1'
258
+const splitLineColor = '#e2e8f0'
259
+const tooltipBg = 'rgba(255,255,255,0.95)'
260
+const tooltipBorder = '#cbd5e1'
261
+const tooltipText = '#1e293b'
262
+
263
+// 1. 监察问题统计-折线图
264
+const supervisionLineOpt = computed(() => ({
265
+  grid: { top: 20, bottom: 25, left: 40, right: 15 },
266
+  xAxis: { type: 'category', data: ['03:00', '05:00', '07:00', '09:00', '11:00', '13:00', '15:00', '17:00', '19:00', '20:00'], axisLabel: { color: axisLabelColor, fontSize: 11 }, axisLine: { lineStyle: { color: axisLineColor } } },
267
+  yAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
268
+  series: [{ type: 'line', data: [3, 17, 11, 12, 9, 14, 15, 10, 7, 4], smooth: true, lineStyle: { color: '#3b82f6', width: 2 }, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: 'rgba(59,130,246,0.2)' }, { offset: 1, color: 'rgba(59,130,246,0)' }] } }, symbol: 'circle', symbolSize: 6, itemStyle: { color: '#3b82f6' } }],
269
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
270
+}))
271
+
272
+// 2. 问题类型分布-条形图
273
+const problemTypeOpt = computed(() => ({
274
+  grid: { top: 15, bottom: 20, left: 110, right: 30 },
275
+  xAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
276
+  yAxis: { type: 'category', data: ['设备设施流程类', '岗位规范类', '岗位操作类'], axisLabel: { color: '#1e293b', fontSize: 12 }, axisLine: { lineStyle: { color: axisLineColor } } },
277
+  series: [{ type: 'bar', data: [{ value: 2, itemStyle: { color: '#3b82f6' } }, { value: 75, itemStyle: { color: '#3b82f6' } }, { value: 101, itemStyle: { color: '#3b82f6' } }], barWidth: '50%', label: { show: true, position: 'right', color: '#1e293b', fontSize: 13 } }],
278
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
279
+}))
280
+
281
+// 3. 班组问题统计(监察)-柱状图
282
+const teamSupervisionOpt = computed(() => ({
283
+  grid: { top: 15, bottom: 25, left: 40, right: 15 },
284
+  xAxis: { type: 'category', data: ['安平班组', '安行班组', '木兰班组', '芮茜班组', '拓新班组', '屹动班组'], axisLabel: { color: axisLabelColor, fontSize: 11, rotate: 15 }, axisLine: { lineStyle: { color: axisLineColor } } },
285
+  yAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
286
+  series: [{ type: 'bar', data: [{ value: 34, itemStyle: { color: '#3b82f6' } }, { value: 21, itemStyle: { color: '#3b82f6' } }, { value: 2, itemStyle: { color: '#3b82f6' } }, { value: 41, itemStyle: { color: '#3b82f6' } }, { value: 39, itemStyle: { color: '#3b82f6' } }, { value: 34, itemStyle: { color: '#3b82f6' } }], barWidth: '55%', label: { show: true, position: 'top', color: '#1e293b', fontSize: 11 } }],
287
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
288
+}))
289
+
290
+// 4. 区域问题占比-饼图
291
+const areaPieOpt = computed(() => ({
292
+  grid: { top: 10, bottom: 10, left: 0, right: '45%' },
293
+  series: [{
294
+    type: 'pie', radius: ['30%', '60%'], center: ['35%', '50%'],
295
+    data: [
296
+      { value: 128, name: 'T3国内出发', itemStyle: { color: '#a78bfa' } },
297
+      { value: 39, name: 'T3国际国内', itemStyle: { color: '#22c55e' } },
298
+      { value: 5, name: 'T3中心实时指控', itemStyle: { color: '#f59e0b' } }
299
+    ],
300
+    label: { show: true, color: '#475569', fontSize: 11, formatter: '{b}\n{c} ({d}%)' },
301
+    labelLine: { lineStyle: { color: '#cbd5e1' } },
302
+    emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.1)' } }
303
+  }],
304
+  tooltip: { trigger: 'item', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
305
+}))
306
+
307
+// 5. 班组问题统计(实时)-柱状图
308
+const teamRealtimeOpt = computed(() => ({
309
+  grid: { top: 15, bottom: 25, left: 40, right: 15 },
310
+  xAxis: { type: 'category', data: ['安平班组', '安行班组', '木兰班组', '芮茜班组', '拓新班组', '屹动班组'], axisLabel: { color: axisLabelColor, fontSize: 11, rotate: 15 }, axisLine: { lineStyle: { color: axisLineColor } } },
311
+  yAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
312
+  series: [{ type: 'bar', data: [{ value: 60, itemStyle: { color: '#3b82f6' } }, { value: 44, itemStyle: { color: '#3b82f6' } }, { value: 21, itemStyle: { color: '#3b82f6' } }, { value: 65, itemStyle: { color: '#3b82f6' } }, { value: 56, itemStyle: { color: '#3b82f6' } }, { value: 75, itemStyle: { color: '#3b82f6' } }], barWidth: '55%', label: { show: true, position: 'top', color: '#1e293b', fontSize: 11 } }],
313
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
314
+}))
315
+
316
+// 6. 实时质控拦截情况-折线图
317
+const realtimeLineOpt = computed(() => ({
318
+  grid: { top: 20, bottom: 25, left: 40, right: 15 },
319
+  xAxis: { type: 'category', data: ['04:00', '06:00', '08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00'], axisLabel: { color: axisLabelColor, fontSize: 11 }, axisLine: { lineStyle: { color: axisLineColor } } },
320
+  yAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
321
+  series: [{ type: 'line', data: [8, 28, 31, 21, 26, 21, 11, 4, 1, 2], smooth: true, lineStyle: { color: '#3b82f6', width: 2 }, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: 'rgba(59,130,246,0.15)' }, { offset: 1, color: 'rgba(59,130,246,0)' }] } }, symbol: 'circle', symbolSize: 6, itemStyle: { color: '#3b82f6' } }],
322
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
323
+}))
324
+
325
+// 7. 实时质控开机年限分布
326
+const yearLimitOpt = computed(() => ({
327
+  grid: { top: 15, bottom: 25, left: 40, right: 15 },
328
+  xAxis: { type: 'category', data: Array.from({ length: 14 }, (_, i) => i + 1), axisLabel: { color: axisLabelColor, fontSize: 11 }, axisLine: { lineStyle: { color: axisLineColor } } },
329
+  yAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
330
+  series: [{ type: 'bar', data: [5, 32, 65, 11, 14, 11, 9, 16, 27, 45, 31, 23, 27, 8], barWidth: '55%', itemStyle: { color: '#3b82f6', borderRadius: [3, 3, 0, 0] }, label: { show: true, position: 'top', color: '#1e293b', fontSize: 11 } }],
331
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
332
+}))
333
+
334
+// 8. 实时质控开机年龄分布
335
+const yearRingOpt = computed(() => ({
336
+  grid: { top: 15, bottom: 25, left: 40, right: 15 },
337
+  xAxis: { type: 'category', data: Array.from({ length: 42 }, (_, i) => i + 1), axisLabel: { color: axisLabelColor, fontSize: 10, interval: 4 }, axisLine: { lineStyle: { color: axisLineColor } } },
338
+  yAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
339
+  series: [{ type: 'bar', data: Array.from({ length: 42 }, (_, i) => Math.floor(Math.random() * 40 + 10)), barWidth: '60%', itemStyle: { color: '#3b82f6', borderRadius: [3, 3, 0, 0] } }],
340
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
341
+}))
342
+
343
+// 9. 实时质控围难易度
344
+const difficultyOpt = computed(() => ({
345
+  grid: { top: 15, bottom: 20, left: 40, right: 15 },
346
+  xAxis: { type: 'category', data: ['0', '1', '2', '3', '4', '5'], axisLabel: { color: axisLabelColor, fontSize: 11 }, axisLine: { lineStyle: { color: axisLineColor } } },
347
+  yAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
348
+  series: [{ type: 'bar', data: [2, 145, 70, 92, 16, 1], barWidth: '50%', itemStyle: { color: '#3b82f6', borderRadius: [5, 5, 0, 0] }, label: { show: true, position: 'top', color: '#1e293b', fontSize: 11 } }],
349
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
350
+}))
351
+
352
+// 9. 实时拦截物品汇总
353
+const interceptItemsOpt = computed(() => ({
354
+  grid: { top: 15, bottom: 40, left: 45, right: 15 },
355
+  xAxis: { type: 'category', data: ['打火机', '香烟', '刀具', '充电宝', '液体', '剪刀', '钥匙扣', '其他'], axisLabel: { color: axisLabelColor, fontSize: 10, rotate: 30 }, axisLine: { lineStyle: { color: axisLineColor } } },
356
+  yAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
357
+  series: [{ type: 'bar', data: [136, 98, 45, 67, 23, 34, 12, 56], barWidth: '55%', itemStyle: { color: '#3b82f6', borderRadius: [5, 5, 0, 0] } }],
358
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
359
+}))
360
+
361
+// 10. 服务巡查-柱状图
362
+const servicePatrolOpt = computed(() => ({
363
+  grid: { top: 15, bottom: 25, left: 40, right: 15 },
364
+  xAxis: { type: 'category', data: ['安平班组', '安行班组', '木兰班组', '芮茜班组', '拓新班组', '屹动班组'], axisLabel: { color: axisLabelColor, fontSize: 11, rotate: 15 }, axisLine: { lineStyle: { color: axisLineColor } } },
365
+  yAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
366
+  series: [{ type: 'bar', data: [3, 16, 7, 15, 14, 11], barWidth: '55%', itemStyle: { color: '#3b82f6', borderRadius: [5, 5, 0, 0] }, label: { show: true, position: 'top', color: '#1e293b', fontSize: 11 } }],
367
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
368
+}))
369
+
370
+// 11. 服务巡查-折线图
371
+const servicePatrolLineOpt = computed(() => ({
372
+  legend: { data: ['计数'], textStyle: { color: axisLabelColor, fontSize: 12 }, top: 0, left: 0, icon: 'circle' },
373
+  grid: { top: 30, bottom: 35, left: 40, right: 15 },
374
+  xAxis: { type: 'category', data: ['05:00', '06:00', '07:00', '08:00', '09:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00', '18:00', '19:00', '20:00', '21:00', '空值'], axisLabel: { color: axisLabelColor, fontSize: 11, rotate: 30 }, axisLine: { lineStyle: { color: axisLineColor } } },
375
+  yAxis: { type: 'value', min: 0, max: 7, axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
376
+  series: [{ name: '计数', type: 'line', data: [2, 5, 5, 5, 3, 4, 3, 2, 6, 5, 5, 3, 7, 3, 2, 3, 3, 1], lineStyle: { color: '#3b82f6', width: 2 }, itemStyle: { color: '#3b82f6' }, symbol: 'circle', symbolSize: 6, label: { show: true, position: 'top', color: '#1e293b', fontSize: 11 } }],
377
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
378
+}))
379
+
380
+// 12. 投诉涉及班组情况-条形图
381
+const complaintTeamOpt = computed(() => ({
382
+  grid: { top: 15, bottom: 20, left: 100, right: 30 },
383
+  xAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
384
+  yAxis: { type: 'category', data: ['屹动班组', '新训队', '拓新班组', '芮茜班组', '木兰班组', '空乘', '安行班组', '安平班组'], axisLabel: { color: '#1e293b', fontSize: 11 }, axisLine: { lineStyle: { color: axisLineColor } } },
385
+  series: [{ type: 'bar', data: [50, 1, 30, 45, 19, 19, 22, 37], barWidth: '55%', itemStyle: { color: '#ef4444', borderRadius: [0, 5, 5, 0] }, label: { show: true, position: 'right', color: '#1e293b', fontSize: 11 } }],
386
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
387
+}))
388
+
389
+// 13. 不安全事件发生对比-柱状图
390
+const unsafeCompareOpt = computed(() => ({
391
+  grid: { top: 15, bottom: 25, left: 40, right: 15 },
392
+  xAxis: { type: 'category', data: ['安平班组', '安行班组', '木兰班组', '芮茜班组', '拓新班组', '屹动班组'], axisLabel: { color: axisLabelColor, fontSize: 11, rotate: 15 }, axisLine: { lineStyle: { color: axisLineColor } } },
393
+  yAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
394
+  series: [{ type: 'bar', data: [3, 4, 4, 8, 3, 2], barWidth: '55%', itemStyle: { color: '#3b82f6', borderRadius: [5, 5, 0, 0] }, label: { show: true, position: 'top', color: '#1e293b', fontSize: 11 } }],
395
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
396
+}))
397
+
398
+// 14. 亚健康人数占比-环形图
399
+const subhealthPieOpt = computed(() => ({
400
+  tooltip: { trigger: 'item', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } },
401
+  series: [{
402
+    type: 'pie', radius: ['35%', '60%'], center: ['50%', '50%'],
403
+    data: [
404
+      { value: 25, name: '屹动班组', itemStyle: { color: '#06b6d4' } },
405
+      { value: 23, name: '安平班组', itemStyle: { color: '#22c55e' } },
406
+      { value: 28, name: '拓新班组', itemStyle: { color: '#f59e0b' } },
407
+      { value: 27, name: '芮茜班组', itemStyle: { color: '#ec4899' } },
408
+      { value: 13, name: '木兰班组', itemStyle: { color: '#a78bfa' } },
409
+      { value: 31, name: '其他', itemStyle: { color: '#f97316' } }
410
+    ],
411
+    label: { show: true, color: '#475569', fontSize: 11, formatter: '{b}: {c} ({d}%)' },
412
+    labelLine: { lineStyle: { color: '#cbd5e1' } },
413
+    emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.1)' } }
414
+  }]
415
+}))
416
+
417
+// 15. 各班组健康与亚健康比例-双柱图
418
+const healthRatioOpt = computed(() => ({
419
+  grid: { top: 20, bottom: 25, left: 45, right: 15 },
420
+  legend: { data: ['健康人员', '亚健康人员'], textStyle: { color: '#475569', fontSize: 11 }, top: 0 },
421
+  xAxis: { type: 'category', data: ['安平班组', '安行班组', '木兰班组', '芮茜班组', '拓新班组', '屹动班组'], axisLabel: { color: axisLabelColor, fontSize: 11, rotate: 15 }, axisLine: { lineStyle: { color: axisLineColor } } },
422
+  yAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
423
+  series: [
424
+    { name: '健康人员', type: 'bar', data: [35, 20, 29, 26, 22, 27], barWidth: '30%', barGap: '10%', itemStyle: { color: '#3b82f6', borderRadius: [5, 5, 0, 0] } },
425
+    { name: '亚健康人员', type: 'bar', data: [23, 29, 13, 27, 28, 25], barWidth: '30%', itemStyle: { color: '#ef4444', borderRadius: [5, 5, 0, 0] } }
426
+  ],
427
+  tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
428
+}))
429
+
430
+// 16. 年龄分布-柱状图
431
+const ageDistOpt = computed(() => {
432
+  const ages = Array.from({ length: 23 }, (_, i) => i + 23)
433
+  const data = [8, 12, 18, 24, 30, 36, 41, 36, 33, 28, 22, 18, 14, 10, 8, 6, 4, 3, 2, 2, 1, 1, 1]
434
+  return {
435
+    grid: { top: 15, bottom: 25, left: 40, right: 15 },
436
+    xAxis: { type: 'category', data: ages, axisLabel: { color: axisLabelColor, fontSize: 10, interval: 2 }, axisLine: { lineStyle: { color: axisLineColor } } },
437
+    yAxis: { type: 'value', axisLabel: { color: axisLabelColor, fontSize: 11 }, splitLine: { lineStyle: { color: splitLineColor } } },
438
+    series: [{ type: 'bar', data, barWidth: '60%', itemStyle: { color: '#3b82f6', borderRadius: [3, 3, 0, 0] } }],
439
+    tooltip: { trigger: 'axis', backgroundColor: tooltipBg, borderColor: tooltipBorder, textStyle: { color: tooltipText } }
440
+  }
441
+})
442
+
443
+// ─── 挂载图表 ─────────────────────────────────────
444
+useECharts(supervisionLineRef, supervisionLineOpt)
445
+useECharts(problemTypeRef, problemTypeOpt)
446
+useECharts(teamSupervisionRef, teamSupervisionOpt)
447
+useECharts(areaPieRef, areaPieOpt)
448
+useECharts(teamRealtimeRef, teamRealtimeOpt)
449
+useECharts(realtimeLineRef, realtimeLineOpt)
450
+useECharts(yearRingRef, yearRingOpt)
451
+useECharts(yearLimitRef, yearLimitOpt)
452
+useECharts(difficultyRef, difficultyOpt)
453
+useECharts(interceptItemsRef, interceptItemsOpt)
454
+useECharts(servicePatrolRef, servicePatrolOpt)
455
+useECharts(servicePatrolLineRef, servicePatrolLineOpt)
456
+useECharts(complaintTeamRef, complaintTeamOpt)
457
+useECharts(unsafeCompareRef, unsafeCompareOpt)
458
+useECharts(subhealthPieRef, subhealthPieOpt)
459
+useECharts(healthRatioRef, healthRatioOpt)
460
+useECharts(ageDistRef, ageDistOpt)
461
+
462
+// ─── 排行榜数据 ──────────────────────────────────
463
+const superVisionTop3 = [
464
+  { name: '徐皓迪', num: 7, color: '#3b82f6' },
465
+  { name: '张悦', num: 7, color: '#22c55e' },
466
+  { name: '匡林', num: 5, color: '#f59e0b' }
467
+]
468
+const superVisionList = [
469
+  { name: '周雨浓', num: 4 }, { name: '蒲越', num: 5 }, { name: '傅建', num: 6 },
470
+  { name: '李明', num: 4 }, { name: '王芳', num: 3 }, { name: '赵强', num: 4 },
471
+  { name: '刘洋', num: 3 }, { name: '陈静', num: 5 }, { name: '杨磊', num: 4 }, { name: '吴倩', num: 3 }
472
+]
473
+const vulnTop3 = [
474
+  { name: '安俊', num: 11, color: '#ec4899' },
475
+  { name: '黄鑫', num: 11, color: '#a78bfa' },
476
+  { name: '廖艺森', num: 9, color: '#f97316' }
477
+]
478
+const vulnList = [
479
+  { name: '李敏', num: 4 }, { name: '林', num: 8 }, { name: '宇', num: 8 },
480
+  { name: '张华', num: 6 }, { name: '王丽', num: 5 }, { name: '赵明', num: 7 },
481
+  { name: '陈红', num: 4 }, { name: '刘伟', num: 6 }, { name: '杨婷', num: 5 }, { name: '吴刚', num: 4 }
482
+]
483
+const bonusTop3 = [
484
+  { name: '文渊', num: 24.5, color: '#3b82f6' },
485
+  { name: '杨晨汐', num: 34.5, color: '#22c55e' },
486
+  { name: '何欧怡', num: 16, color: '#f59e0b' }
487
+]
488
+const bonusList = [
489
+  { name: '杨林', num: 4 }, { name: '马灿', num: 5 }, { name: '蒋灿', num: 6 },
490
+  { name: '谢涛', num: 3.5 }, { name: '韩雪', num: 4.5 }, { name: '唐亮', num: 5.5 },
491
+  { name: '曹琳', num: 3 }, { name: '邓超', num: 4 }, { name: '彭波', num: 6.5 }, { name: '冯娟', num: 3.5 }
492
+]
493
+const seizureTop3 = [
494
+  { name: '苗苗', num: 113, color: '#ec4899' },
495
+  { name: '黄真', num: 90, color: '#a78bfa' },
496
+  { name: '王磊', num: 78, color: '#f97316' }
497
+]
498
+const seizureList = [
499
+  { name: '李兆厚', num: 4 }, { name: '黄元宏', num: 5 }, { name: '何跃智', num: 6 },
500
+  { name: '周文', num: 7 }, { name: '吴斌', num: 5 }, { name: '郑丽', num: 8 },
501
+  { name: '孙强', num: 4 }, { name: '朱敏', num: 6 }, { name: '沈涛', num: 5 }, { name: '贺军', num: 7 }
502
+]
503
+
504
+// ─── 挂载顶部迷你折线图 ──────────────────────────
505
+onMounted(() => {
506
+  metricCards.value.forEach(m => {
507
+    if (m._sparkRef) {
508
+      const chart = echarts.init(m._sparkRef)
509
+      chart.setOption({
510
+        grid: { top: 0, bottom: 0, left: 0, right: 0 },
511
+        xAxis: { show: false, type: 'category', data: ['', '', '', '', '', '', '', '', '', ''] },
512
+        yAxis: { show: false },
513
+        series: [{ type: 'line', data: [3, 5, 2, 8, 4, 7, 3, 6, 4, 5], smooth: true, lineStyle: { color: m.sparkColor, width: 2 }, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: m.sparkColor + '33' }, { offset: 1, color: m.sparkColor + '00' }] } }, symbol: 'none' }]
514
+      })
515
+    }
516
+  })
517
+})
518
+</script>
519
+
520
+<style lang="scss" scoped>
521
+.org-profile {
522
+  padding: 15px;
523
+  box-sizing: border-box;
524
+  min-height: calc(100vh - 90px);
525
+
526
+  // ─ 顶部指标卡片 ──
527
+  .metric-row {
528
+    display: flex;
529
+    gap: 12px;
530
+    margin-bottom: 15px;
531
+
532
+    .metric-card {
533
+      display: flex;
534
+      flex-direction: column;
535
+      justify-content: space-between;
536
+      flex: 1;
537
+      border-radius: 12px;
538
+      padding: 12px 16px;
539
+      min-height: 100px;
540
+
541
+      .metric-title {
542
+        font-size: 13px;
543
+        color: #475569;
544
+        font-weight: 500;
545
+        margin-bottom: 4px;
546
+      }
547
+
548
+      .metric-value {
549
+        font-size: 36px;
550
+        font-weight: bold;
551
+        line-height: 1.2;
552
+      }
553
+
554
+      .metric-change-row {
555
+        display: flex;
556
+        align-items: flex-end;
557
+        justify-content: space-between;
558
+        margin-top: auto;
559
+
560
+        .metric-change-info {
561
+          display: flex;
562
+          flex-direction: column;
563
+          gap: 2px;
564
+
565
+          .metric-change-label {
566
+            font-size: 12px;
567
+            color: #64748b;
568
+          }
569
+
570
+          .metric-change {
571
+            font-size: 13px;
572
+            display: inline-flex;
573
+            align-items: center;
574
+            gap: 4px;
575
+            font-weight: 500;
576
+
577
+            &::before {
578
+              content: '';
579
+              display: inline-block;
580
+              width: 0;
581
+              height: 0;
582
+              vertical-align: middle;
583
+            }
584
+
585
+            &.change-down::before {
586
+              border-left: 5px solid transparent;
587
+              border-right: 5px solid transparent;
588
+              border-top: 7px solid currentColor;
589
+            }
590
+
591
+            &.change-up::before {
592
+              border-left: 5px solid transparent;
593
+              border-right: 5px solid transparent;
594
+              border-bottom: 7px solid currentColor;
595
+            }
596
+
597
+            &.change-flat::before {
598
+              width: 10px;
599
+              height: 0;
600
+              border-top: 3px solid currentColor;
601
+              border-radius: 1px;
602
+            }
603
+          }
604
+        }
605
+
606
+        .metric-chart {
607
+          height: 30px;
608
+          width: 80px;
609
+        }
610
+      }
611
+    }
612
+
613
+    .metric-side-cards {
614
+      display: flex;
615
+      flex-direction: column;
616
+      gap: 8px;
617
+      width: 200px;
618
+      flex-shrink: 0;
619
+
620
+      .metric-card {
621
+        flex: 1;
622
+        min-height: 50px;
623
+        justify-content: center;
624
+
625
+        .metric-title {
626
+          font-size: 12px;
627
+          color: #fff;
628
+          margin-bottom: 2px;
629
+        }
630
+
631
+        .metric-value {
632
+          font-size: 24px;
633
+        }
634
+      }
635
+    }
636
+
637
+    .metric-warning-top {
638
+      background: linear-gradient(135deg, #f59e0b, #d97706) !important;
639
+    }
640
+
641
+    .metric-warning-bottom {
642
+      background: linear-gradient(135deg, #fbbf24, #f59e0b) !important;
643
+    }
644
+  }
645
+
646
+  // ── 图表区域 ──
647
+  .charts-row {
648
+    display: flex;
649
+    gap: 15px;
650
+
651
+    .charts-col-main {
652
+      flex: 1;
653
+      display: flex;
654
+      flex-direction: column;
655
+      gap: 12px;
656
+      min-width: 0;
657
+    }
658
+
659
+    .charts-grid {
660
+      display: grid;
661
+      gap: 12px;
662
+
663
+      &.charts-grid-2 {
664
+        grid-template-columns: 1fr 1fr;
665
+      }
666
+
667
+      &.charts-grid-1 {
668
+        grid-template-columns: 1fr;
669
+      }
670
+    }
671
+
672
+    .chart-box {
673
+      width: 100%;
674
+      height: 220px;
675
+    }
676
+
677
+    .chart-box-wide {
678
+      width: 100%;
679
+      height: 260px;
680
+    }
681
+
682
+    .charts-col-side {
683
+      width: 320px;
684
+      flex-shrink: 0;
685
+      display: flex;
686
+      flex-direction: column;
687
+      gap: 12px;
688
+    }
689
+  }
690
+
691
+  // ── 排行榜 ──
692
+  .rank-list {
693
+    .rank-avatar-row {
694
+      display: flex;
695
+      justify-content: center;
696
+      gap: 20px;
697
+      margin-top: 15px;
698
+      margin-bottom: 12px;
699
+
700
+      .rank-avatar-item {
701
+        display: flex;
702
+        flex-direction: column;
703
+        align-items: center;
704
+        gap: 6px;
705
+
706
+        &:nth-child(2) {
707
+          transform: translateY(-10px);
708
+        }
709
+
710
+        .rank-avatar-wrapper {
711
+          position: relative;
712
+          display: inline-flex;
713
+          align-items: center;
714
+          justify-content: center;
715
+          border-radius: 50%;
716
+
717
+
718
+          &.rank-pos-1 {
719
+            border: 3px solid #94a3b8;
720
+
721
+            .rank-badge {
722
+              background: #94a3b8;
723
+            }
724
+
725
+          }
726
+
727
+          &.rank-pos-2 {
728
+            border: 3px solid #fbbf24;
729
+
730
+            .rank-badge {
731
+              background: #fbbf24;
732
+            }
733
+          }
734
+
735
+          &.rank-pos-3 {
736
+            border: 3px solid #fb923c;
737
+
738
+            .rank-badge {
739
+              background: #fb923c;
740
+            }
741
+          }
742
+
743
+          .rank-avatar {
744
+            width: 44px;
745
+            height: 44px;
746
+            border-radius: 50%;
747
+            display: flex;
748
+            align-items: center;
749
+            justify-content: center;
750
+            color: #fff;
751
+            font-size: 16px;
752
+            font-weight: bold;
753
+          }
754
+
755
+          .rank-badge {
756
+            position: absolute;
757
+            bottom: -8px;
758
+            left: 50%;
759
+            transform: translateX(-50%);
760
+            width: 12px;
761
+            height: 12px;
762
+            border-radius: 50%;
763
+            display: flex;
764
+            align-items: center;
765
+            justify-content: center;
766
+            font-size: 11px;
767
+            font-weight: bold;
768
+            color: #fff;
769
+          }
770
+        }
771
+
772
+        .rank-name {
773
+          font-size: 12px;
774
+          color: #475569;
775
+        }
776
+
777
+        .rank-num {
778
+          font-size: 18px;
779
+          color: '#1e293b';
780
+          font-weight: bold;
781
+        }
782
+      }
783
+    }
784
+
785
+    .rank-table {
786
+      .rank-tr {
787
+        display: flex;
788
+        padding: 5px 0;
789
+        font-size: 12px;
790
+        color: #64748b;
791
+        border-bottom: 1px solid #e2e8f0;
792
+
793
+        span {
794
+          flex: 1;
795
+          text-align: center;
796
+        }
797
+      }
798
+
799
+      .rank-th {
800
+        color: '#1e293b';
801
+        font-weight: bold;
802
+        border-bottom: 2px solid #cbd5e1;
803
+      }
804
+    }
805
+  }
806
+}
807
+</style>

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

@@ -139,7 +139,7 @@ const fetchData = (params) => {
139 139
 
140 140
 watch(() => props.queryParams, (newParams) => {
141 141
   fetchData(newParams)
142
-}, { deep: true })
142
+}, { deep: true, immediate: true })
143 143
 
144 144
 
145 145
 </script>

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

@@ -205,6 +205,12 @@
205 205
                 value-format="YYYY-MM-DD" style="width: 100%;" />
206 206
             </el-form-item>
207 207
           </el-col>
208
+           <el-col :span="12">
209
+            <el-form-item label="X射线操作员开始时间" prop="xrayOperatorStarttime" label-width="150px">
210
+              <el-date-picker v-model="form.xrayOperatorStarttime" type="date" placeholder="请选择X射线操作员开始时间"
211
+                value-format="YYYY-MM-DD" style="width: 100%;" />
212
+            </el-form-item>
213
+          </el-col>
208 214
           <el-col :span="12">
209 215
             <el-form-item label="曾在安检工作中担任的最高职务" prop="securityInspectionPosition" label-width="220px">
210 216
               <el-select v-model="form.securityInspectionPosition" placeholder="请选择曾在安检工作中担任的最高职务">

+ 720 - 0
src/views/warningManage/redLineWarning/index.vue

@@ -0,0 +1,720 @@
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-tree-select v-model="selectedOrg" :data="cascadeOptions"
32
+                    :props="{ label: 'label', value: 'value', children: 'children' }" node-key="value"
33
+                    placeholder="组织架构/员工" clearable filterable check-strictly style="width: 480px;"
34
+                     />
35
+            </div>
36
+            <div class="filter-group">
37
+                <el-select v-model="selectedAlertLevel" placeholder="预警等级" clearable style="width: 250px;">
38
+                    <el-option label="全部" value=""></el-option>
39
+                    <el-option v-for="item in alert_level" :key="item.value" :label="item.label" :value="item.value" />
40
+                </el-select>
41
+                <el-button type="primary" @click="handleSearch">搜索</el-button>
42
+                <el-button @click="handleReset">重置</el-button>
43
+            </div>
44
+        </div>
45
+
46
+        <div class="employee-section">
47
+            <div class="section-title">
48
+                <i class="fas fa-users" style="color:#dc2626;"></i> 员工综合预警
49
+                <span
50
+                    style="font-size:0.7rem; background:#eef2ff; padding:4px 12px; border-radius:30px; margin-left:12px;">
51
+                    评分依据:员工配分表
52
+                </span>
53
+            </div>
54
+            <div class="employee-card">
55
+                <div style="overflow-x: auto;">
56
+                    <el-table :data="filteredEmployees" :row-class-name="getRowClass" style="width:100%;" border stripe>
57
+                        <el-table-column prop="userId" label="员工ID" />
58
+                        <el-table-column prop="nickName" label="姓名">
59
+                            <template #default="{ row }">
60
+                                <strong>{{ row.nickName }}</strong>
61
+                            </template>
62
+                        </el-table-column>
63
+                        <el-table-column prop="deptName" label="所属部门" />
64
+                        <el-table-column prop="eventTime" label="事件时间" sortable width="180" />
65
+                        <el-table-column prop="dimName" label="维度名称" />
66
+                        <el-table-column prop="secondIndicator" label="二级指标名称" />
67
+                        <el-table-column prop="deductionTotal" label="扣分分值合计" sortable />
68
+                        <el-table-column prop="occurrenceCount" label="发生次数" sortable />
69
+                        <el-table-column prop="overallScore" label="预警等级">
70
+                            <template #default="{ row }">
71
+                                <span v-if="row.overallScore < 75" class="status-badge"
72
+                                    style="animation: subtlePulse 1s infinite;"><i
73
+                                        class="fas fa-exclamation-triangle"></i> 红色预警</span>
74
+                                <span v-else-if="row.overallScore >= 90" class="status-excellent"
75
+                                    style="background:#d1fae5; color:#065f46;"><i class="fas fa-star"></i>
76
+                                    优秀标杆</span>
77
+                                <span v-else class="status-warning">正常范围</span>
78
+                            </template>
79
+                        </el-table-column>
80
+                        <el-table-column prop="coreRisk" label="核心风险" />
81
+                        <el-table-column prop="statusLabel" label="状态标签">
82
+                            <template #default="{ row }">
83
+                                <span v-if="row.overallScore < 75" style="color:#b91c1c;"><i class="fas fa-bell"></i>
84
+                                    {{ getAlertLabel(row.statusLabel) }}</span>
85
+                                <span v-else-if="row.overallScore >= 90" style="color:#15803d;"><i
86
+                                        class="fas fa-crown"></i> {{ getAlertLabel(row.statusLabel) }}</span>
87
+                                <span v-else>{{ getAlertLabel(row.statusLabel) }}</span>
88
+                            </template>
89
+                        </el-table-column>
90
+                    </el-table>
91
+                    <el-pagination v-show="total > 0" :total="total" v-model:current-page="queryParams.pageNum"
92
+                        v-model:page-size="queryParams.pageSize" :page-sizes="[10, 20, 50, 100]"
93
+                        layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
94
+                        @current-change="handlePageChange" style="margin-top: 16px; justify-content: flex-end;" />
95
+                </div>
96
+                <div class="warning-summary">
97
+                    <div><i class="fas fa-circle" style="color:#ef4444;"></i> <strong>红色预警</strong> (综合评估 &lt; 75分) 共计
98
+                        {{ redAlertCount }} 人 → 立即约谈/培训</div>
99
+                    <div><i class="fas fa-circle" style="color:#22c55e;"></i> <strong>优秀标杆</strong> (综合评估 ≥ 90分) 共计 {{
100
+                        excellentCount }} 人 → 表彰激励</div>
101
+                    <div><i class="fas fa-chart-simple"></i> 全员平均综合得分: <strong>{{ avgScore }}</strong> 分</div>
102
+                </div>
103
+            </div>
104
+        </div>
105
+
106
+        <footer>
107
+            <i class="far fa-bell"></i> 员工部门:旅检一/二/三部 | 阈值:<75红色预警 | ≥90优秀
108
+        </footer>
109
+    </div>
110
+</template>
111
+
112
+<script setup>
113
+import { ref, computed, reactive, onMounted, watch } from 'vue'
114
+import { getDeptUserTree } from '@/api/item/items'
115
+import { getRedLineWarningPageData } from '@/api/warningManage/redLineWarning'
116
+import { useDict } from '@/utils/dict'
117
+import { useRoute } from 'vue-router'
118
+
119
+const route = useRoute()
120
+
121
+const { alert_level } = useDict('alert_level')
122
+
123
+const dateRangeInput = ref(null)
124
+const activeRange = ref('month')
125
+
126
+const startDate = ref(null)
127
+const endDate = ref(null)
128
+const selectedAlertLevel = ref('')
129
+const selectedOrg = ref('')
130
+const cascadeOptions = ref([])
131
+
132
+
133
+
134
+const newNames = [
135
+    "刘孟", "王苡衡", "周海涵", "何欣怡", "王崎",
136
+    "黄政", "刘韵儿", "汪安霖", "张诗敏", "张维", "陈凯琳"
137
+]
138
+const deptList = ["旅检一部", "旅检二部", "旅检三部"]
139
+const scores = [68, 92, 55, 96, 72, 88, 48, 94, 81, 63, 91]
140
+const coreRisksOrOutstandingAchievementsList = [
141
+    "违规操作2次+投诉1起,考试成绩62分",
142
+    "优秀服务案例,安保测试满分,无违规",
143
+    "安保测试未通过,不安全事件责任人",
144
+    "典型服务案例主导者,考试成绩98",
145
+    "质控拦截违规2次,考试成绩74分",
146
+    "考试成绩89分,服务巡查良好",
147
+    "严重不规范操作,安保测试未过",
148
+    "质控拦截贡献突出,考试成绩96",
149
+    "表现良好,无安全事故,考试86分",
150
+    "服务巡查扣分,投诉关联2件",
151
+    "临近预警线,服务巡查扣分1次"
152
+]
153
+
154
+const dimNames = ["安全管理", "服务质量", "运行效率", "队伍建设"]
155
+const indicatorNames = ["安全违规次数", "旅客投诉率", "安检差错率", "培训合格率", "操作规范度"]
156
+const deductionScores = [2, 5, 3, 1, 4]
157
+const occurrenceCounts = [1, 2, 3, 1, 2]
158
+
159
+const employeesData = ref([])
160
+const queryParams = reactive({
161
+    pageNum: 1,
162
+    pageSize: 10
163
+})
164
+
165
+for (let i = 0; i < newNames.length; i++) {
166
+    employeesData.value.push({
167
+        userId: String(10021 + i),
168
+        nickName: newNames[i],
169
+        deptName: deptList[i % deptList.length],
170
+        eventTime: `2026-0${(i % 9) + 1}-${String((i * 3 + 10) % 28 + 1).padStart(2, '0')}`,
171
+        overallScore: scores[i % scores.length],
172
+        dimName: dimNames[i % dimNames.length],
173
+        secondIndicator: indicatorNames[i % indicatorNames.length],
174
+        deductionTotal: deductionScores[i % deductionScores.length],
175
+        occurrenceCount: occurrenceCounts[i % occurrenceCounts.length],
176
+        coreRisk: coreRisksOrOutstandingAchievementsList[i % coreRisksOrOutstandingAchievementsList.length],
177
+        statusLabel: scores[i % scores.length] < 75 ? '1' : scores[i % scores.length] >= 90 ? '2' : '3'
178
+    })
179
+}
180
+
181
+// 将组织架构数据转换为树形数据
182
+const transformCascadeData = (nodes) => {
183
+    if (!nodes) return []
184
+    return nodes.map(node => {
185
+        const deptType = node.deptType || node.nodeType
186
+        const label = deptType === 'user'
187
+            ? (node.nickName || node.label)
188
+            : (node.deptName || node.name || node.label)
189
+
190
+        let value;
191
+        if (deptType === 'STATION') value = `station_${node.id}`;
192
+        else if (deptType === 'BRIGADE') value = `dept_${node.id}`
193
+        else if (deptType === 'MANAGER') value = `team_${node.id}`
194
+        else if (deptType === 'TEAMS') value = `group_${node.id}`
195
+        else value = `user_${node.id}`
196
+
197
+        const children = node.children && node.children.length > 0 ? transformCascadeData(node.children) : undefined
198
+
199
+        return {
200
+            label,
201
+            value,
202
+            deptType,
203
+            children
204
+        }
205
+    })
206
+}
207
+
208
+// 获取选中的部门或用户信息
209
+const getSelectedInfo = (selectedValue) => {
210
+    if (!selectedValue) return null
211
+
212
+    const findNodeByValue = (nodes, value) => {
213
+        for (const node of nodes) {
214
+            if (node.value === value) {
215
+                return node
216
+            }
217
+            if (node.children) {
218
+                const found = findNodeByValue(node.children, value)
219
+                if (found) return found
220
+            }
221
+        }
222
+        return null
223
+    }
224
+
225
+    return findNodeByValue(cascadeOptions.value, selectedValue)
226
+}
227
+
228
+const allFilteredEmployees = computed(() => {
229
+    let result = employeesData.value.ledgerWarningDetailItemList || []
230
+
231
+    // 预警等级筛选
232
+    if (selectedAlertLevel.value) {
233
+        // 根据字典值判断筛选条件(红色预警 <75,优秀标杆 >=90,正常范围 75-89)
234
+        // 可以根据字典的 value 或 label 来判断
235
+        const alertItem = alert_level.value.find(item => item.value === selectedAlertLevel.value)
236
+        if (alertItem) {
237
+            const label = alertItem.label
238
+            if (label.includes('红色') || label.includes('预警')) {
239
+                result = result.filter(emp => emp.overallScore < 75)
240
+            } else if (label.includes('优秀') || label.includes('标杆')) {
241
+                result = result.filter(emp => emp.overallScore >= 90)
242
+            } else if (label.includes('正常')) {
243
+                result = result.filter(emp => emp.overallScore >= 75 && emp.overallScore < 90)
244
+            }
245
+        }
246
+    }
247
+
248
+    return result
249
+})
250
+
251
+const filteredEmployees = computed(() => {
252
+    const start = (queryParams.pageNum - 1) * queryParams.pageSize
253
+    const end = start + queryParams.pageSize
254
+    return allFilteredEmployees.value.slice(start, end)
255
+})
256
+
257
+const total = computed(() => allFilteredEmployees.value.length)
258
+
259
+function handlePageChange(newPage) {
260
+    queryParams.pageNum = newPage
261
+}
262
+
263
+function handleSizeChange(newSize) {
264
+    queryParams.pageSize = newSize
265
+    queryParams.pageNum = 1
266
+}
267
+
268
+const redAlertCount = computed(() => {
269
+    return employeesData.value?.redAlertNum || 0
270
+})
271
+
272
+const excellentCount = computed(() => {
273
+    return employeesData.value?.excellentBenchmarkNum || 0
274
+})
275
+
276
+const avgScore = computed(() => {
277
+    return employeesData.value?.averageComprehensiveScore || 0
278
+})
279
+
280
+const getRowClass = ({ row }) => {
281
+    if (row.overallScore < 75) return "employee-warning-row"
282
+    if (row.overallScore >= 90) return "employee-excellent-row"
283
+    return ""
284
+}
285
+
286
+const getAlertLabel = (value) => {
287
+    if (!value) return ''
288
+    const item = alert_level.value.find(d => d.value === value)
289
+    return item ? item.label : value
290
+}
291
+
292
+const setActiveRange = (range) => {
293
+    activeRange.value = range
294
+    // 清除自定义日期
295
+    if (range !== 'custom') {
296
+        startDate.value = null
297
+        endDate.value = null
298
+    }
299
+}
300
+
301
+const handleSearch = () => {
302
+    fetchWarningData()
303
+}
304
+
305
+const handleReset = () => {
306
+    startDate.value = null
307
+    endDate.value = null
308
+    activeRange.value = 'month'
309
+    selectedAlertLevel.value = ''
310
+    selectedOrg.value = ''
311
+    queryParams.pageNum = 1
312
+    fetchWarningData()
313
+}
314
+
315
+
316
+const formatDate = (d) => {
317
+    if (!d) return ''
318
+    const date = new Date(d)
319
+    const y = date.getFullYear()
320
+    const m = String(date.getMonth() + 1).padStart(2, '0')
321
+    const day = String(date.getDate()).padStart(2, '0')
322
+    return `${y}-${m}-${day}`
323
+}
324
+
325
+const getDateRangeFromActive = () => {
326
+    const now = new Date()
327
+    let start = new Date(now)
328
+    if (activeRange.value === 'week') {
329
+        start.setDate(now.getDate() - 7)
330
+    } else if (activeRange.value === 'month') {
331
+        start.setMonth(now.getMonth() - 1)
332
+    } else if (activeRange.value === 'quarter') {
333
+        start.setMonth(now.getMonth() - 3)
334
+    } else {
335
+        start.setFullYear(now.getFullYear() - 1)
336
+    }
337
+    return { startDate: formatDate(start), endDate: formatDate(now) }
338
+}
339
+
340
+
341
+
342
+const fetchWarningData = async () => {
343
+    queryParams.pageNum = 1
344
+    let params = {}
345
+    if (startDate.value && endDate.value) {
346
+        params.startDate = formatDate(startDate.value)
347
+        params.endDate = formatDate(endDate.value)
348
+    } else {
349
+        const range = getDateRangeFromActive()
350
+        params.startDate = range.startDate
351
+        params.endDate = range.endDate
352
+    }
353
+
354
+    const selectedInfo = getSelectedInfo(selectedOrg.value)
355
+    if (selectedInfo) {
356
+        const rawId = Number(selectedInfo.value.split('_')[1])
357
+
358
+        if (selectedInfo.deptType === 'BRIGADE') params.deptId = rawId
359
+        else if (selectedInfo.deptType === 'MANAGER') params.teamId = rawId
360
+        else if (selectedInfo.deptType === 'TEAMS') params.groupId = rawId
361
+        else if (selectedInfo.deptType === 'user') params.userId = rawId
362
+    }
363
+
364
+    try {
365
+        const r1 = await getRedLineWarningPageData(params)
366
+        if (r1.data) {
367
+            employeesData.value = r1.data
368
+        }
369
+    } catch (error) {
370
+        console.error('获取预警数据失败:', error)
371
+    }
372
+}
373
+
374
+onMounted(async () => {
375
+    try {
376
+        const res = await getDeptUserTree()
377
+        if (res.data) {
378
+            cascadeOptions.value = transformCascadeData(res.data)
379
+            console.log(cascadeOptions.value, "cascadeOptions")
380
+        }
381
+    } catch (error) {
382
+        console.error('获取组织架构数据失败:', error)
383
+    }
384
+    fetchWarningData()
385
+})
386
+
387
+// 监听路由参数变化,回显查询条件并查询
388
+watch(() => route.query, (query) => {
389
+    const { id, startDate: sd, endDate: ed, activeRange: ar, alertLevel, org } = query
390
+
391
+    // 回显员工选择
392
+    if (id) {
393
+        selectedOrg.value = `user_${id}`
394
+    } else if (org) {
395
+        selectedOrg.value = org
396
+    } else {
397
+        selectedOrg.value = ''
398
+    }
399
+
400
+    // 回显日期范围
401
+    if (sd && ed) {
402
+        startDate.value = sd
403
+        endDate.value = ed
404
+        activeRange.value = 'custom'
405
+    } else {
406
+        startDate.value = null
407
+        endDate.value = null
408
+        activeRange.value = ar || 'month'
409
+    }
410
+
411
+    // 回显预警等级
412
+    selectedAlertLevel.value = alertLevel || ''
413
+
414
+    queryParams.pageNum = 1
415
+    fetchWarningData()
416
+}, { immediate: true })
417
+</script>
418
+
419
+<style scoped>
420
+@keyframes subtlePulse {
421
+    0% {
422
+        opacity: 0.7;
423
+    }
424
+
425
+    50% {
426
+        opacity: 1;
427
+        background: #ffb3b3;
428
+    }
429
+
430
+    100% {
431
+        opacity: 0.7;
432
+    }
433
+}
434
+
435
+
436
+
437
+.dashboard {
438
+    max-width: 100%;
439
+    padding: 10px;
440
+}
441
+
442
+.header {
443
+    margin-bottom: 28px;
444
+    display: flex;
445
+    justify-content: space-between;
446
+    align-items: flex-end;
447
+    flex-wrap: wrap;
448
+    gap: 16px;
449
+}
450
+
451
+.title-section h1 {
452
+    font-size: 1.7rem;
453
+    font-weight: 700;
454
+    background: linear-gradient(135deg, #1e3c72, #2a5298);
455
+    -webkit-background-clip: text;
456
+    background-clip: text;
457
+    color: transparent;
458
+    letter-spacing: -0.3px;
459
+}
460
+
461
+.title-section p {
462
+    color: #475569;
463
+    margin-top: 6px;
464
+    font-size: 0.85rem;
465
+}
466
+
467
+.badge-group {
468
+    display: flex;
469
+    gap: 12px;
470
+}
471
+
472
+.alert-badge {
473
+    background: #fff1f0;
474
+    border-left: 5px solid #ef4444;
475
+    padding: 6px 16px;
476
+    border-radius: 40px;
477
+    font-weight: 600;
478
+    font-size: 0.85rem;
479
+}
480
+
481
+.alert-badge i {
482
+    color: #ef4444;
483
+    margin-right: 6px;
484
+}
485
+
486
+.filter-bar {
487
+    background: white;
488
+    border-radius: 60px;
489
+    padding: 8px 20px;
490
+    margin-bottom: 10px;
491
+    display: flex;
492
+    flex-wrap: wrap;
493
+    align-items: center;
494
+    justify-content: space-between;
495
+    gap: 16px;
496
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02), 0 1px 2px rgba(0, 0, 0, 0.03);
497
+    border: 1px solid #eef2f6;
498
+}
499
+
500
+.time-range {
501
+    display: flex;
502
+    gap: 8px;
503
+    align-items: center;
504
+    flex-wrap: wrap;
505
+}
506
+
507
+.time-btn {
508
+    background: #f8fafc;
509
+    border: 1px solid #e2e8f0;
510
+    padding: 6px 18px;
511
+    border-radius: 40px;
512
+    font-size: 0.8rem;
513
+    font-weight: 500;
514
+    cursor: pointer;
515
+    transition: all 0.2s;
516
+    color: #1e293b;
517
+}
518
+
519
+.time-btn.active {
520
+    background: #2563eb;
521
+    border-color: #2563eb;
522
+    color: white;
523
+}
524
+
525
+.time-btn:hover {
526
+    background: #e2e8f0;
527
+}
528
+
529
+.custom-date {
530
+    display: flex;
531
+    align-items: center;
532
+    gap: 8px;
533
+}
534
+
535
+.clear-btn {
536
+    background: transparent;
537
+    border: 1px solid #e2e8f0;
538
+    padding: 4px 8px;
539
+    border-radius: 40px;
540
+    cursor: pointer;
541
+    color: #64748b;
542
+}
543
+
544
+.clear-btn:hover {
545
+    background: #f1f5f9;
546
+}
547
+
548
+.custom-date :deep(.el-date-picker) {
549
+    background: transparent;
550
+}
551
+
552
+.custom-date :deep(.el-input__wrapper) {
553
+    background: transparent;
554
+
555
+
556
+}
557
+
558
+.filter-group {
559
+    display: flex;
560
+    gap: 12px;
561
+    align-items: center;
562
+}
563
+
564
+.search-wrapper {
565
+    display: flex;
566
+    align-items: center;
567
+    gap: 8px;
568
+    background: #f8fafc;
569
+    padding: 4px 12px;
570
+    border-radius: 40px;
571
+    border: 1px solid #e2e8f0;
572
+}
573
+
574
+.search-btn {
575
+    background: #2563eb;
576
+    border: none;
577
+    color: white;
578
+    padding: 4px 14px;
579
+    border-radius: 30px;
580
+    font-size: 0.75rem;
581
+    font-weight: 500;
582
+    cursor: pointer;
583
+    transition: 0.2s;
584
+}
585
+
586
+.search-btn:hover {
587
+    background: #1d4ed8;
588
+}
589
+
590
+
591
+
592
+.employee-section {
593
+    margin-top: 20px;
594
+    width: 100%;
595
+}
596
+
597
+.section-title {
598
+    font-size: 1.2rem;
599
+    font-weight: 700;
600
+    margin-bottom: 1rem;
601
+    display: flex;
602
+    align-items: center;
603
+    gap: 8px;
604
+    border-left: 5px solid #ef4444;
605
+    padding-left: 14px;
606
+}
607
+
608
+.employee-card {
609
+    background: #ffffff;
610
+    border-radius: 24px;
611
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03), 0 1px 2px rgba(0, 0, 0, 0.05);
612
+    border: 1px solid #eef2f6;
613
+    padding: 1rem 1.5rem;
614
+    width: 100%;
615
+}
616
+
617
+.data-table {
618
+    width: 100%;
619
+    border-collapse: collapse;
620
+    font-size: 0.8rem;
621
+}
622
+
623
+.data-table th {
624
+    text-align: left;
625
+    padding: 10px 6px 8px 0;
626
+    font-weight: 600;
627
+    color: #334155;
628
+    border-bottom: 2px solid #e2e8f0;
629
+}
630
+
631
+.data-table td {
632
+    padding: 8px 6px 8px 0;
633
+    border-bottom: 1px solid #f1f5f9;
634
+    vertical-align: middle;
635
+}
636
+
637
+.employee-warning-row {
638
+    background-color: #fff5f5;
639
+    border-left: 4px solid #ef4444;
640
+}
641
+
642
+.employee-excellent-row {
643
+    background-color: #f0fdf4;
644
+    border-left: 4px solid #22c55e;
645
+}
646
+
647
+.score-danger {
648
+    font-weight: 800;
649
+    color: #dc2626;
650
+    background: #fee2e2;
651
+    padding: 2px 8px;
652
+    border-radius: 30px;
653
+    display: inline-block;
654
+    font-size: 0.8rem;
655
+}
656
+
657
+.score-excellent {
658
+    font-weight: 800;
659
+    color: #15803d;
660
+    background: #dcfce7;
661
+    padding: 2px 8px;
662
+    border-radius: 30px;
663
+    display: inline-block;
664
+    font-size: 0.8rem;
665
+}
666
+
667
+.warning-summary {
668
+    background: #fff9f0;
669
+    border-radius: 18px;
670
+    padding: 12px 20px;
671
+    margin-top: 18px;
672
+    display: flex;
673
+    gap: 28px;
674
+    flex-wrap: wrap;
675
+    font-weight: 500;
676
+    font-size: 0.85rem;
677
+}
678
+
679
+footer {
680
+    text-align: center;
681
+    margin-top: 32px;
682
+    font-size: 0.7rem;
683
+    color: #7e8b9c;
684
+    border-top: 1px solid #e2edf7;
685
+    padding-top: 18px;
686
+}
687
+
688
+@media (max-width: 1200px) {
689
+    .cards-grid {
690
+        grid-template-columns: repeat(4, 1fr);
691
+        gap: 14px;
692
+    }
693
+}
694
+
695
+@media (max-width: 800px) {
696
+    .cards-grid {
697
+        grid-template-columns: repeat(2, 1fr);
698
+    }
699
+
700
+    body {
701
+        padding: 20px;
702
+    }
703
+
704
+    .filter-bar {
705
+        border-radius: 24px;
706
+        flex-direction: column;
707
+        align-items: stretch;
708
+    }
709
+
710
+    .org-search {
711
+        justify-content: space-between;
712
+    }
713
+}
714
+
715
+@media (max-width: 550px) {
716
+    .cards-grid {
717
+        grid-template-columns: 1fr;
718
+    }
719
+}
720
+</style>

+ 128 - 212
src/views/warningPage/index.vue

@@ -2,7 +2,7 @@
2 2
     <div class="dashboard">
3 3
         <div class="header">
4 4
             <div class="title-section">
5
-                <h1><i class="fas fa-shield-alt" style="color:#2c5282; margin-right: 8px;"></i>预警中枢</h1>
5
+                <h1><i class="fas fa-shield-alt" style="color:#2c5282; margin-right: 8px;"></i>综合预警工作台</h1>
6 6
                 <p>员工综合评估(<75分红色预警 | ≥90分优秀)</p>
7 7
             </div>
8 8
             <div class="badge-group">
@@ -28,45 +28,24 @@
28 28
                     <el-date-picker v-model="endDate" type="date" placeholder="结束日期" style="width: 150px;" />
29 29
 
30 30
                 </div>
31
-                <el-tree-select
32
-                    v-model="selectedOrg"
33
-                    :data="cascadeOptions"
34
-                    :props="{ label: 'label', value: 'value', children: 'children' }"
35
-                    node-key="value"
36
-                    placeholder="组织架构/员工"
37
-                    clearable
38
-                    filterable
39
-                    check-strictly
40
-                    style="width: 480px;"
41
-                    @change="fetchWarningData"
42
-                />
31
+                <el-tree-select v-model="selectedOrg" :data="cascadeOptions"
32
+                    :props="{ label: 'label', value: 'value', children: 'children' }" node-key="value"
33
+                    placeholder="组织架构/员工" clearable filterable check-strictly style="width: 480px;"
34
+                     />
43 35
             </div>
44 36
             <div class="filter-group">
45 37
                 <el-select v-model="selectedAlertLevel" placeholder="预警等级" clearable style="width: 250px;">
46 38
                     <el-option label="全部" value=""></el-option>
47 39
                     <el-option v-for="item in alert_level" :key="item.value" :label="item.label" :value="item.value" />
48 40
                 </el-select>
49
-
50
-            </div>
51
-        </div>
52
-
53
-        <div class="cards-container">
54
-            <div class="cards-grid" style="overflow-x: auto; white-space: nowrap;">
55
-                <div class="card" v-for="(item, index) in summaryCards" :key="index"
56
-                    style="display: inline-block; width: 200px; flex-shrink: 0;">
57
-                    <div class="card-header">
58
-                        <i :class="item.icon"></i>
59
-                        <h3>{{ item.title }}</h3>
60
-                        <span class="card-badge">{{ item.badge }}</span>
61
-                    </div>
62
-                    <div class="value-large" :style="{ color: item.color }">{{ item.value }}</div>
63
-                </div>
41
+                <el-button type="primary" @click="handleSearch">搜索</el-button>
42
+                <el-button @click="handleReset">重置</el-button>
64 43
             </div>
65 44
         </div>
66 45
 
67 46
         <div class="employee-section">
68 47
             <div class="section-title">
69
-                <i class="fas fa-users" style="color:#dc2626;"></i> 员工综合评估预警看板
48
+                <i class="fas fa-users" style="color:#dc2626;"></i> 员工综合预警
70 49
                 <span
71 50
                     style="font-size:0.7rem; background:#eef2ff; padding:4px 12px; border-radius:30px; margin-left:12px;">
72 51
                     评分依据:员工配分表
@@ -74,7 +53,7 @@
74 53
             </div>
75 54
             <div class="employee-card">
76 55
                 <div style="overflow-x: auto;">
77
-                    <el-table :data="filteredEmployees" :row-class-name="getRowClass" style="width:100%;" border stripe>
56
+                    <el-table :data="filteredEmployees" :row-class-name="getRowClass" style="width:100%;" border stripe @sort-change="handleSortChange">
78 57
                         <el-table-column prop="userId" label="员工ID" />
79 58
                         <el-table-column prop="nickName" label="姓名">
80 59
                             <template #default="{ row }">
@@ -82,14 +61,11 @@
82 61
                             </template>
83 62
                         </el-table-column>
84 63
                         <el-table-column prop="deptName" label="所属部门" />
85
-                        <el-table-column prop="eventTime" label="事件时间" sortable width="180" />
86
-                        <el-table-column prop="overallScore" label="综合评估得分" sortable>
87
-                            <template #default="{ row }">
88
-                                <span v-if="row.overallScore < 75" class="score-danger">{{ row.overallScore }} 分</span>
89
-                                <span v-else-if="row.overallScore >= 90" class="score-excellent">{{ row.overallScore }} 分</span>
90
-                                <span v-else style="font-weight:600;">{{ row.overallScore }} 分</span>
91
-                            </template>
92
-                        </el-table-column>
64
+                        <el-table-column prop="eventTime" label="事件时间" sortable="custom" width="180" />
65
+                        <el-table-column prop="dimName" label="维度名称" />
66
+                        <el-table-column prop="secondIndicator" label="二级指标名称" />
67
+                        <el-table-column prop="deductionTotal" label="扣分分值合计" sortable="custom" />
68
+                        <el-table-column prop="occurrenceCount" label="发生次数" sortable="custom" />
93 69
                         <el-table-column prop="overallScore" label="预警等级">
94 70
                             <template #default="{ row }">
95 71
                                 <span v-if="row.overallScore < 75" class="status-badge"
@@ -101,29 +77,26 @@
101 77
                                 <span v-else class="status-warning">正常范围</span>
102 78
                             </template>
103 79
                         </el-table-column>
104
-                        <el-table-column prop="coreRisksOrOutstandingAchievements" label="核心风险/优秀事迹" />
80
+                        <el-table-column prop="coreRisk" label="核心风险" />
105 81
                         <el-table-column prop="statusLabel" label="状态标签">
106 82
                             <template #default="{ row }">
107
-                                <span v-if="row.overallScore < 75" style="color:#b91c1c;"><i
108
-                                        class="fas fa-bell"></i>
83
+                                <span v-if="row.overallScore < 75" style="color:#b91c1c;"><i class="fas fa-bell"></i>
109 84
                                     {{ getAlertLabel(row.statusLabel) }}</span>
110 85
                                 <span v-else-if="row.overallScore >= 90" style="color:#15803d;"><i
111 86
                                         class="fas fa-crown"></i> {{ getAlertLabel(row.statusLabel) }}</span>
112 87
                                 <span v-else>{{ getAlertLabel(row.statusLabel) }}</span>
113 88
                             </template>
114 89
                         </el-table-column>
90
+                        <el-table-column label="详情" width="80">
91
+                            <template #default="{ row }">
92
+                                <span class="detail-link" @click="goToDetail(row)">详情</span>
93
+                            </template>
94
+                        </el-table-column>
115 95
                     </el-table>
116
-                    <el-pagination
117
-                        v-show="total > 0"
118
-                        :total="total"
119
-                        v-model:current-page="queryParams.pageNum"
120
-                        v-model:page-size="queryParams.pageSize"
121
-                        :page-sizes="[10, 20, 50, 100]"
122
-                        layout="total, sizes, prev, pager, next, jumper"
123
-                        @size-change="handleSizeChange"
124
-                        @current-change="handlePageChange"
125
-                        style="margin-top: 16px; justify-content: flex-end;"
126
-                    />
96
+                    <el-pagination v-show="total > 0" :total="total" v-model:current-page="queryParams.pageNum"
97
+                        v-model:page-size="queryParams.pageSize" :page-sizes="[10, 20, 50, 100]"
98
+                        layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
99
+                        @current-change="handlePageChange" style="margin-top: 16px; justify-content: flex-end;" />
127 100
                 </div>
128 101
                 <div class="warning-summary">
129 102
                     <div><i class="fas fa-circle" style="color:#ef4444;"></i> <strong>红色预警</strong> (综合评估 &lt; 75分) 共计
@@ -142,13 +115,15 @@
142 115
 </template>
143 116
 
144 117
 <script setup>
145
-import { ref, computed, onMounted, watch } from 'vue'
118
+import { ref, computed, reactive, onMounted, watch } from 'vue'
119
+import { useRouter } from 'vue-router'
146 120
 import { getDeptUserTree } from '@/api/item/items'
147
-import { getWarningPageData, getEmployeeWarningPageData } from '@/api/warningPage/warningPage'
121
+import { getEmployeeWarningPageData } from '@/api/warningManage/warningPage'
148 122
 import { useDict } from '@/utils/dict'
149 123
 import { useRoute } from 'vue-router'
150 124
 
151 125
 const route = useRoute()
126
+const router = useRouter()
152 127
 
153 128
 const { alert_level } = useDict('alert_level')
154 129
 
@@ -161,18 +136,6 @@ const selectedAlertLevel = ref('')
161 136
 const selectedOrg = ref('')
162 137
 const cascadeOptions = ref([])
163 138
 
164
-const summaryCards = ref([
165
-    { icon: 'fas fa-clipboard-list', title: '部门监察问题', badge: '部门级', value: '13项', color: '#b45309' },
166
-    { icon: 'fas fa-microchip', title: '实时质控拦截', badge: '部门级', value: '347次', color: '#2563eb' },
167
-    { icon: 'fas fa-bug', title: '不安全事件', badge: '一级预警', value: '18起', color: '#dc2626' },
168
-    { icon: 'fas fa-shield-virus', title: '安保测试记录', badge: '部门级', value: '4项', color: '#e67e22' },
169
-    { icon: 'fas fa-comment-dots', title: '旅客服务投诉', badge: '服务响应', value: '11件', color: '#e67e22' },
170
-    { icon: 'fas fa-clipboard-check', title: '服务巡查', badge: '部门级', value: '5项', color: '#333' },
171
-    { icon: 'fas fa-graduation-cap', title: '培训及考试成绩', badge: '平均分数', value: '92.4分', color: '#333' },
172
-    { icon: 'fas fa-plane-departure', title: '航站楼', badge: '吞吐量', value: '2.8万', color: '#059669' },
173
-    { icon: 'fas fa-gift', title: '小额奖励', badge: '奖励次数', value: '156次', color: '#7c3aed' }
174
-])
175
-
176 139
 const newNames = [
177 140
     "刘孟", "王苡衡", "周海涵", "何欣怡", "王崎",
178 141
     "黄政", "刘韵儿", "汪安霖", "张诗敏", "张维", "陈凯琳"
@@ -193,19 +156,30 @@ const coreRisksOrOutstandingAchievementsList = [
193 156
     "临近预警线,服务巡查扣分1次"
194 157
 ]
195 158
 
159
+const dimNames = ["安全管理", "服务质量", "运行效率", "队伍建设"]
160
+const indicatorNames = ["安全违规次数", "旅客投诉率", "安检差错率", "培训合格率", "操作规范度"]
161
+const deductionScores = [2, 5, 3, 1, 4]
162
+const occurrenceCounts = [1, 2, 3, 1, 2]
163
+
196 164
 const employeesData = ref([])
197 165
 const queryParams = reactive({
198
-  pageNum: 1,
199
-  pageSize: 10
166
+    pageNum: 1,
167
+    pageSize: 10
200 168
 })
201 169
 
202 170
 for (let i = 0; i < newNames.length; i++) {
203 171
     employeesData.value.push({
204
-        id: String(10021 + i),
205
-        name: newNames[i],
206
-        dept: deptList[i % deptList.length],
172
+        userId: String(10021 + i),
173
+        nickName: newNames[i],
174
+        deptName: deptList[i % deptList.length],
175
+        eventTime: `2026-0${(i % 9) + 1}-${String((i * 3 + 10) % 28 + 1).padStart(2, '0')}`,
207 176
         overallScore: scores[i % scores.length],
208
-        coreRisksOrOutstandingAchievements: coreRisksOrOutstandingAchievementsList[i % coreRisksOrOutstandingAchievementsList.length]
177
+        dimName: dimNames[i % dimNames.length],
178
+        secondIndicator: indicatorNames[i % indicatorNames.length],
179
+        deductionTotal: deductionScores[i % deductionScores.length],
180
+        occurrenceCount: occurrenceCounts[i % occurrenceCounts.length],
181
+        coreRisk: coreRisksOrOutstandingAchievementsList[i % coreRisksOrOutstandingAchievementsList.length],
182
+        statusLabel: scores[i % scores.length] < 75 ? '1' : scores[i % scores.length] >= 90 ? '2' : '3'
209 183
     })
210 184
 }
211 185
 
@@ -213,25 +187,23 @@ for (let i = 0; i < newNames.length; i++) {
213 187
 const transformCascadeData = (nodes) => {
214 188
     if (!nodes) return []
215 189
     return nodes.map(node => {
216
-        const label = node.nickName || node.deptName || node.name || node.label
217 190
         const deptType = node.deptType || node.nodeType
218
-    //    console.log(node,"node")
219
-    //     let value
220
-    //     if (deptType === 'BRIGADE') {
221
-    //         value = String(node.deptId)
222
-    //     } else if (deptType === 'MANAGER') {
223
-    //         value = String(node.teamId)
224
-    //     } else if (deptType === 'TEAMS') {
225
-    //         value = String(node.groupId)
226
-    //     } else {
227
-    //         value = String(node.userId ?? node.id ?? node.deptId ?? '')
228
-    //     }
191
+        const label = deptType === 'user'
192
+            ? (node.nickName || node.label)
193
+            : (node.deptName || node.name || node.label)
194
+
195
+        let value;
196
+        if (deptType === 'STATION') value = `station_${node.id}`;
197
+        else if (deptType === 'BRIGADE') value = `dept_${node.id}`
198
+        else if (deptType === 'MANAGER') value = `team_${node.id}`
199
+        else if (deptType === 'TEAMS') value = `group_${node.id}`
200
+        else value = `user_${node.id}`
229 201
 
230 202
         const children = node.children && node.children.length > 0 ? transformCascadeData(node.children) : undefined
231 203
 
232 204
         return {
233 205
             label,
234
-            value:node.id,
206
+            value,
235 207
             deptType,
236 208
             children
237 209
         }
@@ -281,12 +253,31 @@ const allFilteredEmployees = computed(() => {
281 253
     return result
282 254
 })
283 255
 
256
+const sortState = ref({ prop: '', order: '' })
257
+
284 258
 const filteredEmployees = computed(() => {
259
+    let data = [...allFilteredEmployees.value]
260
+    if (sortState.value.prop && sortState.value.order) {
261
+        const { prop, order } = sortState.value
262
+        data.sort((a, b) => {
263
+            const valA = a[prop]
264
+            const valB = b[prop]
265
+            if (valA == null) return 1
266
+            if (valB == null) return -1
267
+            const result = valA > valB ? 1 : valA < valB ? -1 : 0
268
+            return order === 'ascending' ? result : -result
269
+        })
270
+    }
285 271
     const start = (queryParams.pageNum - 1) * queryParams.pageSize
286 272
     const end = start + queryParams.pageSize
287
-    return allFilteredEmployees.value.slice(start, end)
273
+    return data.slice(start, end)
288 274
 })
289 275
 
276
+function handleSortChange({ prop, order }) {
277
+    sortState.value = { prop, order }
278
+    queryParams.pageNum = 1
279
+}
280
+
290 281
 const total = computed(() => allFilteredEmployees.value.length)
291 282
 
292 283
 function handlePageChange(newPage) {
@@ -316,6 +307,23 @@ const getRowClass = ({ row }) => {
316 307
     return ""
317 308
 }
318 309
 
310
+const goToDetail = (row) => {
311
+    const query = { }
312
+    if (startDate.value && endDate.value) {
313
+        query.startDate = formatDate(startDate.value)
314
+        query.endDate = formatDate(endDate.value)
315
+    } else {
316
+        query.activeRange = activeRange.value
317
+    }
318
+    if (selectedAlertLevel.value) {
319
+        query.alertLevel = selectedAlertLevel.value
320
+    }
321
+    if (selectedOrg.value) {
322
+        query.org = selectedOrg.value
323
+    }
324
+    router.push({ path: '/warningManage/redLineWarning', query })
325
+}
326
+
319 327
 const getAlertLabel = (value) => {
320 328
     if (!value) return ''
321 329
     const item = alert_level.value.find(d => d.value === value)
@@ -324,13 +332,26 @@ const getAlertLabel = (value) => {
324 332
 
325 333
 const setActiveRange = (range) => {
326 334
     activeRange.value = range
327
-    // 清除自定义日期
328 335
     if (range !== 'custom') {
329 336
         startDate.value = null
330 337
         endDate.value = null
331 338
     }
332 339
 }
333 340
 
341
+const handleSearch = () => {
342
+    fetchWarningData()
343
+}
344
+
345
+const handleReset = () => {
346
+    startDate.value = null
347
+    endDate.value = null
348
+    activeRange.value = 'month'
349
+    selectedAlertLevel.value = ''
350
+    selectedOrg.value = ''
351
+    queryParams.pageNum = 1
352
+    fetchWarningData()
353
+}
354
+
334 355
 
335 356
 const formatDate = (d) => {
336 357
     if (!d) return ''
@@ -356,18 +377,6 @@ const getDateRangeFromActive = () => {
356 377
     return { startDate: formatDate(start), endDate: formatDate(now) }
357 378
 }
358 379
 
359
-const warningDataMap = {
360
-    ledgerSupervisionProblem: 0,
361
-    ledgerRealtimeInterception: 1,
362
-    ledgerUnsafeEvent: 2,
363
-    ledgerSecurityTest: 3,
364
-    ledgerComplaint: 4,
365
-    ledgerServicePatrol: 5,
366
-    ledgerExamScore: 6,
367
-    ledgerTerminalBonus: 7,
368
-    ledgerRewardApproval: 8
369
-}
370
-
371 380
 const fetchWarningData = async () => {
372 381
     queryParams.pageNum = 1
373 382
     let params = {}
@@ -382,28 +391,16 @@ const fetchWarningData = async () => {
382 391
 
383 392
     const selectedInfo = getSelectedInfo(selectedOrg.value)
384 393
     if (selectedInfo) {
385
-        if (selectedInfo.deptType === 'BRIGADE') params.deptId = selectedInfo.value
386
-        else if (selectedInfo.deptType === 'MANAGER') params.teamId = selectedInfo.value
387
-        else if (selectedInfo.deptType === 'TEAMS') params.groupId = selectedInfo.value
388
-        else if (selectedInfo.deptType === 'user') params.userId = selectedInfo.value
394
+        const rawId = Number(selectedInfo.value.split('_')[1])
395
+
396
+        if (selectedInfo.deptType === 'BRIGADE') params.deptId = rawId
397
+        else if (selectedInfo.deptType === 'MANAGER') params.teamId = rawId
398
+        else if (selectedInfo.deptType === 'TEAMS') params.groupId = rawId
399
+        else if (selectedInfo.deptType === 'user') params.userId = rawId
389 400
     }
390 401
 
391 402
     try {
392
-        const [r1, r2] = await Promise.all([
393
-            getWarningPageData(params),
394
-            getEmployeeWarningPageData(params)
395
-        ])
396
-        if (r1.data) {
397
-            const d = r1.data
398
-            summaryCards.value.forEach((card, idx) => {
399
-                for (const [key, i] of Object.entries(warningDataMap)) {
400
-                    if (i === idx) {
401
-                        card.value = d[key] !== undefined ? String(d[key]) : card.value
402
-                        break
403
-                    }
404
-                }
405
-            })
406
-        }
403
+        const r2 = await getEmployeeWarningPageData(params)
407 404
         if (r2.data) {
408 405
             employeesData.value = r2.data
409 406
         }
@@ -417,6 +414,7 @@ onMounted(async () => {
417 414
         const res = await getDeptUserTree()
418 415
         if (res.data) {
419 416
             cascadeOptions.value = transformCascadeData(res.data)
417
+            console.log(cascadeOptions.value, "cascadeOptions")
420 418
         }
421 419
     } catch (error) {
422 420
         console.error('获取组织架构数据失败:', error)
@@ -424,36 +422,16 @@ onMounted(async () => {
424 422
     fetchWarningData()
425 423
 })
426 424
 
427
-watch(startDate, () => {
428
-    fetchWarningData()
429
-})
430
-watch(endDate, () => {
431
-    fetchWarningData()
432
-})
433
-watch(activeRange, () => {
434
-    fetchWarningData()
435
-})
436
-
437
-watch(cascadeOptions, (val) => {
438
-    if (val.length && selectedOrg.value) {
439
-        fetchWarningData()
440
-    }
441
-})
442
-
443
-watch(selectedAlertLevel, () => {
444
-    queryParams.pageNum = 1
445
-})
446
-
447 425
 // 监听路由参数变化,回显到级联选择器
448 426
 watch(() => route.query, (query) => {
449 427
     const { id } = query
450 428
     if (id) {
451
-        selectedOrg.value =Number(id)
429
+        selectedOrg.value = `user_${id}`
452 430
     } else {
453 431
         selectedOrg.value = ''
454 432
     }
455 433
     fetchWarningData()
456
-}, { immediate: true })
434
+})
457 435
 </script>
458 436
 
459 437
 <style scoped>
@@ -627,78 +605,6 @@ watch(() => route.query, (query) => {
627 605
     background: #1d4ed8;
628 606
 }
629 607
 
630
-.cards-container {
631
-    width: 100%;
632
-    margin-bottom: 36px;
633
-    overflow-x: auto;
634
-}
635
-
636
-.cards-grid {
637
-    display: flex;
638
-
639
-    gap: 16px;
640
-    flex-wrap: nowrap;
641
-    min-width: max-content;
642
-}
643
-
644
-.card {
645
-    background: #ffffff;
646
-    border-radius: 20px;
647
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03), 0 1px 2px rgba(0, 0, 0, 0.05);
648
-    transition: all 0.2s;
649
-    border: 1px solid #eef2f6;
650
-    padding: 1rem 0.5rem;
651
-    display: flex;
652
-    flex-direction: column;
653
-    text-align: center;
654
-    min-width: 280px;
655
-    flex-shrink: 0;
656
-}
657
-
658
-.card:hover {
659
-    transform: translateY(-3px);
660
-    box-shadow: 0 12px 20px -10px rgba(0, 0, 0, 0.1);
661
-    border-color: #cbd5e1;
662
-}
663
-
664
-.card-header {
665
-    display: flex;
666
-    flex-direction: column;
667
-    align-items: center;
668
-    gap: 6px;
669
-    border-bottom: none;
670
-    padding-bottom: 0;
671
-    margin-bottom: 12px;
672
-}
673
-
674
-.card-header i {
675
-    font-size: 1.5rem;
676
-    color: #2563eb;
677
-}
678
-
679
-.card-header h3 {
680
-    font-size: 0.85rem;
681
-    font-weight: 700;
682
-    margin: 0;
683
-    white-space: nowrap;
684
-}
685
-
686
-.card-badge {
687
-    font-size: 0.6rem;
688
-    background: #f1f5f9;
689
-    padding: 2px 8px;
690
-    border-radius: 30px;
691
-    margin-top: 4px;
692
-    display: inline-block;
693
-}
694
-
695
-.value-large {
696
-    font-size: 1.8rem;
697
-    font-weight: 800;
698
-    line-height: 1.2;
699
-    margin: 8px 0 4px 0;
700
-}
701
-
702 608
 .employee-section {
703 609
     margin-top: 20px;
704 610
     width: 100%;
@@ -786,6 +692,16 @@ watch(() => route.query, (query) => {
786 692
     font-size: 0.85rem;
787 693
 }
788 694
 
695
+.detail-link {
696
+    color: #2563eb;
697
+    cursor: pointer;
698
+    font-weight: 500;
699
+}
700
+
701
+.detail-link:hover {
702
+    text-decoration: underline;
703
+}
704
+
789 705
 footer {
790 706
     text-align: center;
791 707
     margin-top: 32px;