Pārlūkot izejas kodu

feat(portraitManagement): 新增器官画像模块及白卡组件

- 添加白卡组件whiteCard.vue,支持标题和背景色自定义
- 新增organProfile页面,展示多种监察和质控指标及图表
- 实现顶部指标卡片,支持环比数据显示及小折线图展示
- 集成丰富图表组件,包含折线图、柱状图、饼图等多种类型
- 实现右侧多维度排行榜显示,展示人员排名和相关数据
- 使用echarts完成图表初始化及数据绑定,提升数据可视化效果
- 完善scss样式,保证布局清晰、视觉统一、响应合理
huoyi 2 dienas atpakaļ
vecāks
revīzija
a31ceb834e

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

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

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

+ 41 - 39
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">
@@ -31,14 +31,15 @@
31 31
                 <el-tree-select v-model="selectedOrg" :data="cascadeOptions"
32 32
                     :props="{ label: 'label', value: 'value', children: 'children' }" node-key="value"
33 33
                     placeholder="组织架构/员工" clearable filterable check-strictly style="width: 480px;"
34
-                    @change="fetchWarningData" />
34
+                     />
35 35
             </div>
36 36
             <div class="filter-group">
37 37
                 <el-select v-model="selectedAlertLevel" placeholder="预警等级" clearable style="width: 250px;">
38 38
                     <el-option label="全部" value=""></el-option>
39 39
                     <el-option v-for="item in alert_level" :key="item.value" :label="item.label" :value="item.value" />
40 40
                 </el-select>
41
-
41
+                <el-button type="primary" @click="handleSearch">搜索</el-button>
42
+                <el-button @click="handleReset">重置</el-button>
42 43
             </div>
43 44
         </div>
44 45
 
@@ -58,7 +59,7 @@
58 59
 
59 60
         <div class="employee-section">
60 61
             <div class="section-title">
61
-                <i class="fas fa-users" style="color:#dc2626;"></i> 员工综合评估预警看板
62
+                <i class="fas fa-users" style="color:#dc2626;"></i> 员工综合预警
62 63
                 <span
63 64
                     style="font-size:0.7rem; background:#eef2ff; padding:4px 12px; border-radius:30px; margin-left:12px;">
64 65
                     评分依据:员工配分表
@@ -75,14 +76,10 @@
75 76
                         </el-table-column>
76 77
                         <el-table-column prop="deptName" label="所属部门" />
77 78
                         <el-table-column prop="eventTime" label="事件时间" sortable width="180" />
78
-                        <el-table-column prop="overallScore" label="综合评估得分" sortable>
79
-                            <template #default="{ row }">
80
-                                <span v-if="row.overallScore < 75" class="score-danger">{{ row.overallScore }} 分</span>
81
-                                <span v-else-if="row.overallScore >= 90" class="score-excellent">{{ row.overallScore }}
82
-                                    分</span>
83
-                                <span v-else style="font-weight:600;">{{ row.overallScore }} 分</span>
84
-                            </template>
85
-                        </el-table-column>
79
+                        <el-table-column prop="dimName" label="维度名称" />
80
+                        <el-table-column prop="secondIndicator" label="二级指标名称" />
81
+                        <el-table-column prop="deductionTotal" label="扣分分值合计" sortable />
82
+                        <el-table-column prop="occurrenceCount" label="发生次数" sortable />
86 83
                         <el-table-column prop="overallScore" label="预警等级">
87 84
                             <template #default="{ row }">
88 85
                                 <span v-if="row.overallScore < 75" class="status-badge"
@@ -94,7 +91,7 @@
94 91
                                 <span v-else class="status-warning">正常范围</span>
95 92
                             </template>
96 93
                         </el-table-column>
97
-                        <el-table-column prop="coreRisksOrOutstandingAchievements" label="核心风险/优秀事迹" />
94
+                        <el-table-column prop="coreRisk" label="核心风险" />
98 95
                         <el-table-column prop="statusLabel" label="状态标签">
99 96
                             <template #default="{ row }">
100 97
                                 <span v-if="row.overallScore < 75" style="color:#b91c1c;"><i class="fas fa-bell"></i>
