Selaa lähdekoodia

feat: 新增预警页面功能,优化员工画像页面跳转与搜索

1.  新增预警页面路由与完整预警中枢页面
2.  调整员工画像页面能力图表点击跳转预警页
3.  为部门搜索组件新增id查询参数
4.  移除群组画像页面无用的默认post数据
5.  在用户信息编辑页面新增标签输入项
6.  重构员工画像页面的鼠标监听逻辑,修复绑定问题
huoyi 2 viikkoa sitten
vanhempi
commit
85377c225b

+ 4 - 4
src/router/index.js

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

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

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

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

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

+ 32 - 8
src/views/portraitManagement/employeeProfile/index.vue

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

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

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

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

@@ -90,6 +90,11 @@
90 90
               </el-select>
91 91
             </el-form-item>
92 92
           </el-col>
93
+          <el-col :span="12">
94
+            <el-form-item label="标签" prop="tags">
95
+              <el-input v-model="form.tags" placeholder="请输入标签" maxlength="50" />
96
+            </el-form-item>
97
+          </el-col>
93 98
         </el-row>
94 99
       </el-card>
95 100
       <el-card header="状态管理信息" style="margin-bottom: 10px;">

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

@@ -0,0 +1,715 @@
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
+            </div>
32
+            <div class="filter-group">
33
+                <el-select v-model="selectedAlertLevel" placeholder="预警等级" clearable style="width: 250px;">
34
+                    <el-option label="全部" value=""></el-option>
35
+                    <el-option v-for="item in alert_level" :key="item.value" :label="item.label" :value="item.value" />
36
+                </el-select>
37
+                <el-cascader v-model="selectedOrg" :options="cascadeOptions"
38
+                    :props="{ expandTrigger: 'hover', label: 'label', value: 'value', children: 'children', emitPath: false }"
39
+                    placeholder="组织架构/员工" clearable style="width: 280px;" />
40
+            </div>
41
+        </div>
42
+
43
+        <div class="cards-container">
44
+            <div class="cards-grid" style="overflow-x: auto; white-space: nowrap;">
45
+                <div class="card" v-for="(item, index) in summaryCards" :key="index"
46
+                    style="display: inline-block; width: 200px; flex-shrink: 0;">
47
+                    <div class="card-header">
48
+                        <i :class="item.icon"></i>
49
+                        <h3>{{ item.title }}</h3>
50
+                        <span class="card-badge">{{ item.badge }}</span>
51
+                    </div>
52
+                    <div class="value-large" :style="{ color: item.color }">{{ item.value }}</div>
53
+                </div>
54
+            </div>
55
+        </div>
56
+
57
+        <div class="employee-section">
58
+            <div class="section-title">
59
+                <i class="fas fa-users" style="color:#dc2626;"></i> 员工综合评估预警看板
60
+                <span
61
+                    style="font-size:0.7rem; background:#eef2ff; padding:4px 12px; border-radius:30px; margin-left:12px;">
62
+                    评分依据:员工配分表
63
+                </span>
64
+            </div>
65
+            <div class="employee-card">
66
+                <div style="overflow-x: auto;">
67
+                    <table class="data-table" style="width:100%;">
68
+                        <thead>
69
+                            <tr>
70
+                                <th>员工ID</th>
71
+                                <th>姓名</th>
72
+                                <th>所属部门</th>
73
+                                <th>综合评估得分</th>
74
+                                <th>预警等级</th>
75
+                                <th>核心风险/优秀事迹</th>
76
+                                <th>状态标签</th>
77
+                            </tr>
78
+                        </thead>
79
+                        <tbody>
80
+                            <tr v-for="emp in filteredEmployees" :key="emp.id" :class="getRowClass(emp.baseScore)">
81
+                                <td>{{ emp.id }}</td>
82
+                                <td><strong>{{ emp.name }}</strong></td>
83
+                                <td>{{ emp.dept }}</td>
84
+                                <td>
85
+                                    <span v-if="emp.baseScore < 75" class="score-danger">{{ emp.baseScore }} 分</span>
86
+                                    <span v-else-if="emp.baseScore >= 90" class="score-excellent">{{ emp.baseScore }}
87
+                                        分</span>
88
+                                    <span v-else style="font-weight:600;">{{ emp.baseScore }} 分</span>
89
+                                </td>
90
+                                <td>
91
+                                    <span v-if="emp.baseScore < 75" class="status-badge"
92
+                                        style="animation: subtlePulse 1s infinite;"><i
93
+                                            class="fas fa-exclamation-triangle"></i> 红色预警</span>
94
+                                    <span v-else-if="emp.baseScore >= 90" class="status-excellent"
95
+                                        style="background:#d1fae5; color:#065f46;"><i class="fas fa-star"></i>
96
+                                        优秀标杆</span>
97
+                                    <span v-else class="status-warning">正常范围</span>
98
+                                </td>
99
+                                <td style="font-size:0.75rem;">{{ emp.riskDesc }}</td>
100
+                                <td>
101
+                                    <span v-if="emp.baseScore < 75" style="color:#b91c1c;"><i class="fas fa-bell"></i>
102
+                                        紧急干预</span>
103
+                                    <span v-else-if="emp.baseScore >= 90" style="color:#15803d;"><i
104
+                                            class="fas fa-crown"></i> 表彰激励</span>
105
+                                    <span v-else>常规辅导</span>
106
+                                </td>
107
+                            </tr>
108
+                        </tbody>
109
+                    </table>
110
+                </div>
111
+                <div class="warning-summary">
112
+                    <div><i class="fas fa-circle" style="color:#ef4444;"></i> <strong>红色预警</strong> (综合评估 &lt; 75分) 共计
113
+                        {{ redAlertCount }} 人 → 立即约谈/培训</div>
114
+                    <div><i class="fas fa-circle" style="color:#22c55e;"></i> <strong>优秀标杆</strong> (综合评估 ≥ 90分) 共计 {{
115
+                        excellentCount }} 人 → 表彰激励</div>
116
+                    <div><i class="fas fa-chart-simple"></i> 全员平均综合得分: <strong>{{ avgScore }}</strong> 分</div>
117
+                </div>
118
+            </div>
119
+        </div>
120
+
121
+        <footer>
122
+            <i class="far fa-bell"></i> 员工部门:旅检一/二/三部 | 阈值:<75红色预警 | ≥90优秀
123
+        </footer>
124
+    </div>
125
+</template>
126
+
127
+<script setup>
128
+import { ref, computed, onMounted, watch } from 'vue'
129
+import { getDeptUserTree } from '@/api/item/items'
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
+const currentFilterText = ref('')
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 riskDescList = [
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 employeesData = ref([])
179
+
180
+for (let i = 0; i < newNames.length; i++) {
181
+    employeesData.value.push({
182
+        id: String(10021 + i),
183
+        name: newNames[i],
184
+        dept: deptList[i % deptList.length],
185
+        baseScore: scores[i % scores.length],
186
+        riskDesc: riskDescList[i % riskDescList.length]
187
+    })
188
+}
189
+
190
+// 将组织架构数据转换为级联选择器格式
191
+const transformCascadeData = (nodes) => {
192
+    if (!nodes) return []
193
+    return nodes.map(node => {
194
+        const label = node.nickName || node.deptName || node.name || node.label
195
+        const value = node.id || node.deptId
196
+
197
+        let children = []
198
+        if (node.children && node.children.length > 0) {
199
+            children = transformCascadeData(node.children)
200
+        }
201
+        // 如果是用户节点,没有子节点
202
+        if (node.nodeType === 'user' || node.deptType === 'user') {
203
+            children = []
204
+        }
205
+
206
+        return {
207
+            label,
208
+            value,
209
+            nodeType: node.nodeType || node.deptType,
210
+            children: children.length > 0 ? children : undefined
211
+        }
212
+    })
213
+}
214
+
215
+// 获取选中的部门或用户信息
216
+const getSelectedInfo = (selectedValue) => {
217
+    if (!selectedValue) return null
218
+
219
+    const findNodeByValue = (nodes, value) => {
220
+        for (const node of nodes) {
221
+            if (node.value === value) {
222
+                return node
223
+            }
224
+            if (node.children) {
225
+                const found = findNodeByValue(node.children, value)
226
+                if (found) return found
227
+            }
228
+        }
229
+        return null
230
+    }
231
+
232
+    return findNodeByValue(cascadeOptions.value, selectedValue)
233
+}
234
+
235
+const filteredEmployees = computed(() => {
236
+    let result = [...employeesData.value]
237
+
238
+    // 模糊搜索筛选
239
+    if (currentFilterText.value.trim()) {
240
+        const keyword = currentFilterText.value.trim().toLowerCase()
241
+        result = result.filter(emp =>
242
+            emp.name.toLowerCase().includes(keyword) ||
243
+            emp.dept.toLowerCase().includes(keyword) ||
244
+            emp.id.toLowerCase().includes(keyword)
245
+        )
246
+    }
247
+
248
+    // 预警等级筛选
249
+    if (selectedAlertLevel.value) {
250
+        // 根据字典值判断筛选条件(红色预警 <75,优秀标杆 >=90,正常范围 75-89)
251
+        // 可以根据字典的 value 或 label 来判断
252
+        const alertItem = alert_level.value.find(item => item.value === selectedAlertLevel.value)
253
+        if (alertItem) {
254
+            const label = alertItem.label
255
+            if (label.includes('红色') || label.includes('预警')) {
256
+                result = result.filter(emp => emp.baseScore < 75)
257
+            } else if (label.includes('优秀') || label.includes('标杆')) {
258
+                result = result.filter(emp => emp.baseScore >= 90)
259
+            } else if (label.includes('正常')) {
260
+                result = result.filter(emp => emp.baseScore >= 75 && emp.baseScore < 90)
261
+            }
262
+        }
263
+    }
264
+
265
+    // 组织架构筛选
266
+    if (selectedOrg.value) {
267
+        const selectedInfo = getSelectedInfo(selectedOrg.value)
268
+        if (selectedInfo) {
269
+            if (selectedInfo.nodeType === 'user') {
270
+                // 筛选特定用户
271
+                result = result.filter(emp => emp.name === selectedInfo.label)
272
+            } else {
273
+                // 筛选特定部门
274
+                result = result.filter(emp => emp.dept.includes(selectedInfo.label))
275
+            }
276
+        }
277
+    }
278
+
279
+    return result
280
+})
281
+
282
+const redAlertCount = computed(() => {
283
+    return filteredEmployees.value.filter(emp => emp.baseScore < 75).length
284
+})
285
+
286
+const excellentCount = computed(() => {
287
+    return filteredEmployees.value.filter(emp => emp.baseScore >= 90).length
288
+})
289
+
290
+const avgScore = computed(() => {
291
+    if (!filteredEmployees.value.length) return "0"
292
+    const total = filteredEmployees.value.reduce((sum, emp) => sum + emp.baseScore, 0)
293
+    return (total / filteredEmployees.value.length).toFixed(1)
294
+})
295
+
296
+const getRowClass = (score) => {
297
+    if (score < 75) return "employee-warning-row"
298
+    if (score >= 90) return "employee-excellent-row"
299
+    return ""
300
+}
301
+
302
+const setActiveRange = (range) => {
303
+    activeRange.value = range
304
+    // 清除自定义日期
305
+    if (range !== 'custom') {
306
+        startDate.value = null
307
+        endDate.value = null
308
+    }
309
+}
310
+
311
+const handleSearch = () => {
312
+    console.log('执行筛选', {
313
+        activeRange: activeRange.value,
314
+        startDate: startDate.value,
315
+        endDate: endDate.value,
316
+        selectedAlertLevel: selectedAlertLevel.value,
317
+        selectedOrg: selectedOrg.value
318
+    })
319
+}
320
+
321
+onMounted(async () => {
322
+    try {
323
+        // 获取组织架构数据
324
+        const res = await getDeptUserTree()
325
+        if (res.data) {
326
+            cascadeOptions.value = transformCascadeData(res.data)
327
+        }
328
+    } catch (error) {
329
+        console.error('获取组织架构数据失败:', error)
330
+    }
331
+})
332
+
333
+// 监听路由参数变化,回显到级联选择器
334
+watch(() => route.query, (query) => {
335
+    const { id } = query
336
+    if (id) {
337
+        selectedOrg.value = id
338
+    } else {
339
+        selectedOrg.value = ''
340
+    }
341
+}, { immediate: true })
342
+</script>
343
+
344
+<style scoped>
345
+@keyframes subtlePulse {
346
+    0% {
347
+        opacity: 0.7;
348
+    }
349
+
350
+    50% {
351
+        opacity: 1;
352
+        background: #ffb3b3;
353
+    }
354
+
355
+    100% {
356
+        opacity: 0.7;
357
+    }
358
+}
359
+
360
+
361
+
362
+.dashboard {
363
+    max-width: 100%;
364
+    padding: 10px;
365
+}
366
+
367
+.header {
368
+    margin-bottom: 28px;
369
+    display: flex;
370
+    justify-content: space-between;
371
+    align-items: flex-end;
372
+    flex-wrap: wrap;
373
+    gap: 16px;
374
+}
375
+
376
+.title-section h1 {
377
+    font-size: 1.7rem;
378
+    font-weight: 700;
379
+    background: linear-gradient(135deg, #1e3c72, #2a5298);
380
+    -webkit-background-clip: text;
381
+    background-clip: text;
382
+    color: transparent;
383
+    letter-spacing: -0.3px;
384
+}
385
+
386
+.title-section p {
387
+    color: #475569;
388
+    margin-top: 6px;
389
+    font-size: 0.85rem;
390
+}
391
+
392
+.badge-group {
393
+    display: flex;
394
+    gap: 12px;
395
+}
396
+
397
+.alert-badge {
398
+    background: #fff1f0;
399
+    border-left: 5px solid #ef4444;
400
+    padding: 6px 16px;
401
+    border-radius: 40px;
402
+    font-weight: 600;
403
+    font-size: 0.85rem;
404
+}
405
+
406
+.alert-badge i {
407
+    color: #ef4444;
408
+    margin-right: 6px;
409
+}
410
+
411
+.filter-bar {
412
+    background: white;
413
+    border-radius: 60px;
414
+    padding: 8px 20px;
415
+    margin-bottom: 28px;
416
+    display: flex;
417
+    flex-wrap: wrap;
418
+    align-items: center;
419
+    justify-content: space-between;
420
+    gap: 16px;
421
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02), 0 1px 2px rgba(0, 0, 0, 0.03);
422
+    border: 1px solid #eef2f6;
423
+}
424
+
425
+.time-range {
426
+    display: flex;
427
+    gap: 8px;
428
+    align-items: center;
429
+    flex-wrap: wrap;
430
+}
431
+
432
+.time-btn {
433
+    background: #f8fafc;
434
+    border: 1px solid #e2e8f0;
435
+    padding: 6px 18px;
436
+    border-radius: 40px;
437
+    font-size: 0.8rem;
438
+    font-weight: 500;
439
+    cursor: pointer;
440
+    transition: all 0.2s;
441
+    color: #1e293b;
442
+}
443
+
444
+.time-btn.active {
445
+    background: #2563eb;
446
+    border-color: #2563eb;
447
+    color: white;
448
+}
449
+
450
+.time-btn:hover {
451
+    background: #e2e8f0;
452
+}
453
+
454
+.custom-date {
455
+    display: flex;
456
+    align-items: center;
457
+    gap: 8px;
458
+}
459
+
460
+.clear-btn {
461
+    background: transparent;
462
+    border: 1px solid #e2e8f0;
463
+    padding: 4px 8px;
464
+    border-radius: 40px;
465
+    cursor: pointer;
466
+    color: #64748b;
467
+}
468
+
469
+.clear-btn:hover {
470
+    background: #f1f5f9;
471
+}
472
+
473
+.custom-date :deep(.el-date-picker) {
474
+    background: transparent;
475
+}
476
+
477
+.custom-date :deep(.el-input__wrapper) {
478
+    background: transparent;
479
+
480
+
481
+}
482
+
483
+.filter-group {
484
+    display: flex;
485
+    gap: 12px;
486
+    align-items: center;
487
+}
488
+
489
+.search-wrapper {
490
+    display: flex;
491
+    align-items: center;
492
+    gap: 8px;
493
+    background: #f8fafc;
494
+    padding: 4px 12px;
495
+    border-radius: 40px;
496
+    border: 1px solid #e2e8f0;
497
+}
498
+
499
+.search-btn {
500
+    background: #2563eb;
501
+    border: none;
502
+    color: white;
503
+    padding: 4px 14px;
504
+    border-radius: 30px;
505
+    font-size: 0.75rem;
506
+    font-weight: 500;
507
+    cursor: pointer;
508
+    transition: 0.2s;
509
+}
510
+
511
+.search-btn:hover {
512
+    background: #1d4ed8;
513
+}
514
+
515
+.cards-container {
516
+    width: 100%;
517
+    margin-bottom: 36px;
518
+    overflow-x: auto;
519
+}
520
+
521
+.cards-grid {
522
+    display: flex;
523
+
524
+    gap: 16px;
525
+    flex-wrap: nowrap;
526
+    min-width: max-content;
527
+}
528
+
529
+.card {
530
+    background: #ffffff;
531
+    border-radius: 20px;
532
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03), 0 1px 2px rgba(0, 0, 0, 0.05);
533
+    transition: all 0.2s;
534
+    border: 1px solid #eef2f6;
535
+    padding: 1rem 0.5rem;
536
+    display: flex;
537
+    flex-direction: column;
538
+    text-align: center;
539
+    min-width: 280px;
540
+    flex-shrink: 0;
541
+}
542
+
543
+.card:hover {
544
+    transform: translateY(-3px);
545
+    box-shadow: 0 12px 20px -10px rgba(0, 0, 0, 0.1);
546
+    border-color: #cbd5e1;
547
+}
548
+
549
+.card-header {
550
+    display: flex;
551
+    flex-direction: column;
552
+    align-items: center;
553
+    gap: 6px;
554
+    border-bottom: none;
555
+    padding-bottom: 0;
556
+    margin-bottom: 12px;
557
+}
558
+
559
+.card-header i {
560
+    font-size: 1.5rem;
561
+    color: #2563eb;
562
+}
563
+
564
+.card-header h3 {
565
+    font-size: 0.85rem;
566
+    font-weight: 700;
567
+    margin: 0;
568
+    white-space: nowrap;
569
+}
570
+
571
+.card-badge {
572
+    font-size: 0.6rem;
573
+    background: #f1f5f9;
574
+    padding: 2px 8px;
575
+    border-radius: 30px;
576
+    margin-top: 4px;
577
+    display: inline-block;
578
+}
579
+
580
+.value-large {
581
+    font-size: 1.8rem;
582
+    font-weight: 800;
583
+    line-height: 1.2;
584
+    margin: 8px 0 4px 0;
585
+}
586
+
587
+.employee-section {
588
+    margin-top: 20px;
589
+    width: 100%;
590
+}
591
+
592
+.section-title {
593
+    font-size: 1.2rem;
594
+    font-weight: 700;
595
+    margin-bottom: 1rem;
596
+    display: flex;
597
+    align-items: center;
598
+    gap: 8px;
599
+    border-left: 5px solid #ef4444;
600
+    padding-left: 14px;
601
+}
602
+
603
+.employee-card {
604
+    background: #ffffff;
605
+    border-radius: 24px;
606
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03), 0 1px 2px rgba(0, 0, 0, 0.05);
607
+    border: 1px solid #eef2f6;
608
+    padding: 1rem 1.5rem;
609
+    width: 100%;
610
+}
611
+
612
+.data-table {
613
+    width: 100%;
614
+    border-collapse: collapse;
615
+    font-size: 0.8rem;
616
+}
617
+
618
+.data-table th {
619
+    text-align: left;
620
+    padding: 10px 6px 8px 0;
621
+    font-weight: 600;
622
+    color: #334155;
623
+    border-bottom: 2px solid #e2e8f0;
624
+}
625
+
626
+.data-table td {
627
+    padding: 8px 6px 8px 0;
628
+    border-bottom: 1px solid #f1f5f9;
629
+    vertical-align: middle;
630
+}
631
+
632
+.employee-warning-row {
633
+    background-color: #fff5f5;
634
+    border-left: 4px solid #ef4444;
635
+}
636
+
637
+.employee-excellent-row {
638
+    background-color: #f0fdf4;
639
+    border-left: 4px solid #22c55e;
640
+}
641
+
642
+.score-danger {
643
+    font-weight: 800;
644
+    color: #dc2626;
645
+    background: #fee2e2;
646
+    padding: 2px 8px;
647
+    border-radius: 30px;
648
+    display: inline-block;
649
+    font-size: 0.8rem;
650
+}
651
+
652
+.score-excellent {
653
+    font-weight: 800;
654
+    color: #15803d;
655
+    background: #dcfce7;
656
+    padding: 2px 8px;
657
+    border-radius: 30px;
658
+    display: inline-block;
659
+    font-size: 0.8rem;
660
+}
661
+
662
+.warning-summary {
663
+    background: #fff9f0;
664
+    border-radius: 18px;
665
+    padding: 12px 20px;
666
+    margin-top: 18px;
667
+    display: flex;
668
+    gap: 28px;
669
+    flex-wrap: wrap;
670
+    font-weight: 500;
671
+    font-size: 0.85rem;
672
+}
673
+
674
+footer {
675
+    text-align: center;
676
+    margin-top: 32px;
677
+    font-size: 0.7rem;
678
+    color: #7e8b9c;
679
+    border-top: 1px solid #e2edf7;
680
+    padding-top: 18px;
681
+}
682
+
683
+@media (max-width: 1200px) {
684
+    .cards-grid {
685
+        grid-template-columns: repeat(4, 1fr);
686
+        gap: 14px;
687
+    }
688
+}
689
+
690
+@media (max-width: 800px) {
691
+    .cards-grid {
692
+        grid-template-columns: repeat(2, 1fr);
693
+    }
694
+
695
+    body {
696
+        padding: 20px;
697
+    }
698
+
699
+    .filter-bar {
700
+        border-radius: 24px;
701
+        flex-direction: column;
702
+        align-items: stretch;
703
+    }
704
+
705
+    .org-search {
706
+        justify-content: space-between;
707
+    }
708
+}
709
+
710
+@media (max-width: 550px) {
711
+    .cards-grid {
712
+        grid-template-columns: 1fr;
713
+    }
714
+}
715
+</style>