@@ -178,6 +175,11 @@ const coreRisksOrOutstandingAchievementsList = [
178 175
     "临近预警线,服务巡查扣分1次"
179 176
 ]
180 177
 
178
+const dimNames = ["安全管理", "服务质量", "运行效率", "队伍建设"]
179
+const indicatorNames = ["安全违规次数", "旅客投诉率", "安检差错率", "培训合格率", "操作规范度"]
180
+const deductionScores = [2, 5, 3, 1, 4]
181
+const occurrenceCounts = [1, 2, 3, 1, 2]
182
+
181 183
 const employeesData = ref([])
182 184
 const queryParams = reactive({
183 185
     pageNum: 1,
@@ -186,11 +188,17 @@ const queryParams = reactive({
186 188
 
187 189
 for (let i = 0; i < newNames.length; i++) {
188 190
     employeesData.value.push({
189
-        id: String(10021 + i),
190
-        name: newNames[i],
191
-        dept: deptList[i % deptList.length],
191
+        userId: String(10021 + i),
192
+        nickName: newNames[i],
193
+        deptName: deptList[i % deptList.length],
194
+        eventTime: `2026-0${(i % 9) + 1}-${String((i * 3 + 10) % 28 + 1).padStart(2, '0')}`,
192 195
         overallScore: scores[i % scores.length],
193
-        coreRisksOrOutstandingAchievements: coreRisksOrOutstandingAchievementsList[i % coreRisksOrOutstandingAchievementsList.length]
196
+        dimName: dimNames[i % dimNames.length],
197
+        secondIndicator: indicatorNames[i % indicatorNames.length],
198
+        deductionTotal: deductionScores[i % deductionScores.length],
199
+        occurrenceCount: occurrenceCounts[i % occurrenceCounts.length],
200
+        coreRisk: coreRisksOrOutstandingAchievementsList[i % coreRisksOrOutstandingAchievementsList.length],
201
+        statusLabel: scores[i % scores.length] < 75 ? '1' : scores[i % scores.length] >= 90 ? '2' : '3'
194 202
     })
195 203
 }
196 204
 
@@ -314,6 +322,20 @@ const setActiveRange = (range) => {
314 322
     }
315 323
 }
316 324
 
325
+const handleSearch = () => {
326
+    fetchWarningData()
327
+}
328
+
329
+const handleReset = () => {
330
+    startDate.value = null
331
+    endDate.value = null
332
+    activeRange.value = 'month'
333
+    selectedAlertLevel.value = ''
334
+    selectedOrg.value = ''
335
+    queryParams.pageNum = 1
336
+    fetchWarningData()
337
+}
338
+
317 339
 
318 340
 const formatDate = (d) => {
319 341
     if (!d) return ''
@@ -410,26 +432,6 @@ onMounted(async () => {
410 432
     fetchWarningData()
411 433
 })
412 434
 
413
-watch(startDate, () => {
414
-    fetchWarningData()
415
-})
416
-watch(endDate, () => {
417
-    fetchWarningData()
418
-})
419
-watch(activeRange, () => {
420
-    fetchWarningData()
421
-})
422
-
423
-watch(cascadeOptions, (val) => {
424
-    if (val.length && selectedOrg.value) {
425
-        fetchWarningData()
426
-    }
427
-})
428
-
429
-watch(selectedAlertLevel, () => {
430
-    queryParams.pageNum = 1
431
-})
432
-
433 435
 // 监听路由参数变化,回显到级联选择器
434 436
 watch(() => route.query, (query) => {
435 437
     const { id } = query
@@ -439,7 +441,7 @@ watch(() => route.query, (query) => {
439 441
         selectedOrg.value = ''
440 442
     }
441 443
     fetchWarningData()
442
-}, { immediate: true })
444
+})
443 445
 </script>
444 446
 
445 447
 <style scoped>
@@ -513,7 +515,7 @@ watch(() => route.query, (query) => {
513 515
     background: white;
514 516
     border-radius: 60px;
515 517
     padding: 8px 20px;
516
-    margin-bottom: 28px;
518
+    margin-bottom: 10px;
517 519
     display: flex;
518 520
     flex-wrap: wrap;
519 521
     align-items: center;

+ 836 - 0
src/views/warningManage/warningPage/index.vue

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