Преглед изворни кода

Merge branch 'homepageLargeScreen' into dev

huoyi пре 1 месец
родитељ
комит
06765c1238
52 измењених фајлова са 9462 додато и 166 уклоњено
  1. 327 0
      src/api/item/items.js
  2. 88 0
      src/api/largeScreen/largeScreen.js
  3. BIN
      src/assets/icons/one.png
  4. BIN
      src/assets/icons/three.png
  5. BIN
      src/assets/icons/two.png
  6. 92 0
      src/hooks/useEcharts.js
  7. 10 7
      src/store/modules/user.js
  8. 118 0
      src/views/dataBigScreen/dashboard/InactivityTimer.js
  9. 1 1
      src/views/dataBigScreen/dashboard/SeizedRanking.vue
  10. 2 36
      src/views/dataBigScreen/dashboard/TopTitle.vue
  11. 146 0
      src/views/dataBigScreen/dashboard/components/ChartsContainer.vue
  12. 10 57
      src/views/dataBigScreen/dashboard/components/DashboardContainer.vue
  13. 113 0
      src/views/dataBigScreen/dashboard/components/pageItems/Attendance.vue
  14. 182 0
      src/views/dataBigScreen/dashboard/components/pageItems/ChannelOpeningSituation.vue
  15. 161 0
      src/views/dataBigScreen/dashboard/components/pageItems/ChannelsAndPersonnel.vue
  16. 61 0
      src/views/dataBigScreen/dashboard/components/pageItems/ClassTaskCompletionRank.vue
  17. 57 0
      src/views/dataBigScreen/dashboard/components/pageItems/ClassTaskDetails.vue
  18. 405 0
      src/views/dataBigScreen/dashboard/components/pageItems/Collaboration.vue
  19. 192 0
      src/views/dataBigScreen/dashboard/components/pageItems/CollaborationPersonal.vue
  20. 29 0
      src/views/dataBigScreen/dashboard/components/pageItems/CustomStyleSegmented.vue
  21. 105 0
      src/views/dataBigScreen/dashboard/components/pageItems/CustomStyleSelect.vue
  22. 31 0
      src/views/dataBigScreen/dashboard/components/pageItems/DataViewLeft.vue
  23. 67 0
      src/views/dataBigScreen/dashboard/components/pageItems/DepartmentInfo.vue
  24. 77 0
      src/views/dataBigScreen/dashboard/components/pageItems/DepartmentalTaskCompletionRank.vue
  25. 658 0
      src/views/dataBigScreen/dashboard/components/pageItems/HomePageOverview.vue
  26. 543 0
      src/views/dataBigScreen/dashboard/components/pageItems/HomePageStats.vue
  27. 347 0
      src/views/dataBigScreen/dashboard/components/pageItems/HomePageSystemStatus.vue
  28. 149 0
      src/views/dataBigScreen/dashboard/components/pageItems/HomePageWarning.vue
  29. 270 0
      src/views/dataBigScreen/dashboard/components/pageItems/InspectionTask.vue
  30. 304 0
      src/views/dataBigScreen/dashboard/components/pageItems/IssueRectification.vue
  31. 101 0
      src/views/dataBigScreen/dashboard/components/pageItems/LearningGrowth.vue
  32. 44 0
      src/views/dataBigScreen/dashboard/components/pageItems/PersonalTaskCompletionRank.vue
  33. 170 0
      src/views/dataBigScreen/dashboard/components/pageItems/PersonnelOnDutySituation.vue
  34. 252 0
      src/views/dataBigScreen/dashboard/components/pageItems/ProblemDiscovery.vue
  35. 202 0
      src/views/dataBigScreen/dashboard/components/pageItems/QualificationCapability.vue
  36. 129 0
      src/views/dataBigScreen/dashboard/components/pageItems/QualificationCapabilityPersonal.vue
  37. 57 0
      src/views/dataBigScreen/dashboard/components/pageItems/SectionTaskDetails.vue
  38. 354 0
      src/views/dataBigScreen/dashboard/components/pageItems/SeizeDetails.vue
  39. 355 0
      src/views/dataBigScreen/dashboard/components/pageItems/StandardExecution.vue
  40. 58 0
      src/views/dataBigScreen/dashboard/components/pageItems/StationTaskDetails.vue
  41. 209 0
      src/views/dataBigScreen/dashboard/components/pageItems/SubjectiveImpression.vue
  42. 150 0
      src/views/dataBigScreen/dashboard/components/pageItems/WorkDetails.vue
  43. 141 0
      src/views/dataBigScreen/dashboard/components/pageItems/WorkOutput.vue
  44. 540 0
      src/views/dataBigScreen/dashboard/components/pageItems/WorkPortrait.vue
  45. 61 0
      src/views/dataBigScreen/dashboard/components/pageItems/index.js
  46. 33 0
      src/views/dataBigScreen/dashboard/components/pageItems/useTimeOut.js
  47. 700 0
      src/views/dataBigScreen/dashboard/components/pageView/HomePage.vue
  48. 295 0
      src/views/dataBigScreen/dashboard/components/pageView/OrganizationalPortrait.vue
  49. 449 0
      src/views/dataBigScreen/dashboard/components/pageView/OrganizationalPortraitSectionLevel.vue
  50. 248 0
      src/views/dataBigScreen/dashboard/components/pageView/QualityControl.vue
  51. 148 0
      src/views/dataBigScreen/dashboard/components/pageView/RealTimeStatus.vue
  52. 221 65
      src/views/dataBigScreen/dashboard/index.vue

+ 327 - 0
src/api/item/items.js

@@ -42,3 +42,330 @@ export function delItems(id) {
42 42
     method: 'delete'
43 43
   })
44 44
 }
45
+
46
+//查获总数量+移交公安数量+故意隐匿数量
47
+export function getTotalSome (params = {}) {
48
+  return request({
49
+    url: '/item/largeScreen/getAppTotalSome',
50
+    method: 'get',
51
+    params
52
+  })
53
+}
54
+
55
+//获取当天所有用户统计数据
56
+export function getDailyAllUsersRanking(params = {}) {
57
+  return request({
58
+    url: '/exam/daily/statistics/dashboard/all-users-ranking',
59
+    method: 'get',
60
+    params
61
+  })
62
+}
63
+
64
+//巡检执行
65
+export function getExecutionStatusTotal(params) {
66
+  return request({
67
+    url: '/check/largeScreen/inspectionExecute',
68
+    method: 'get',
69
+    params: params
70
+  })
71
+}
72
+
73
+// 获取通道统计
74
+export function getChannelStatistics(status) {
75
+  return request({
76
+    url: '/attendance/postRecord/count?regionalStatus='+status,
77
+    method: 'post',
78
+  })
79
+}
80
+
81
+// 答题个人排行
82
+export function userRanking() {
83
+  return request({
84
+    url: '/exam/daily/statistics/dashboard/user-ranking?timeRange=today&topN=10&sortBy=completionRate',
85
+    method: 'get',
86
+  })
87
+}
88
+// 获取通道统计
89
+export function deptRanking() {
90
+  return request({
91
+    url: '/exam/daily/statistics/dashboard/dept-ranking?timeRange=today&topN=10&sortBy=completionRate',
92
+    method: 'get',
93
+  })
94
+}
95
+
96
+
97
+// 获取总体问题分布
98
+export function problemDistribution(params = {}) {
99
+  return request({
100
+    url: '/check/largeScreen/problemDistribution',
101
+    method: 'get',
102
+    params
103
+  })
104
+}
105
+
106
+// 获取问题分布对比
107
+export function problemComparison(params = {}) {
108
+  return request({
109
+    url: '/check/largeScreen/problemComparison',
110
+    method: 'get',
111
+    params
112
+  })
113
+}
114
+
115
+// 获取计划安排总览
116
+export function planOverview(params={}) {
117
+  return request({
118
+    url: '/check/largeScreen/planOverview',
119
+    method: 'get',
120
+    params
121
+  })
122
+}
123
+// 获取日常任务检查指标累计分布
124
+export function planDistribution(params={}) {
125
+  return request({
126
+    url: '/check/largeScreen/planDistribution',
127
+    method: 'get',
128
+    params
129
+  })
130
+}
131
+// 获取任务明细统计
132
+export function planStatistics(params={}) {
133
+  return request({
134
+    url: '/check/largeScreen/planStatistics',
135
+    method: 'get',
136
+    params
137
+  })
138
+}
139
+
140
+// 查获总览
141
+export function getAppTotalSome(params = {}) {
142
+  return request({
143
+    url: '/item/largeScreen/getAppTotalSome',
144
+    method: 'get',
145
+    params
146
+  })
147
+}
148
+// 违禁品类别
149
+export function category(params = {}) {
150
+  return request({
151
+    url: '/item/largeScreen/category',
152
+    method: 'get',
153
+    params: {
154
+      levelType: 1,
155
+      ...params,
156
+    }
157
+  })
158
+}
159
+// 违禁品查获部位
160
+export function appPosition(params = {}) {
161
+  return request({
162
+    url: '/item/largeScreen/appPosition',
163
+    method: 'get',
164
+    params: {
165
+      levelType: 1,
166
+      ...params,
167
+    }
168
+  })
169
+}
170
+// 查获岗位
171
+export function largeScreenPost(params = {}) {
172
+  return request({
173
+    url: '/item/largeScreen/post',
174
+    method: 'get',
175
+    params
176
+  })
177
+}
178
+// 查获时间分布
179
+export function appTimeSpan(params = {}) {
180
+  return request({
181
+    url: '/item/largeScreen/appTimeSpan',
182
+    method: 'get',
183
+    params
184
+  })
185
+}
186
+
187
+// 问题整改统计
188
+export function correction(params = {}) {
189
+  return request({
190
+    url: '/check/largeScreen/correction',
191
+    method: 'get',
192
+    params
193
+  })
194
+}
195
+// 问题整改分布
196
+export function correctionDistribution(params = {}) {
197
+  return request({
198
+    url: '/check/largeScreen/correctionDistribution',
199
+    method: 'get',
200
+    params
201
+  })
202
+}
203
+
204
+//工作画像--查获审批时长统计(柱状图)
205
+export function getDurationChart (params) {
206
+  return request({
207
+    url: '/item/user-ranking/seizure-approval/duration',
208
+    method: 'get',
209
+    params: params
210
+  });
211
+}
212
+
213
+
214
+//获取站级别抽问抽答完成率
215
+export function getStationLevelRate (params = {}) {
216
+  return request({
217
+    url: '/exam/daily/site-profile/daily-completion-rate',
218
+    method: 'get',
219
+    params: params
220
+  });
221
+}
222
+//获取部门抽问抽答完成率
223
+export function getDepartmentLevelRate (params = {}) {
224
+  return request({
225
+    url: '/exam/daily/dept-profile/daily-completion-rate',
226
+    method: 'get',
227
+    params: params
228
+  });
229
+}
230
+//工作画像--查获趋势图,获取有效查获趋势数据(默认近90天)
231
+export function getSeizureTrendChart (params = {}) {
232
+  return request({
233
+    url: '/item/user-ranking/seizure-trend',
234
+    method: 'get',
235
+    params: params
236
+  });
237
+}
238
+
239
+//工作画像--通道开放趋势图(折线图)
240
+export function getChannelOpenTrendChart (params = {}) {
241
+  return request({
242
+    url: '/attendance/stats/channel/open/trend',
243
+    method: 'get',
244
+    params: params
245
+  });
246
+}
247
+
248
+export function getDeptList() {
249
+  return request({
250
+    url: '/system/user/deptTree',
251
+    method: 'get'
252
+  })
253
+}
254
+
255
+//能力画像-协同配合
256
+export function getCollaborationProfile(params) {
257
+  return request({
258
+    url: '/system/user/cooperation',
259
+    method: 'get',
260
+    params: params
261
+  })
262
+}
263
+//能力画像-明细
264
+export function getDetailProfile(params) {
265
+  return request({
266
+    url: '/system/user/detail',
267
+    method: 'get',
268
+    params: params
269
+  })
270
+}
271
+
272
+// 查获取指定模块的指标值
273
+export function getModuleMetrics(params) {
274
+  return request({
275
+    url: '/user/basic/portrait/module/info',
276
+    method: 'get',
277
+    params: params
278
+  })
279
+}
280
+//总体概览接口
281
+export function getOverview(params) {
282
+  return request({
283
+    url: '/system/user/population',
284
+    method: 'get',
285
+    params: params
286
+  })
287
+}
288
+
289
+//计算站级考勤工作统计
290
+export function getAttendanceStatistics (params) {
291
+  return request({
292
+    url: `/attendance/stats/station`,
293
+    method: 'get',
294
+    params: params  
295
+  })
296
+}
297
+//计算站级查获统计
298
+export function getSiteStatistics(params) {
299
+  return request({
300
+    url: `/item/user-ranking/station`,
301
+    method: 'get',
302
+    params: params
303
+  })
304
+}
305
+//能力画像-学习成长
306
+export function getGrowthPortrait(params) {
307
+  return request({
308
+    url: '/system/growth/portrait',
309
+    method: 'get',
310
+    params: params
311
+  })
312
+}
313
+
314
+//获取指定用户画像
315
+export function getUserProfile(params) {
316
+  return request({
317
+    url: '/exam/daily/user-profile',
318
+    method: 'get',
319
+    params: params
320
+  })
321
+}
322
+
323
+//获取班组和科室画像
324
+export function getDeptProfile(params) {
325
+  return request({
326
+    url: '/exam/daily/dept-profile',
327
+    method: 'get',
328
+    params: params
329
+  })
330
+}
331
+//获取站级画像
332
+export function getSiteProfile(params) {
333
+  return request({
334
+    url: '/exam/daily/site-profile',
335
+    method: 'get',
336
+    params: params
337
+  })
338
+}
339
+
340
+//巡检画像
341
+export function getPortrait(params) {
342
+  return request({
343
+    url: '/check/largeScreen/portrait',
344
+    method: 'get',
345
+    params: params
346
+  })
347
+}
348
+
349
+//获取所有部门和班组下人员
350
+export function getDeptUserTree(params) {
351
+  return request({
352
+    url: '/system/user/deptUserTree',
353
+    method: 'get',
354
+    params: params
355
+  })
356
+}
357
+//获取用户在指定层级的详细排名信息
358
+export function getRankInfo(params) {
359
+  return request({
360
+    url: '/item/user-ranking/ranking-detail',
361
+    method: 'get',
362
+    params: params
363
+  })
364
+}
365
+//根据用户ID查询用户信息
366
+export function getUserInfoById(userId) {
367
+  return request({
368
+    url: `/system/user/${userId}`,
369
+    method: 'get',
370
+  })
371
+}

+ 88 - 0
src/api/largeScreen/largeScreen.js

@@ -89,4 +89,92 @@ export function addCheckRecord(data) {
89 89
   })
90 90
 }
91 91
 
92
+// 查询我的任务列表(当天有效任务)shudong
93
+export function getHomePage(params) {
94
+  return request({
95
+    url: '/check/largeScreen/homePage',
96
+    method: 'get',
97
+    params: params
98
+  })
99
+}
100
+//获取查获上报数据  xiaoxiong
101
+export function getSeizureReport(params) {
102
+  return request({
103
+    url: '/system/check/seizureReport/data',
104
+    method: 'get',
105
+    params: params
106
+  })
107
+}
108
+
109
+//获取考勤统计数据   binge
110
+export function getAttendanceStats(params) {
111
+  return request({
112
+    url: '/attendance/stats/getAttendanceStats',
113
+    method: 'get',
114
+    params: params
115
+  })
116
+}
117
+
118
+//抽问抽答,首页   binge
119
+export function getAccuracyStatistics(params) {
120
+  return request({
121
+    url: '/exam/daily/accuracy-statistics',
122
+    method: 'get',
123
+    params: params
124
+  })
125
+}
126
+
127
+//根据角色获取查获排名  xiaoxiong
128
+export function getSeizureRanking(data) {
129
+  return request({
130
+    url: '/item/seizure/ranking/getRankingByRole',
131
+    method: 'post',
132
+    data: data
133
+  })
134
+}
135
+
92 136
 
137
+//获取检查排名  shudong
138
+export function getCheckRanking(params) {
139
+  return request({
140
+    url: '/system/homePage/homePageRanking',
141
+    method: 'get',
142
+    params: params
143
+  })
144
+}
145
+
146
+//首页-整体   shudong binge xiaoxiong
147
+export function getHomePageWhole(params) {
148
+  return request({
149
+    url: '/system/homePage/homePageWhole',
150
+    method: 'get',
151
+    params: params
152
+  })
153
+}
154
+//首页-明细(能力对比) shudong binge xiaoxiong
155
+export function getHomePageDetail(data) {
156
+  return request({
157
+    url: '/system/homePage/homePageDetail',
158
+    method: 'post',
159
+    data: data
160
+  })
161
+}
162
+
163
+//根据角色标识查询今日上岗用户列表
164
+export function selectUserListByRoleKey(data) {
165
+  return request({
166
+    url: '/attendance/postRecord/selectUserListByRoleKey',
167
+    method: 'post',
168
+    data: data
169
+  })
170
+}
171
+
172
+//首页报表-下载
173
+export function getHomeReportDownload(params) {
174
+  return request({
175
+    url: '/system/homeReport/download',
176
+    method: 'get',
177
+    params: params,
178
+    responseType: 'blob'
179
+  })
180
+}

BIN
src/assets/icons/one.png


BIN
src/assets/icons/three.png


BIN
src/assets/icons/two.png


+ 92 - 0
src/hooks/useEcharts.js

@@ -0,0 +1,92 @@
1
+import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
2
+import * as echarts from 'echarts';
3
+import { useResizeObserver } from '@vueuse/core';
4
+
5
+/**
6
+ *  ECharts Hook
7
+ * @param containerRef 图表容器 Ref
8
+ * @param options 图表配置 Ref
9
+ * @param theme 主题配置(可选)
10
+ * @param autoResize 是否自动响应容器大小变化(默认 true)
11
+ */
12
+export function useECharts(
13
+  containerRef,
14
+  options,
15
+  theme = 'macarons',
16
+  autoResize = true
17
+) {
18
+  const chartInstance = ref(null);
19
+  const resizeObserver = ref(null);
20
+
21
+  // 初始化图表
22
+  const init = () => {
23
+    if (!containerRef.value) return;
24
+    dispose();
25
+    chartInstance.value = markRaw(echarts.init(containerRef.value, theme))
26
+    chartInstance.value.setOption(options.value);
27
+    // 初始化 resize 监听
28
+    if (autoResize) {
29
+      setupResizeObserver();
30
+    }
31
+  };
32
+
33
+  // 设置 resize 监听
34
+  const setupResizeObserver = () => {
35
+    if (!containerRef.value) return;
36
+    
37
+    resizeObserver.value = useResizeObserver(containerRef.value, () => {
38
+      resize();
39
+    });
40
+  };
41
+
42
+  // 销毁图表
43
+  const dispose = () => {
44
+    // 移除 resize 监听
45
+    if (resizeObserver.value) {
46
+      resizeObserver.value.stop();
47
+      resizeObserver.value = null;
48
+    }
49
+    
50
+    // 销毁图表实例
51
+    if (chartInstance.value && !chartInstance.value.isDisposed()) {
52
+      chartInstance.value.dispose();
53
+      chartInstance.value = null;
54
+    }
55
+  };
56
+
57
+  // 重置图表
58
+  const reset = () => {
59
+    dispose();
60
+    init();
61
+  };
62
+
63
+  // 手动触发 resize
64
+  const resize = () => {
65
+    if (chartInstance.value) {
66
+      try {
67
+        chartInstance.value.resize();
68
+      } catch (error) {
69
+        console.error('ECharts resize 失败:', error);
70
+      }
71
+    }
72
+  };
73
+
74
+  // 监听 options 变化
75
+  watch(options, (newOptions) => {
76
+    if (chartInstance.value) {
77
+      chartInstance.value.setOption(newOptions);
78
+    }
79
+  }, { deep: true, immediate: true });
80
+
81
+  // 组件生命周期
82
+  onMounted(() => init());
83
+  onBeforeUnmount(() => dispose());
84
+
85
+  return {
86
+    instance: chartInstance,
87
+    init,
88
+    dispose,
89
+    reset,
90
+    resize
91
+  };
92
+}

+ 10 - 7
src/store/modules/user.js

@@ -15,7 +15,8 @@ const useUserStore = defineStore(
15 15
       nickName: '',
16 16
       avatar: '',
17 17
       roles: [],
18
-      permissions: []
18
+      permissions: [],
19
+      userInfo: {},
19 20
     }),
20 21
     actions: {
21 22
       // 登录
@@ -53,17 +54,18 @@ const useUserStore = defineStore(
53 54
             this.name = user.userName
54 55
             this.nickName = user.nickName
55 56
             this.avatar = avatar
57
+            this.userInfo = res.userInfo
56 58
             /* 初始密码提示 */
57
-            if(res.isDefaultModifyPwd) {
58
-              ElMessageBox.confirm('您的密码还是初始密码,请修改密码!',  '安全提示', {  confirmButtonText: '确定',  cancelButtonText: '取消',  type: 'warning' }).then(() => {
59
+            if (res.isDefaultModifyPwd) {
60
+              ElMessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
59 61
                 router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
60
-              }).catch(() => {})
62
+              }).catch(() => { })
61 63
             }
62 64
             /* 过期密码提示 */
63
-            if(!res.isDefaultModifyPwd && res.isPasswordExpired) {
64
-              ElMessageBox.confirm('您的密码已过期,请尽快修改密码!',  '安全提示', {  confirmButtonText: '确定',  cancelButtonText: '取消',  type: 'warning' }).then(() => {
65
+            if (!res.isDefaultModifyPwd && res.isPasswordExpired) {
66
+              ElMessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
65 67
                 router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
66
-              }).catch(() => {})
68
+              }).catch(() => { })
67 69
             }
68 70
             resolve(res)
69 71
           }).catch(error => {
@@ -78,6 +80,7 @@ const useUserStore = defineStore(
78 80
             this.token = ''
79 81
             this.roles = []
80 82
             this.permissions = []
83
+            this.userInfo = {}
81 84
             removeToken()
82 85
             resolve()
83 86
           }).catch(error => {

+ 118 - 0
src/views/dataBigScreen/dashboard/InactivityTimer.js

@@ -0,0 +1,118 @@
1
+export class InactivityTimer {
2
+  constructor(options = {}) {
3
+    this.defaults = {
4
+      timeout: 30000,       // 默认30秒超时
5
+      warningTime: 5000,   // 提前5秒警告
6
+      onTimeout: () => console.log('Timeout callback'),
7
+      onWarning: () => console.log('Warning callback'),
8
+      onReset: () => {},
9
+      onStart: () => {},
10
+      onStop: () => {}
11
+    };
12
+    
13
+    this.config = { ...this.defaults, ...options };
14
+    this.timer = null;
15
+    this.timeLeft = this.config.timeout;
16
+    this.isRunning = false;
17
+    this.isPaused = false;
18
+    this.warningTriggered = false;
19
+    this.startTime = null;
20
+    this.pageVisible = true;
21
+    
22
+    this.init();
23
+  }
24
+
25
+  init() {
26
+    document.addEventListener('visibilitychange', () => this.handleVisibilityChange());
27
+    this.setupActivityListeners();
28
+  }
29
+
30
+  setupActivityListeners() {
31
+    ['mousemove', 'keydown', 'click', 'scroll', 'touchstart'].forEach(event => {
32
+      document.addEventListener(event, () => this.handleUserActivity());
33
+    });
34
+  }
35
+
36
+  handleUserActivity() {
37
+    if (this.isRunning && !this.isPaused) {
38
+      this.resetTimer();
39
+    }
40
+  }
41
+
42
+  handleVisibilityChange() {
43
+    this.pageVisible = document.visibilityState === 'visible';
44
+    
45
+    if (this.pageVisible && this.isRunning && this.isPaused) {
46
+      this.isPaused = false;
47
+      this.startTime = Date.now() - (this.config.timeout - this.timeLeft);
48
+      this.startTimer();
49
+    } else if (!this.pageVisible && this.isRunning && !this.isPaused) {
50
+      this.isPaused = true;
51
+      this.clearTimer();
52
+    }
53
+  }
54
+
55
+  start() {
56
+    if (this.isRunning) return;
57
+    
58
+    this.isRunning = true;
59
+    this.isPaused = false;
60
+    this.warningTriggered = false;
61
+    this.startTime = Date.now();
62
+    this.timeLeft = this.config.timeout;
63
+    
64
+    this.config.onStart();
65
+    this.startTimer();
66
+  }
67
+
68
+  startTimer() {
69
+    this.clearTimer();
70
+    
71
+    this.timer = setInterval(() => {
72
+      if (!this.isPaused) {
73
+        const elapsed = Date.now() - this.startTime;
74
+        this.timeLeft = this.config.timeout - elapsed;
75
+        
76
+        if (this.timeLeft <= 0) {
77
+          this.clearTimer();
78
+          this.isRunning = false;
79
+          this.timeLeft = 0;
80
+          this.config.onTimeout();
81
+        } else if (this.timeLeft <= this.config.warningTime && !this.warningTriggered) {
82
+          this.warningTriggered = true;
83
+          this.config.onWarning();
84
+        }
85
+      }
86
+    }, 100);
87
+  }
88
+
89
+  resetTimer() {
90
+    this.startTime = Date.now();
91
+    this.timeLeft = this.config.timeout;
92
+    this.warningTriggered = false;
93
+    this.config.onReset();
94
+  }
95
+
96
+  reset() {
97
+    if (!this.isRunning) return;
98
+    this.resetTimer();
99
+  }
100
+
101
+  stop() {
102
+    if (!this.isRunning) return;
103
+    
104
+    this.clearTimer();
105
+    this.isRunning = false;
106
+    this.isPaused = false;
107
+    this.timeLeft = this.config.timeout;
108
+    this.warningTriggered = false;
109
+    this.config.onStop();
110
+  }
111
+
112
+  clearTimer() {
113
+    if (this.timer) {
114
+      clearInterval(this.timer);
115
+      this.timer = null;
116
+    }
117
+  }
118
+}

+ 1 - 1
src/views/dataBigScreen/dashboard/SeizedRanking.vue

@@ -3,7 +3,7 @@
3 3
     <template #right-menu>
4 4
       <div class="dashboard-right-menu">
5 5
         <span :class="{ active: currentType === 1 }" @click="handleClick(1)">
6
-          按主管
6
+          按大队
7 7
         </span>
8 8
         <span :class="{ active: currentType === 2 }" @click="handleClick(2)">
9 9
           按班组

+ 2 - 36
src/views/dataBigScreen/dashboard/TopTitle.vue

@@ -1,16 +1,9 @@
1 1
 <template>
2 2
   <div class="pageTop wow fadeInDown">
3 3
     <div class="pageTopbg"></div>
4
-    <div class="zhuangshi">
5
-      <div class="leftZhuangshi"></div>
6
-      <div class="rightZhuangshi"></div>
7
-    </div>
8
-    <div class="left"></div>
9 4
     <div class="title">
10 5
       <span>{{ name }}</span>
11 6
     </div>
12
-    <div class="right">
13
-    </div>
14 7
   </div>
15 8
 </template>
16 9
 
@@ -37,33 +30,6 @@
37 30
     align-content: flex-start;
38 31
     pointer-events: none;
39 32
     background: url('../../../assets/images/topbg-2521ad78.png') center top no-repeat;
40
-
41
-    .zhuangshi {
42
-      position: absolute;
43
-      top: 10px;
44
-      width: 96%;
45
-      left: 2%;
46
-      display: flex;
47
-      justify-content: space-between;
48
-      align-items: center;
49
-      flex-wrap: nowrap;
50
-      flex-direction: row;
51
-      align-content: flex-start;
52
-      z-index: 100;
53
-
54
-      .leftZhuangshi, .rightZhuangshi {
55
-        width: 192px;
56
-        background: url('../../../assets/images/下载.png') left center no-repeat;
57
-        height: 29px;
58
-        position: relative;
59
-      }
60
-
61
-      .rightZhuangshi {
62
-        position: relative;
63
-        transform: scaleX(-1);
64
-      }
65
-    }
66
-
67 33
     .title {
68 34
       position: relative;
69 35
       width: 40%;
@@ -76,8 +42,8 @@
76 42
       align-content: flex-start;
77 43
 
78 44
       span {
79
-        font-size: 36px;
80
-        font-weight: normal;
45
+        font-size: 42px;
46
+        font-weight: 900;
81 47
         color: #FFFFFF;
82 48
         margin-top: 15px;
83 49
         letter-spacing: 2px;

+ 146 - 0
src/views/dataBigScreen/dashboard/components/ChartsContainer.vue

@@ -0,0 +1,146 @@
1
+<template>
2
+  <div class="chartsContainer">
3
+    <div v-if=" title " class="chartsContainer-title">
4
+      <span class="title-text">{{ title }}</span>
5
+      <div class="title-slot">
6
+        <slot name="title"></slot>
7
+      </div>
8
+    </div>
9
+    <div class="chartsContainer-content">
10
+      <div class="chartsContainer-content-top" v-if=" name ">
11
+        <div class="chartsContainer-content-name">{{ name }}</div>
12
+        <div class="chartsContainer-content-other">
13
+          <slot name="other"></slot>
14
+        </div>
15
+      </div>
16
+      <div class="chartsContainer-content-describe" v-if=" $slots.describe ">
17
+        <slot name="describe"></slot>
18
+      </div>
19
+      <div class="chartsContainer-content-content">
20
+        <slot></slot>
21
+      </div>
22
+    </div>
23
+  </div>
24
+</template>
25
+
26
+<script setup>
27
+import { onMounted } from 'vue';
28
+const porps = defineProps({
29
+  title: {
30
+    type: String,
31
+    default: ''
32
+  },
33
+  name: {
34
+    type: String,
35
+    default: ''
36
+  }
37
+})
38
+</script>
39
+
40
+<style lang="scss" scoped>
41
+.chartsContainer {
42
+  width: 100%;
43
+  height: 100%;
44
+  position: relative;
45
+  background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
46
+  border-radius: 4px;
47
+  overflow: hidden;
48
+  display: flex;
49
+  flex-direction: column;
50
+
51
+  .chartsContainer-title {
52
+    height: 42px;
53
+    width: 100%;
54
+    background: linear-gradient(to right, #004387 0%, #090B18 100%);
55
+    border-left: 1px solid #1892CE;
56
+    line-height: 42px;
57
+    color: #fff;
58
+    font-weight: 900;
59
+    font-size: 20px;
60
+    text-indent: 1em;
61
+    display: flex;
62
+    align-items: center;
63
+    justify-content: space-between;
64
+    // padding: 0 15px;
65
+    
66
+    .title-text {
67
+      flex: 1;
68
+    }
69
+    
70
+    .title-slot {
71
+      display: flex;
72
+      align-items: center;
73
+      margin-left: 10px;
74
+    }
75
+  }
76
+
77
+  .chartsContainer-content {
78
+    padding: 20px 15px 10px;
79
+    box-sizing: border-box;
80
+    display: flex;
81
+    width: 100%;
82
+    flex: 1;
83
+    flex-direction: column;
84
+    row-gap: 10px;
85
+    overflow: hidden;
86
+
87
+    .chartsContainer-content-top {
88
+      display: flex;
89
+      align-items: center;
90
+      justify-content: space-between;
91
+      color: #78DEF5;
92
+
93
+      .chartsContainer-content-name {
94
+        color: #fff;
95
+        font-weight: bold;
96
+        font-size: 16px;
97
+        display: flex;
98
+        align-items: center;
99
+
100
+        &::before {
101
+          content: '';
102
+          display: inline-block;
103
+          border-left: 9px solid #1CB6FF;
104
+          border-top: 8px solid transparent;
105
+          border-right: 9px solid transparent;
106
+          border-bottom: 8px solid transparent;
107
+        }
108
+      }
109
+      .chartsContainer-content-other {
110
+        display: flex;
111
+        align-items: center;
112
+      }
113
+    }
114
+
115
+    .chartsContainer-content-describe {
116
+      color: #78DEF5;
117
+      font-size: 12px;
118
+      font-weight: bold;
119
+       display: flex;
120
+      align-items: center;
121
+    }
122
+
123
+    .chartsContainer-content-content {
124
+      flex: 1;
125
+      width: 100%;
126
+      overflow: hidden;
127
+      --el-bg-color: transparent;
128
+      --el-fill-color-blank: transparent;
129
+      --el-border-color-lighter: #1887A6;
130
+      --el-text-color-regular: #fff;
131
+      --el-fill-color-light: #5e93a1;
132
+
133
+      :deep(.el-table) {
134
+        .el-table__header-wrapper th {
135
+          background-color: #1CA7C8 !important;
136
+          color: #fff;
137
+        }
138
+
139
+        .cell {
140
+          font-weight: bold;
141
+        }
142
+      }
143
+    }
144
+  }
145
+}
146
+</style>

+ 10 - 57
src/views/dataBigScreen/dashboard/components/DashboardContainer.vue

@@ -1,16 +1,8 @@
1 1
 <template>
2
-  <div :class="{ none: !$slots.default}" class="dashboard-container">
2
+  <div :class="{ none: !$slots.default}" class="dashboard-container" ref="page">
3 3
     <slot></slot>
4 4
     <div v-if="!props.hide" class="wow home-main">
5
-      <div class="home-main-left wow slideInLeft">
6
-        <slot name="left"></slot>
7
-      </div>
8
-      <div class="home-main-center wow bounceIn">
9
-        <slot name="center"></slot>
10
-      </div>
11
-      <div class="home-main-right wow slideInRight">
12
-        <slot name="right"></slot>
13
-      </div>
5
+      <slot name="center"></slot>
14 6
     </div>
15 7
   </div>
16 8
 </template>
@@ -18,7 +10,7 @@
18 10
 <script setup>
19 11
   import { onMounted } from 'vue';
20 12
   import WOW from 'wow.js';
21
-
13
+  const page = ref(null)
22 14
   const props = defineProps({
23 15
     hide: {
24 16
       type: Boolean,
@@ -30,15 +22,18 @@
30 22
     const wow = new WOW();
31 23
     wow.init();
32 24
   });
25
+  defineExpose({
26
+    getRoot: () => page.value
27
+  })
33 28
 </script>
34 29
 
35 30
 <style lang="scss" scoped>
36 31
   .dashboard-container {
37 32
     position: relative;
38 33
     width: 100%;
39
-    min-height: 100vh;
40 34
     background: url('../../../../assets/images/bg-99b6904c.png');
41 35
     background-size: 100% 100%;
36
+    overflow: auto;
42 37
 
43 38
     &.none {
44 39
       .home-main {
@@ -47,51 +42,9 @@
47 42
     }
48 43
 
49 44
     .home-main {
50
-      position: relative;
51
-      width: calc(100% - 20px);
52
-      padding: 100px 0 20px;
53
-      margin-left: 20px;
54
-      height: 100vh;
55
-      display: flex;
56
-      z-index: 1;
57
-      justify-content: space-between;
58
-      align-items: flex-start;
59
-      flex-wrap: nowrap;
60
-      flex-direction: row;
61
-      pointer-events: none;
62
-      overflow: hidden;
63
-
64
-      .home-main-left, .home-main-right {
65
-        width: calc(30% + -0px);
66
-        position: relative;
67
-        height: calc(100% + -0px);
68
-        display: flex;
69
-        justify-content: space-between;
70
-        align-items: center;
71
-        flex-wrap: nowrap;
72
-        flex-direction: column;
73
-        align-content: flex-start;
74
-        z-index: 2;
75
-        pointer-events: initial;
76
-      }
77
-
78
-      .home-main-left-inner, .home-main-center, .home-main-right-inner {
79
-        width: calc(100% + -0px);
80
-        height: 100%;
81
-        position: relative;
82
-        display: flex;
83
-        justify-content: space-between;
84
-        align-items: center;
85
-        flex-wrap: nowrap;
86
-        flex-direction: column;
87
-        align-content: flex-start;
88
-        z-index: 2;
89
-        pointer-events: initial;
90
-      }
91
-
92
-      .home-main-center {
93
-        width: calc(40% - 20px);
94
-      }
45
+      width: 100%;
46
+      padding: 100px 10px 10px;
47
+      box-sizing: border-box;
95 48
     }
96 49
   }
97 50
 </style>

+ 113 - 0
src/views/dataBigScreen/dashboard/components/pageItems/Attendance.vue

@@ -0,0 +1,113 @@
1
+<template>
2
+  <ChartsContainer title="出勤投入">
3
+    <div ref="content" style="height: 100%;">
4
+      <div class="content-row" v-if="type">
5
+        <div class="content-row-list">
6
+          <div class="content-row-item">
7
+            <div>{{ props.type === 'personal' ? '出勤天数' : '人均出勤天数' }}</div>
8
+            <div>{{ rowTableItem.avgWorkingDays }}天</div>
9
+          </div>
10
+          <div class="content-row-item">
11
+            <div>{{  props.type === 'personal' ? '岗时长' : '人均上岗时长' }}</div>
12
+            <div>{{ rowTableItem.avgWorkingHours }}h</div>
13
+          </div>
14
+        </div>
15
+      </div>
16
+      <el-table v-else v-auto-scroll :data="tableData" border style="width: 100%" height="100%">
17
+        <el-table-column prop="dimension" label="统计维度" min-width="100" />
18
+        <el-table-column prop="avgWorkingDays" label="人均出勤天数" />
19
+        <el-table-column prop="avgWorkingHours" label="人均上岗时长" min-width="100" />
20
+      </el-table>
21
+    </div>
22
+  </ChartsContainer>
23
+</template>
24
+
25
+<script setup>
26
+import ChartsContainer from '../ChartsContainer.vue';
27
+import { ref } from 'vue';
28
+import { getAttendanceStatistics, getModuleMetrics } from '@/api/item/items'
29
+import { useTimeOut } from '../pageItems/useTimeOut'
30
+const props = defineProps({
31
+  type: {
32
+    type: String,
33
+    default: '' // team, department, station
34
+  }
35
+})
36
+const tableData = ref([])
37
+const rowTableItem = ref({
38
+  
39
+})
40
+const params = inject('provideParams')
41
+useTimeOut(() => {
42
+  if (props.type !== 'team' && props.type !== 'personal' ) {
43
+    params.value.deptId && getAttendanceStatistics({ deptId: params.value.deptId }).then(res => {
44
+      tableData.value = res
45
+      rowTableItem.value = (res || [ {} ])[ 0 ]
46
+    })
47
+  } else {
48
+    params.value.deptId && getModuleMetrics(
49
+      props.type === 'personal' ? { userId: params.value.deptId, userTypeStr: props.type } : { deptId: params.value.deptId, userTypeStr: props.type }
50
+    ).then(res => {
51
+    const { moduleResults = {
52
+      attendance: {
53
+        workingDate: {},
54
+        workingHours: {},
55
+      }
56
+    } } = res.data
57
+    if (props.type === 'personal') {
58
+      rowTableItem.value = {
59
+        avgWorkingDays: moduleResults.attendance.workingDate,
60
+        avgWorkingHours: moduleResults.attendance.workingHours
61
+      }
62
+    } else {
63
+      rowTableItem.value = {
64
+        avgWorkingDays: moduleResults.attendance.workingDate.averagePersonnel,
65
+        avgWorkingHours: moduleResults.attendance.workingHours.averagePersonnel
66
+      }
67
+    }
68
+    
69
+  })
70
+  }
71
+  
72
+}, [ params ])
73
+
74
+</script>
75
+
76
+<style lang="scss" scoped>
77
+.content-row {
78
+  display: flex;
79
+  flex-direction: column;
80
+  height: 100%;
81
+
82
+  .content-row-title {
83
+    font-size: 14px;
84
+    color: #77DBF4;
85
+    height: 30px;
86
+  }
87
+
88
+  .content-row-list {
89
+    display: flex;
90
+    flex: 1;
91
+    flex-direction: column;
92
+    justify-content: space-evenly;
93
+    row-gap: 15px;
94
+  }
95
+
96
+  .content-row-item {
97
+    flex: 1;
98
+    background: url('../../../../../assets/images/rowBg.png') no-repeat;
99
+    background-position: -10px;
100
+    background-size: calc(100% + 20px) 100%;
101
+    overflow: hidden;
102
+    display: flex;
103
+    justify-content: space-between;
104
+    align-items: center;
105
+    color: #fff;
106
+    font-size: 16px;
107
+    border-radius: 5px;
108
+    padding: 0 15px;
109
+    font-weight: bold;
110
+    max-height: 64px;
111
+  }
112
+}
113
+</style>

+ 182 - 0
src/views/dataBigScreen/dashboard/components/pageItems/ChannelOpeningSituation.vue

@@ -0,0 +1,182 @@
1
+<template>
2
+  <ChartsContainer title="通道开放情况">
3
+    <div ref="pie" style="height: 60%;"></div>
4
+    <div ref="bar" style="height: 40%;"></div>
5
+  </ChartsContainer>
6
+</template>
7
+
8
+<script setup>
9
+import { computed } from 'vue';
10
+import ChartsContainer from '../ChartsContainer.vue';
11
+import { useECharts } from '@/hooks/useEcharts.js';
12
+
13
+const pie = ref(null)
14
+const bar = ref(null)
15
+
16
+const props = defineProps({
17
+  channelCountInfo: {
18
+    type: Object,
19
+    default: () => ({
20
+      allData: 0, // 全部通道
21
+      openRate: 0, // 开放率
22
+      openData: 0,  // 开放通道数
23
+      closeData: 0, // 未开放通道数
24
+      barGrounp: [ 'T2航站楼', 'T1航站楼' ],
25
+      barOpenData: [ 0, 0 ],
26
+      barCloseData: [ -0, -0 ],
27
+      hasValidData: true
28
+    })
29
+  }
30
+})
31
+
32
+
33
+const setChartsOptions = computed(() => {
34
+  return {
35
+    graphic: props.channelCountInfo.hasValidData ? [ {
36
+      type: 'text',
37
+      left: '52%',
38
+      top: '40%',
39
+      style: {
40
+        text: '开放率',
41
+        font: 'bold 20px sans-serif',
42
+        fill: '#fff'
43
+      }
44
+    }, {
45
+      type: 'text',
46
+      left: '52%',
47
+      top: '50%',
48
+      style: {
49
+        text: props.channelCountInfo.openRate + '%',
50
+        font: 'bold 22px sans-serif',
51
+        fill: '#fff'
52
+      }
53
+    } ] : [ {
54
+      // 无数据时显示提示
55
+      type: 'text',
56
+      left: 'center',
57
+      top: 'center',
58
+      style: {
59
+        text: '暂无数据',
60
+        font: 'bold 20px sans-serif',
61
+        fill: '#fff'
62
+      }
63
+    } ],
64
+    legend: {
65
+      data: [ "开放", "未开放" ],
66
+      left: "left",
67
+      top: "center",
68
+      orient: "vertical",
69
+      textStyle: { color: "#fff" },
70
+    },
71
+    tooltip: {
72
+      trigger: 'item'
73
+    },
74
+    series: [ {
75
+      type: "pie",
76
+      radius: [ "50%", "70%" ],
77
+      center: [ "60%", "50%" ],
78
+      label: {
79
+        formatter: '{b}\n{c}',
80
+        color: '#fff',
81
+        fontSize: 14,
82
+      },
83
+      labelLine: {
84
+        length: 10
85
+      },
86
+      data: [
87
+        { value: props.channelCountInfo.openData, name: "开放", itemStyle: { color: "#408CFF" } },
88
+        { value: props.channelCountInfo.closeData, name: "未开放", itemStyle: { color: "#A7C8FF" } }
89
+      ]
90
+    } ],
91
+    animation: false,
92
+    grid: {
93
+      top: 10,
94
+      bottom: 50,
95
+      left: 20,
96
+      right: 20
97
+    },
98
+  }
99
+})
100
+const setChartsOptionsBar = computed(() => {
101
+  return {
102
+    grid: {
103
+      top: 20,
104
+      bottom: 20,
105
+      left: 80,
106
+      right: 20
107
+    },
108
+    legend: { show: false },
109
+    xAxis: {
110
+      type: 'value',
111
+      position: 'bottom',
112
+      axisLine: { show: true, lineStyle: { color: '#fff' } },
113
+      axisTick: { show: false },
114
+      axisLabel: { show: false },
115
+      splitLine: { show: false },
116
+      min: -props.channelCountInfo.allData || -10,
117
+      max: props.channelCountInfo.allData || 10
118
+    },
119
+    yAxis: {
120
+      type: 'category',
121
+      data: props.channelCountInfo.barGrounp,
122
+      axisLine: { show: false },
123
+      axisTick: { show: false },
124
+      axisLabel: {
125
+        color: '#fff',
126
+        fontSize: 14,
127
+        margin: 10
128
+      }
129
+    },
130
+    series: [
131
+      {
132
+        name: '开放',
133
+        type: 'bar',
134
+        stack: 'total',
135
+        barWidth: 20,
136
+        itemStyle: { color: '#408CFF' },
137
+        label: {
138
+          show: true,
139
+          position: 'left',
140
+          color: '#fff',
141
+          formatter: function (params) {
142
+            return Math.abs(params.value)
143
+          }
144
+        },
145
+        data: props.channelCountInfo.barOpenData // T2航站楼开放70,T1航站楼开放30
146
+      },
147
+      {
148
+        name: '未开放',
149
+        type: 'bar',
150
+        stack: 'total',
151
+        barWidth: 20,
152
+        itemStyle: { color: '#A7C8FF' },
153
+        label: {
154
+          show: true,
155
+          position: 'right',
156
+          color: '#fff',
157
+          formatter: function (params) {
158
+            return Math.abs(params.value)
159
+          }
160
+        },
161
+        data: props.channelCountInfo.barCloseData  // T2航站楼未开放30,T1航站楼未开放70
162
+      }
163
+    ],
164
+    tooltip: {
165
+      trigger: 'axis',
166
+      axisPointer: { type: 'shadow' },
167
+      formatter: function (params) {
168
+        return params[ 0 ].name + '<br/>' +
169
+          params[ 0 ].seriesName + ': ' + Math.abs(params[ 0 ].value) + '<br/>' +
170
+          params[ 1 ].seriesName + ': ' + Math.abs(params[ 1 ].value);
171
+      }
172
+    }
173
+  }
174
+})
175
+
176
+useECharts(pie, setChartsOptions);
177
+useECharts(bar, setChartsOptionsBar);
178
+
179
+
180
+</script>
181
+
182
+<style lang="scss" scoped></style>

+ 161 - 0
src/views/dataBigScreen/dashboard/components/pageItems/ChannelsAndPersonnel.vue

@@ -0,0 +1,161 @@
1
+<template>
2
+  <div class="personnel-status">
3
+    <ChannelOpeningSituation :channelCountInfo="channelCountData" />
4
+    <PersonnelOnDutySituation :personnelCountInfo="personnelCountData" />
5
+  </div>
6
+  <div class="information-details">
7
+    <WorkDetails v-model="detailsInfo" />
8
+  </div>
9
+
10
+</template>
11
+
12
+<script setup>
13
+import { ref } from 'vue';
14
+import { getChannelStatistics } from '@/api/item/items'
15
+import {
16
+  ChannelOpeningSituation,
17
+  PersonnelOnDutySituation,
18
+  WorkDetails
19
+} from './index';
20
+import { useTimeOut } from './useTimeOut'
21
+
22
+const channelCountData = ref({
23
+  allData: 0, // 全部通道
24
+  openRate: 0, // 开放率
25
+  openData: 0,  // 开放通道数
26
+  closeData: 0, // 未开放通道数
27
+  barGrounp: [ 'T2航站楼', 'T1航站楼' ],
28
+  barOpenData: [ 0, 0 ],
29
+  barCloseData: [ -0, -0 ],
30
+  hasValidData: true
31
+})
32
+
33
+const personnelCountData = ref({
34
+  allData: 0, // 全部人数
35
+  openRate: 0, // 在岗率
36
+  openData: 0,  // 在岗人数
37
+  closeData: 0, // 空闲人数
38
+  barGrounp: [ 'T2航站楼', 'T1航站楼' ],
39
+  barOpenData: [ 0, 0 ],
40
+  barCloseData: [ -0, -0 ],
41
+  hasValidData: true
42
+})
43
+
44
+const detailsInfo = ref([])
45
+
46
+
47
+const setChannelCountInfo = (result) => {
48
+  const { channelCount, maintainCount, onDutyCount, terminlDetail } = result;
49
+  const channelCountInfo = {
50
+    barGrounp: [ 'T2航站楼', 'T1航站楼' ],
51
+  }
52
+  // 开放通道信息 饼图数据
53
+  channelCountInfo.allData = channelCount
54
+  channelCountInfo.openData = terminlDetail.reduce((item, cur) => {
55
+    return item + cur.onDutyChannelCount;
56
+  }, 0);
57
+  channelCountInfo.openRate = Math.round((channelCountInfo.openData / channelCountInfo.allData) * 10000) / 100;
58
+
59
+  channelCountInfo.closeData = channelCountInfo.allData - channelCountInfo.openData;
60
+  // 检查数据是否有效
61
+  channelCountInfo.hasValidData = channelCountInfo.openData > 0 || channelCountInfo.closeData > 0;
62
+  // 开放通道信息 柱状图数据
63
+  let barT1 = terminlDetail && Array.isArray(terminlDetail)
64
+    ? terminlDetail.filter(item => item.terminlName === channelCountInfo.barGrounp[ 1 ])[ 0 ]
65
+    : null;
66
+  let barT2 = terminlDetail && Array.isArray(terminlDetail)
67
+    ? terminlDetail.filter(item => item.terminlName === channelCountInfo.barGrounp[ 0 ])[ 0 ]
68
+    : null;
69
+
70
+  let batT1Open = barT1?.onDutyChannelCount;//T1航站楼开放人数
71
+  let batT2Open = barT2?.onDutyChannelCount;//T2航站楼开放人数
72
+  let batT1Close = barT1 ? barT1.channelCount - barT1.onDutyChannelCount : 0;//T1航站楼未开放人数
73
+  let batT2Close = barT2 ? barT2.channelCount - barT2.onDutyChannelCount : 0;//T2航站楼未开放人数
74
+  channelCountInfo.barOpenData = [ batT2Open > 0 ? -batT2Open : 0, batT1Open > 0 ? -batT1Open : 0 ]
75
+  channelCountInfo.barCloseData = [ batT2Close, batT1Close ]
76
+  
77
+  channelCountData.value = channelCountInfo
78
+
79
+}
80
+const setPersonnelCountInfo = (result) => {
81
+  const { channelCount, maintainCount, onDutyCount, terminlDetail } = result;
82
+  const personnelCountInfo = {
83
+    barGrounp: [ 'T2航站楼', 'T1航站楼' ],
84
+  }
85
+  // 开放通道信息 饼图数据
86
+  personnelCountInfo.allData = maintainCount
87
+
88
+  personnelCountInfo.openData = terminlDetail.reduce((item, cur) => {
89
+    return item + cur.onDutyCount;
90
+  }, 0);
91
+  personnelCountInfo.openRate = Math.floor((personnelCountInfo.openData / personnelCountInfo.allData) * 10000) / 100;
92
+
93
+  personnelCountInfo.closeData = personnelCountInfo.allData - personnelCountInfo.openData;
94
+  // 检查数据是否有效
95
+  personnelCountInfo.hasValidData = personnelCountInfo.openData > 0 || personnelCountInfo.closeData > 0;
96
+  // 开放通道信息 柱状图数据
97
+  let barT1 = terminlDetail && Array.isArray(terminlDetail)
98
+    ? terminlDetail.filter(item => item.terminlName === personnelCountInfo.barGrounp[ 1 ])[ 0 ]
99
+    : null;
100
+  let barT2 = terminlDetail && Array.isArray(terminlDetail)
101
+    ? terminlDetail.filter(item => item.terminlName === personnelCountInfo.barGrounp[ 0 ])[ 0 ]
102
+    : null;
103
+  let batT1NormalPerson = barT1?.onDutyCount;//T1航站楼在岗人数
104
+  let batT2NormalPerson = barT2?.onDutyCount;//T2航站楼在岗人数
105
+  let batT1FreePerson = barT1 ? barT1.openChannelCount - barT1.onDutyCount : 0;//T1航站楼空闲人数
106
+  let batT2FreePerson = barT2 ? barT2.openChannelCount - barT2.onDutyCount : 0;//T2航站楼空闲人数
107
+  personnelCountInfo.barOpenData = [
108
+    batT2NormalPerson > 0 ? -batT2NormalPerson : 0,
109
+    batT1NormalPerson > 0 ? -batT1NormalPerson : 0 
110
+  ]
111
+  personnelCountInfo.barCloseData = [ batT2FreePerson, batT1FreePerson ]
112
+
113
+  personnelCountData.value = personnelCountInfo
114
+}
115
+
116
+useTimeOut(() => {
117
+  getChannelStatistics(2).then((result) => {
118
+    detailsInfo.value = result.terminlDetail.map(acc => {
119
+      const itemDataOptions = (acc.region || []).map(item => ({
120
+        label: item.regionalName,
121
+        value: item.regionalCode,
122
+        data: item.detailList || []
123
+      }))
124
+      return {
125
+        onDutyChannelCount: acc.onDutyChannelCount,
126
+        name: acc.terminlName,
127
+        options: itemDataOptions,
128
+        stateOptions: [ '在岗', '空闲' ],
129
+        stateActive: '在岗',
130
+        active: itemDataOptions[ 0 ] ? itemDataOptions[ 0 ].value : '',
131
+        data: itemDataOptions[ 0 ] ? itemDataOptions[ 0 ].data : [],
132
+        waitList: acc.waitList
133
+      }
134
+    })
135
+    setChannelCountInfo(result)
136
+    setPersonnelCountInfo(result)
137
+  })
138
+})
139
+
140
+</script>
141
+
142
+<style lang="scss" scoped>
143
+.public-inheritance-style {
144
+  width: 100%;
145
+  display: flex;
146
+  column-gap: 10px;
147
+}
148
+
149
+.personnel-status {
150
+  @extend .public-inheritance-style;
151
+  min-height: 420px;
152
+  height: 40%;
153
+
154
+}
155
+
156
+.information-details {
157
+  @extend .public-inheritance-style;
158
+  flex: 1;
159
+  overflow: hidden;
160
+}
161
+</style>

+ 61 - 0
src/views/dataBigScreen/dashboard/components/pageItems/ClassTaskCompletionRank.vue

@@ -0,0 +1,61 @@
1
+<template>
2
+  <ChartsContainer title="抽问抽答统计" name="班组任务完成情况">
3
+    <template #other>
4
+      <CustomStyleSelect v-model="activeClass" :options="tableData"/>
5
+    </template>
6
+    <div ref="content" style="height: 100%;">
7
+      <el-table v-auto-scroll :data="activeClassDataList" border style="width: 100%" height="100%">
8
+        <el-table-column prop="userName" label="姓名" width="90" />
9
+        <el-table-column prop="completedTasks" label="答题进度"  width="90" />
10
+        <el-table-column prop="avgScore" label="平均分" width="90" />
11
+        <el-table-column prop="completionRate" label="完成率">
12
+          <template #default="scope">
13
+            <el-progress
14
+              :text-inside="true"
15
+              :stroke-width="16"
16
+              :percentage="scope.row.completionRate"
17
+              style="--el-border-color-lighter: #fff"
18
+              color="#58A55C"
19
+            />
20
+          </template>
21
+        </el-table-column>
22
+      </el-table>
23
+    </div>
24
+  </ChartsContainer>
25
+</template>
26
+
27
+<script setup>
28
+import { onMounted, ref, computed } from 'vue';
29
+import ChartsContainer from '../ChartsContainer.vue';
30
+import CustomStyleSelect from './CustomStyleSelect.vue'
31
+import { getDailyAllUsersRanking } from '@/api/item/items'
32
+import { useTimeOut } from './useTimeOut'
33
+const tableData = ref([])
34
+const activeClass = ref('')
35
+
36
+const activeClassDataList = computed(() => {
37
+  return (tableData.value.find(item => item.value === activeClass.value) || { dataList: [] }).dataList
38
+})
39
+
40
+useTimeOut(() => {
41
+  getDailyAllUsersRanking({ pageNum: 1, pageSize: 100 }).then(res => {
42
+    tableData.value = (res.data.rows || []).reduce((cur, acc) => {
43
+      const item = cur.find(item => item.value === acc.deptName)
44
+      if (item) {
45
+        item.dataList.push(acc)
46
+      } else {
47
+        cur.push({
48
+          value: acc.deptName,
49
+          label: acc.deptName,
50
+          dataList: [acc]
51
+        })
52
+      }
53
+      return cur
54
+    }, [])
55
+    activeClass.value = tableData.value[0] ? tableData.value[0].value : ''
56
+  })
57
+})
58
+
59
+</script>
60
+
61
+<style lang="scss" scoped></style>

+ 57 - 0
src/views/dataBigScreen/dashboard/components/pageItems/ClassTaskDetails.vue

@@ -0,0 +1,57 @@
1
+<template>
2
+  <ChartsContainer name="班组级任务明细">
3
+    <template #describe>
4
+      完成情况:{{ `${tableData.notStartedCount || 0}/${tableData.inProgressCount || 0}/${tableData.completedCount || 0}` }}
5
+    </template>
6
+    <template #other>
7
+      <span style="margin-right: 10px; font-size: 14px; white-space: nowrap;">{{ tableData.taskName || '' }} 每日{{ tableData.ruleTypeNum || 1 }}次</span>
8
+      <CustomStyleSelect :options="selectList" v-model="selectActive"/>
9
+    </template>
10
+    <div style="height: 100%;">
11
+      <el-table  v-auto-scroll :data="tableData.checkLargeScreenInspectionExecuteItemDtoList" border style="width: 100%" height="100%">
12
+        <el-table-column prop="userName" label="姓名" width="90" />
13
+        <el-table-column prop="completionPercentage" label="今日完成比例">
14
+          <template #default="scope">
15
+            <el-progress
16
+              :text-inside="true"
17
+              :stroke-width="16"
18
+              :percentage="scope.row.completionPercentage"
19
+              style="--el-border-color-lighter: #fff"
20
+              color="#58A55C"
21
+            />
22
+          </template>
23
+        </el-table-column>
24
+        <el-table-column prop="checkOrderCount" label="整改单数" width="90" />
25
+        <el-table-column prop="rectificationOrderCount" label="不合格项数" width="100" />
26
+      </el-table>
27
+    </div>
28
+  </ChartsContainer>
29
+</template>
30
+
31
+<script setup>
32
+import ChartsContainer from '../ChartsContainer.vue';
33
+import CustomStyleSelect from './CustomStyleSelect.vue'
34
+import { computed, ref } from 'vue';
35
+
36
+const props = defineProps({
37
+  tableData: {
38
+    type: Array,
39
+    default: []
40
+  }
41
+})
42
+const selectActive = ref('')
43
+const selectList = computed(() => { 
44
+  const options = props.tableData.map(item => {
45
+    return ({ label: item.taskName, value: item.taskCode })
46
+  }) 
47
+  selectActive.value = options[0] ? options[0].value : ''
48
+  return options || []
49
+})
50
+
51
+const tableData = computed(() => {
52
+  return props.tableData.find(item => item.taskCode === selectActive.value) || {}
53
+})
54
+
55
+</script>
56
+
57
+<style lang="scss" scoped></style>

+ 405 - 0
src/views/dataBigScreen/dashboard/components/pageItems/Collaboration.vue

@@ -0,0 +1,405 @@
1
+<template>
2
+  <div class="chartsContainer">
3
+    <div class="chartsContainer-title">协同配合</div>
4
+    <div class="chartsContainer-list">
5
+      <div class="chartsContainer-content">
6
+        <div class="chartsContainer-content-top">
7
+          <div class="chartsContainer-content-name">性格特征</div>
8
+        </div>
9
+        <div class="chartsContainer-content-content" ref="bubblesRef" />
10
+      </div>
11
+
12
+      <div class="chartsContainer-content-right">
13
+        <div class="chartsContainer-content-item">
14
+          <div class="chartsContainer-content-top">
15
+            <div class="chartsContainer-content-name">工作风格</div>
16
+          </div>
17
+          <div class="chartsContainer-content-content" ref="pie" />
18
+        </div>
19
+
20
+        <div class="chartsContainer-content-item">
21
+          <div class="chartsContainer-content-top">
22
+            <div class="chartsContainer-content-name">性别人数</div>
23
+          </div>
24
+          <div class="chartsContainer-content-content" ref="bar" />
25
+        </div>
26
+      </div>
27
+    </div>
28
+  </div>
29
+</template>
30
+
31
+<script setup>
32
+import { getCollaborationProfile } from '@/api/item/items'
33
+import { useTimeOut } from './useTimeOut';
34
+import { useECharts } from '@/hooks/useEcharts.js';
35
+import { ref, computed, inject } from 'vue'
36
+
37
+const personalityData = ref([])
38
+const params = inject('provideParams')
39
+const bubblesRef = ref(null)
40
+const getBubblesOption = computed(() => {
41
+
42
+  let maxValue = 1; // 防止数据全都为 0,后面会除以  maxValue,为 0 会出错
43
+  let valueList = personalityData.value.map((item) => item.value);
44
+  maxValue = Math.max(maxValue, ...valueList);
45
+
46
+  // 设定尺寸的大小
47
+  let minSymbolSize = 70; // 最小尺寸
48
+  let symbolSize = 80; // 大小
49
+  let repulsion = symbolSize * 1.3; // 斥力 为了防止重叠
50
+
51
+  // 可以根据数据的多少,动态调整 symbolSize 的大小
52
+  let valueListLen = valueList.length;
53
+  if (valueListLen < 3) {
54
+    symbolSize = 120;
55
+  } else if (valueListLen < 5) {
56
+    symbolSize = 100;
57
+  }
58
+
59
+  // 获取要渲染的数据
60
+  let data = personalityData.value.map((item) => {
61
+    // 根据比例与最小尺寸,算出每个元素的大小
62
+    let size = Math.max(symbolSize * (item.value / maxValue), minSymbolSize);
63
+    return {
64
+      name: item.name,
65
+      value: item.value,
66
+      code: item.code,
67
+      symbolSize: size,
68
+      itemStyle: {
69
+        color: item.type === 'I' ? {
70
+          type: 'radial',
71
+          x: 0.4, y: 0.3, r: 1,
72
+          colorStops: [
73
+            { offset: 0, color: '#FFD700' },
74
+            { offset: 0.6, color: '#FFA500' },
75
+            { offset: 1, color: '#333333' }
76
+          ]
77
+        } : {
78
+          type: 'radial',
79
+          x: 0.4, y: 0.3, r: 1,
80
+          colorStops: [
81
+            { offset: 0, color: '#87CEEB' },
82
+            { offset: 0.6, color: '#1E90FF' },
83
+            { offset: 1, color: '#333333' }
84
+          ]
85
+        }
86
+      }
87
+    };
88
+  });
89
+
90
+  return {
91
+    xAxis: {
92
+      show: false,
93
+    },
94
+    yAxis: {
95
+      show: false,
96
+    },
97
+    series: [
98
+      {
99
+        data,
100
+        type: "graph", // 关系图
101
+        layout: "force", // 采用力引导布局
102
+        force: {
103
+          repulsion, // 值越大则斥力越大 每个元素间隔越大
104
+          gravity: 0.11,
105
+          edgeLength: 2,
106
+        },
107
+        roam: false,
108
+        draggable: true,
109
+        emphasis: {
110
+          scale: 1.1,
111
+          focus: 'series'
112
+        },
113
+        // 设置 label
114
+        label: {
115
+          show: true,
116
+          position: "inside",
117
+          formatter: (params) => {
118
+            const data = params.data
119
+            return [ data.code, data.name, `${data.value}人` ].join("\n")
120
+          },
121
+          fontSize: 12,
122
+          color: "#FFF",
123
+          align: "center",
124
+          lineHeight: 14,
125
+          fontWeight: 900
126
+        },
127
+        // 设置元素的样式
128
+        itemStyle: {
129
+          borderWidth: 1
130
+        },
131
+      },
132
+    ],
133
+  };
134
+})
135
+useECharts(bubblesRef, getBubblesOption)
136
+
137
+const pie = ref(null)
138
+const pieData = ref([])
139
+const pieALLCount = ref(0)
140
+const pieOption = computed(() => {
141
+  return {
142
+    tooltip: {
143
+      trigger: 'axis',
144
+      axisPointer: {
145
+        type: 'shadow'
146
+      },
147
+      formatter: function (params) {
148
+        return `${params[ 0 ].seriesName}: ${params[ 0 ].name}<br/>${params[ 0 ].marker}人数: ${params[ 0 ].value}人 (${Math.round(params[0].value / pieALLCount.value * 1000) / 10}%)`
149
+      }
150
+    },
151
+    grid: {
152
+      left: '20%',
153
+      right: '10%',
154
+      bottom: '10%',
155
+      top: '5%',
156
+    },
157
+    xAxis: {
158
+      type: 'value',
159
+      show: true,
160
+      axisLabel: {
161
+        color: '#fff',
162
+      },
163
+      axisLine: {
164
+        show: true,
165
+      },
166
+      minInterval: 1,
167
+    },
168
+    yAxis: {
169
+      type: 'category',
170
+      data: pieData.value.map(item => item.name),
171
+      axisLabel: {
172
+        color: '#fff',
173
+        formatter: (value) => {
174
+          return `${value.substring(0, 2)}  \n${value.substring(2).replace('(', '(').replace(')', ')')}`
175
+        }
176
+      }
177
+    },
178
+    series: [
179
+      {
180
+        name: '工作风格',
181
+        type: 'bar',
182
+        stack: 'total',
183
+        label: {
184
+          show: true,
185
+          color: '#fff'
186
+        },
187
+        color: '#408CFF',
188
+        emphasis: {
189
+          focus: 'series'
190
+        },
191
+        data: pieData.value.map(item => item.value),
192
+      }
193
+    ]
194
+  }
195
+})
196
+useECharts(pie, pieOption)
197
+
198
+const bar = ref(null)
199
+const sexList = ref([])
200
+const sexALLCount = ref(0)
201
+const barOption = computed(() => {
202
+  return {
203
+    tooltip: {
204
+      trigger: 'axis',
205
+      axisPointer: {
206
+        type: 'shadow'
207
+      },
208
+      formatter: function (params) {
209
+        return `${params[ 0 ].name}<br/>${params[ 0 ].marker}人数: ${params[ 0 ].value}人 (${Math.round(params[0].value / sexALLCount.value * 1000) / 10}%)`
210
+      }
211
+    },
212
+    grid: {
213
+      left: '20%',
214
+      right: '10%',
215
+      bottom: '10%',
216
+      top: '5%',
217
+    },
218
+    xAxis: {
219
+      type: 'value',
220
+      show: true,
221
+      axisLabel: {
222
+        color: '#fff'
223
+      },
224
+      axisLine: {
225
+        show: true,
226
+        color: '#fff'
227
+      },
228
+      minInterval: 1,
229
+    },
230
+    yAxis: {
231
+      type: 'category',
232
+      data: sexList.value.map(item => item.name),
233
+      axisLabel: {
234
+        color: '#fff'
235
+      }
236
+    },
237
+    series: [ {
238
+      name: '人数',
239
+      type: 'bar',
240
+      stack: 'total',
241
+      label: {
242
+        show: true,
243
+        color: '#fff'
244
+      },
245
+      color: '#408CFF',
246
+      emphasis: {
247
+        focus: 'series'
248
+      },
249
+      data: sexList.value.map(item => item.value)
250
+    } ]
251
+  }
252
+})
253
+useECharts(bar, barOption)
254
+
255
+useTimeOut(() => {
256
+  getCollaborationProfile({ deptId: params.value.deptId }).then(res => {
257
+    let pieALLCountNumber = 0
258
+    let sexALLCountNumber = 0
259
+    pieData.value = (res.data || { workingStyleList: [] }).workingStyleList.map(item => {
260
+      pieALLCountNumber += item.count
261
+      return { value: item.count, name: item.name }
262
+    })
263
+    pieALLCount.value = pieALLCountNumber
264
+    sexList.value = (res.data || { sexList: [] }).sexList.map(item => {
265
+      sexALLCountNumber += item.count
266
+      return { name: item.name, value: item.count }
267
+    })
268
+    sexALLCount.value = sexALLCountNumber
269
+    personalityData.value = (res.data || { characterCharacteristicsList: [] }).characterCharacteristicsList.map(item => {
270
+      return {
271
+        name: item.name.substring(4),
272
+        value: item.count,
273
+        code: item.code,
274
+        type: item.code.substring(0, 1)
275
+      }
276
+    })
277
+  })
278
+}, [ params ])
279
+
280
+</script>
281
+
282
+<style lang="scss" scoped>
283
+.chartsContainer {
284
+  width: 100%;
285
+  height: 100%;
286
+  position: relative;
287
+  background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
288
+  border-radius: 4px;
289
+  overflow: hidden;
290
+  display: flex;
291
+  flex-direction: column;
292
+
293
+  .chartsContainer-title {
294
+    height: 42px;
295
+    width: 100%;
296
+    background: linear-gradient(to right, #004387 0%, #090B18 100%);
297
+    border-left: 1px solid #1892CE;
298
+    line-height: 42px;
299
+    color: #fff;
300
+    font-weight: 900;
301
+    font-size: 20px;
302
+    text-indent: 1em;
303
+  }
304
+
305
+  .chartsContainer-list {
306
+    padding: 20px 15px 10px;
307
+    box-sizing: border-box;
308
+    display: flex;
309
+    flex: 1;
310
+    column-gap: 10px;
311
+    overflow: hidden;
312
+  }
313
+
314
+  .chartsContainer-content {
315
+    display: flex;
316
+    flex-direction: column;
317
+    row-gap: 10px;
318
+    flex: 1;
319
+
320
+    .chartsContainer-content-top {
321
+      display: flex;
322
+      align-items: center;
323
+      color: #78DEF5;
324
+
325
+      .chartsContainer-content-name {
326
+        color: #fff;
327
+        font-weight: bold;
328
+        font-size: 16px;
329
+        display: flex;
330
+        align-items: center;
331
+
332
+        &::before {
333
+          content: '';
334
+          display: inline-block;
335
+          border-left: 9px solid #1CB6FF;
336
+          border-top: 8px solid transparent;
337
+          border-right: 9px solid transparent;
338
+          border-bottom: 8px solid transparent;
339
+        }
340
+      }
341
+    }
342
+
343
+    .chartsContainer-content-describe {
344
+      color: #78DEF5;
345
+      font-size: 12px;
346
+      font-weight: bold;
347
+      display: flex;
348
+      align-items: center;
349
+    }
350
+
351
+    .chartsContainer-content-content {
352
+      flex: 1;
353
+      width: 100%;
354
+    }
355
+  }
356
+
357
+  .chartsContainer-content-right {
358
+    flex: 1;
359
+    display: flex;
360
+    flex-direction: column;
361
+    row-gap: 15px;
362
+    .chartsContainer-content-item {
363
+      display: flex;
364
+      flex-direction: column;
365
+      row-gap: 10px;
366
+      flex: 1;
367
+      .chartsContainer-content-top {
368
+        display: flex;
369
+        align-items: center;
370
+        color: #78DEF5;
371
+
372
+        .chartsContainer-content-name {
373
+          color: #fff;
374
+          font-weight: bold;
375
+          font-size: 16px;
376
+          display: flex;
377
+          align-items: center;
378
+
379
+          &::before {
380
+            content: '';
381
+            display: inline-block;
382
+            border-left: 9px solid #1CB6FF;
383
+            border-top: 8px solid transparent;
384
+            border-right: 9px solid transparent;
385
+            border-bottom: 8px solid transparent;
386
+          }
387
+        }
388
+      }
389
+
390
+      .chartsContainer-content-describe {
391
+        color: #78DEF5;
392
+        font-size: 12px;
393
+        font-weight: bold;
394
+        display: flex;
395
+        align-items: center;
396
+      }
397
+
398
+      .chartsContainer-content-content {
399
+        flex: 1;
400
+        width: 100%;
401
+      }
402
+    }
403
+  }
404
+}
405
+</style>

+ 192 - 0
src/views/dataBigScreen/dashboard/components/pageItems/CollaborationPersonal.vue

@@ -0,0 +1,192 @@
1
+<template>
2
+  <div class="chartsContainer">
3
+    <div class="chartsContainer-title">协同配合</div>
4
+    <div class="chartsContainer-list">
5
+      <div class="chartsContainer-content">
6
+        <div class="chartsContainer-content-top">
7
+          <div class="chartsContainer-content-name">团队配合</div>
8
+        </div>
9
+        <div class="chartsContainer-content-content" style="overflow-y: auto;">
10
+          <div v-if=" pageData.teamCooperationList " class="content-row-item"
11
+            v-for=" item in pageData.teamCooperationList ">
12
+            {{ item.nickName }}
13
+          </div>
14
+        </div>
15
+      </div>
16
+
17
+      <div class="chartsContainer-content">
18
+        <div class="chartsContainer-content-top">
19
+          <div class="chartsContainer-content-name">性格特征</div>
20
+        </div>
21
+        <div class="chartsContainer-content-content">
22
+          <div class="item-title">{{ getLabel( pageData.characterCharacteristics, sys_user_character_characteristics ) }}
23
+          </div>
24
+          <div class="item-img">
25
+            <img :src="getMbtjImgPath( pageData.characterCharacteristics )" alt="">
26
+          </div>
27
+        </div>
28
+      </div>
29
+
30
+      <div class="chartsContainer-content">
31
+        <div class="chartsContainer-content-top">
32
+          <div class="chartsContainer-content-name">性别</div>
33
+        </div>
34
+        <div class="chartsContainer-content-content">
35
+          <div class="item-title">{{ getLabel( pageData.sex, sys_user_sex ) }}</div>
36
+          <div class="item-img">
37
+            <img v-if=" getLabel( pageData.sex, sys_user_sex ) === '男' " :src="sex1" alt="">
38
+            <img v-if=" getLabel( pageData.sex, sys_user_sex ) === '女' " :src="sex2" alt="">
39
+          </div>
40
+        </div>
41
+      </div>
42
+    </div>
43
+  </div>
44
+</template>
45
+
46
+<script setup>
47
+import { getUserInfoById } from '@/api/item/items'
48
+import { useTimeOut } from './useTimeOut';
49
+import { ref, computed, inject } from 'vue'
50
+import { useDict } from '@/utils/dict'
51
+import sex1 from '@/assets/images/sex1.png'
52
+import sex2 from '@/assets/images/sex2.png'
53
+import { getMbtjImgPath } from '@/assets/images/mbtj/load.js'
54
+const pageData = ref({
55
+  teamCooperationList: []
56
+})
57
+const params = inject('provideParams')
58
+
59
+const {
60
+  sys_user_character_characteristics,
61
+  sys_user_sex
62
+} = useDict('sys_user_character_characteristics', 'sys_user_sex')
63
+
64
+const getLabel = (value, list = []) => {
65
+  const item = list.find(item => item.value.includes(value)) || {}
66
+  return item.label
67
+}
68
+useTimeOut(() => {
69
+  params.value.deptId && getUserInfoById(params.value.deptId).then(res => {
70
+    pageData.value = res.data || { teamCooperationList: [] }
71
+  })
72
+}, [ params ])
73
+
74
+</script>
75
+
76
+<style lang="scss" scoped>
77
+.chartsContainer {
78
+  width: 100%;
79
+  height: 100%;
80
+  position: relative;
81
+  background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
82
+  border-radius: 4px;
83
+  overflow: hidden;
84
+  display: flex;
85
+  flex-direction: column;
86
+
87
+  .chartsContainer-title {
88
+    height: 42px;
89
+    width: 100%;
90
+    background: linear-gradient(to right, #004387 0%, #090B18 100%);
91
+    border-left: 1px solid #1892CE;
92
+    line-height: 42px;
93
+    color: #fff;
94
+    font-weight: 900;
95
+    font-size: 20px;
96
+    text-indent: 1em;
97
+  }
98
+
99
+  .chartsContainer-list {
100
+    padding: 20px 15px 10px;
101
+    box-sizing: border-box;
102
+    display: flex;
103
+    flex: 1;
104
+    overflow: hidden;
105
+    column-gap: 10px;
106
+  }
107
+
108
+  .chartsContainer-content {
109
+    display: flex;
110
+    flex-direction: column;
111
+    row-gap: 10px;
112
+    flex: 1;
113
+
114
+    .chartsContainer-content-top {
115
+      display: flex;
116
+      align-items: center;
117
+      color: #78DEF5;
118
+
119
+      .chartsContainer-content-name {
120
+        color: #fff;
121
+        font-weight: bold;
122
+        font-size: 16px;
123
+        display: flex;
124
+        align-items: center;
125
+
126
+        &::before {
127
+          content: '';
128
+          display: inline-block;
129
+          border-left: 9px solid #1CB6FF;
130
+          border-top: 8px solid transparent;
131
+          border-right: 9px solid transparent;
132
+          border-bottom: 8px solid transparent;
133
+        }
134
+      }
135
+    }
136
+
137
+    .chartsContainer-content-describe {
138
+      color: #78DEF5;
139
+      font-size: 12px;
140
+      font-weight: bold;
141
+      display: flex;
142
+      align-items: center;
143
+    }
144
+
145
+    .chartsContainer-content-content {
146
+      flex: 1;
147
+      width: 100%;
148
+      display: flex;
149
+      flex-direction: column;
150
+      row-gap: 10px;
151
+
152
+      .item-title {
153
+        color: #fff;
154
+        font-weight: bold;
155
+        font-size: 16px;
156
+        text-align: center;
157
+        margin-top: 10px;
158
+      }
159
+
160
+      .item-img {
161
+        width: 80%;
162
+        margin: auto;
163
+        height: calc(100% - 40px);
164
+        display: flex;
165
+        align-items: center;
166
+        justify-content: center;
167
+
168
+        img {
169
+          max-width: 100%;
170
+          max-height: 80%;
171
+        }
172
+      }
173
+
174
+      .content-row-item {
175
+        max-width: 10em;
176
+        background: url('../../../../../assets/images/itemBg.png') no-repeat;
177
+        background-size: 100% 100%;
178
+        display: flex;
179
+        justify-content: center;
180
+        align-items: center;
181
+        color: #fff;
182
+        font-size: 16px;
183
+        border-radius: 5px;
184
+        padding: 0 15px;
185
+        font-weight: bold;
186
+        height: 44px;
187
+        white-space: nowrap;
188
+      }
189
+    }
190
+  }
191
+}
192
+</style>

+ 29 - 0
src/views/dataBigScreen/dashboard/components/pageItems/CustomStyleSegmented.vue

@@ -0,0 +1,29 @@
1
+<template>
2
+  <el-segmented
3
+    v-model="value"
4
+    :options="options"
5
+    v-bind="$attrs"
6
+    style="
7
+      --el-segmented-bg-color: #143D57;
8
+      --el-segmented-item-selected-bg-color: #1CA7C8;
9
+      --el-segmented-color: #fff;
10
+      --el-segmented-item-hover-color: #fff;
11
+      --el-segmented-item-active-bg-color: #0D507A;
12
+      --el-segmented-item-hover-bg-color: #0D507A;" 
13
+  />
14
+</template>
15
+
16
+<script setup>
17
+const porps = defineProps({
18
+  options: {
19
+    type: Array,
20
+    default: [ '在岗', '空闲' ]
21
+  }
22
+})
23
+
24
+const value = defineModel()
25
+
26
+</script>
27
+
28
+<style lang="scss" scoped>
29
+</style>

+ 105 - 0
src/views/dataBigScreen/dashboard/components/pageItems/CustomStyleSelect.vue

@@ -0,0 +1,105 @@
1
+<template>
2
+  <el-cascader class="custom-select" v-if="type === 'tree'" v-model="value" :options="options"
3
+    :props="{ checkStrictly: true, emitPath: false, ...propsConfig }" clearable @change="cascaderChange" style="
4
+      --el-fill-color-blank: #0D507A;
5
+      --el-border-color: #0D507A;
6
+      --el-select-border-color-hover: #1CA7C8;
7
+      --el-input-text-color: #fff;
8
+      --el-input-icon-color: #fff;
9
+      --el-text-color-regular: #fff;
10
+    " />
11
+  <el-select v-else class="custom-select" v-model="value" placeholder="请选择" @change="selectChange" clearable style="
12
+      min-width: 130px;
13
+      --el-fill-color-blank: #0D507A;
14
+      --el-border-color: #0D507A;
15
+      --el-select-border-color-hover: #1CA7C8;
16
+      --el-input-text-color: #fff;
17
+      --el-input-icon-color: #fff;
18
+      --el-text-color-regular: #fff;
19
+    ">
20
+    <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
21
+  </el-select>
22
+
23
+</template>
24
+
25
+<script setup>
26
+import { onMounted, onUnmounted } from 'vue'
27
+const props = defineProps({
28
+  options: {
29
+    type: Array,
30
+    default: []
31
+  },
32
+  type: {
33
+    type: String,
34
+    default: ''
35
+  },
36
+  propsConfig: {
37
+    type: Object,
38
+    default: {}
39
+  }
40
+})
41
+const emit = defineEmits(['change'])
42
+const value = defineModel()
43
+
44
+const selectChange = (value) => {
45
+  const selectItem = props.options.find(item => item.value === value) || {}
46
+  emit('change', selectItem)
47
+}
48
+
49
+const cascaderChange = (value) => {
50
+  if (value) {
51
+    emit('change', { value: value })
52
+  }
53
+
54
+}
55
+
56
+</script>
57
+
58
+
59
+<style lang="scss" scoped>
60
+.custom-select {
61
+  .el-select__wrapper {
62
+    line-height: var(--el-input-height, 24px);
63
+    min-height: var(--el-input-height, 32px);
64
+  }
65
+}
66
+
67
+/* 下拉框整体样式 */
68
+.custom-select .el-select-dropdown {
69
+  background-color: #0D507A !important;
70
+  border: 1px solid #0D507A !important;
71
+}
72
+
73
+/* 选项列表样式 */
74
+.custom-select .el-select-dropdown__list {
75
+  background-color: #0D507A !important;
76
+}
77
+
78
+/* 单个选项样式 */
79
+.custom-select .el-select-dropdown__item {
80
+  color: #D9D9D9 !important;
81
+  background-color: #0D507A !important;
82
+}
83
+
84
+/* 选项hover状态 */
85
+.custom-select .el-select-dropdown__item:hover {
86
+  background-color: #0D507A !important;
87
+  color: #fff !important;
88
+}
89
+
90
+/* 选中的选项样式 */
91
+.custom-select .el-select-dropdown__item.selected {
92
+  background-color: #1CA7C8 !important;
93
+  color: #fff !important;
94
+}
95
+
96
+/* 选项禁用状态 */
97
+.custom-select .el-select-dropdown__item.is-disabled {
98
+  color: #666 !important;
99
+}
100
+
101
+/* 下拉框箭头图标颜色 */
102
+.custom-select .el-select .el-input .el-select__caret {
103
+  color: #D9D9D9 !important;
104
+}
105
+</style>

+ 31 - 0
src/views/dataBigScreen/dashboard/components/pageItems/DataViewLeft.vue

@@ -0,0 +1,31 @@
1
+<template>
2
+  <StationTaskDetails :table-data="viewData.STATION_LEVEL" />
3
+  <SectionTaskDetails :table-data="viewData.DEPARTMENT_LEVEL" />
4
+  <ClassTaskDetails :table-data="viewData.TEAM_LEVEL" />
5
+</template>
6
+
7
+<script setup>
8
+import { reactive } from 'vue';
9
+import {
10
+  StationTaskDetails,
11
+  SectionTaskDetails,
12
+  ClassTaskDetails
13
+} from './index';
14
+import { getExecutionStatusTotal } from '@/api/item/items'
15
+import { useTimeOut } from './useTimeOut'
16
+const viewData = reactive({
17
+  STATION_LEVEL: [], // 站级
18
+  TEAM_LEVEL: [], // 班组级
19
+  DEPARTMENT_LEVEL: [], // 科级
20
+})
21
+useTimeOut(() => {
22
+  getExecutionStatusTotal().then(res => {
23
+    viewData.STATION_LEVEL = (res.data || []).filter(item => item.checkLevel === 'STATION_LEVEL')
24
+    viewData.TEAM_LEVEL = (res.data || []).filter(item => item.checkLevel === 'TEAM_LEVEL')
25
+    viewData.DEPARTMENT_LEVEL = (res.data || []).filter(item => item.checkLevel === 'DEPARTMENT_LEVEL')
26
+  })
27
+})
28
+
29
+</script>
30
+
31
+<style lang="scss" scoped></style>

+ 67 - 0
src/views/dataBigScreen/dashboard/components/pageItems/DepartmentInfo.vue

@@ -0,0 +1,67 @@
1
+<template>
2
+  <ChartsContainer :title="title">
3
+    <div ref="content" style="height: 100%;">
4
+      <el-table v-auto-scroll :data="tableData" border style="width: 100%" height="100%">
5
+        <el-table-column v-if="type === 'team'" prop="name" label="姓名" min-width="80" />
6
+        <el-table-column v-if="type === 'department'" prop="name" label="班组" min-width="80" />
7
+        <el-table-column v-if="type === 'station'" label="大队" prop="name"  min-width="80" />
8
+        <el-table-column prop="qualificationLevel" label="资质等级" v-if="type === 'team'">
9
+          <template #default="scope">
10
+            {{ getLabel(scope.row.qualificationLevel) }}
11
+          </template>
12
+        </el-table-column>
13
+        <el-table-column prop="qualificationLevel" label="资质等级高级" v-else />
14
+        <el-table-column prop="avgWorkingDays" label="人均出勤天数" min-width="120"/>
15
+        <el-table-column prop="avgWorkingHours" label="人均上岗时长" min-width="120" />
16
+        <el-table-column prop="avgSeizureCount" label="人均查获数量" min-width="120" />
17
+        <el-table-column prop="checkCount" label="人均巡检问题数" min-width="120" />
18
+        <el-table-column prop="testScore" label="测试平均分" min-width="100"/>
19
+      </el-table>
20
+    </div>
21
+  </ChartsContainer>
22
+</template>
23
+
24
+<script setup>
25
+import ChartsContainer from '../ChartsContainer.vue';
26
+import { inject, ref, computed } from 'vue';
27
+import { getDetailProfile } from '@/api/item/items'
28
+import { useTimeOut } from '../pageItems/useTimeOut'
29
+const props = defineProps({
30
+  type: {
31
+    type: String,
32
+    default: 'station' // team, department, station
33
+  }
34
+})
35
+
36
+const title = computed(() => {
37
+  if (props.type === 'team') {
38
+    return '人员明细'
39
+  } else if (props.type === 'department') {
40
+    return '班组明细'
41
+  } else {
42
+    return '大队明细'
43
+  }
44
+})
45
+
46
+const qualificationLevel = [
47
+  { value: ['LEVEL_ONE', 0, '0', '一'], label: '一级' },
48
+  { value: ['LEVEL_TWO', 1, '1', '二'], label: '二级' },
49
+  { value: ['LEVEL_THREE', 2, '2', '三'], label: '三级' },
50
+  { value: ['LEVEL_FOUR', 3, '3', '四'], label: '四级' },
51
+  { value: ['LEVEL_FIVE', 4, '4', '五'], label: '五级' },
52
+]
53
+const getLabel = (value) => {
54
+  const item = qualificationLevel.find(item => item.value.includes(value)) || {}
55
+  return item.label
56
+}
57
+const tableData = ref([])
58
+const params = inject('provideParams')
59
+useTimeOut(() => {
60
+  params.value.deptId  && getDetailProfile({ deptId: params.value.deptId }).then(res => {
61
+    tableData.value = res.data
62
+  })
63
+}, [params])
64
+
65
+</script>
66
+
67
+<style lang="scss" scoped></style>

+ 77 - 0
src/views/dataBigScreen/dashboard/components/pageItems/DepartmentalTaskCompletionRank.vue

@@ -0,0 +1,77 @@
1
+<template>
2
+  <ChartsContainer name="班组完成排行(Top10)">
3
+    <div ref="content" style="height: 100%;">
4
+    </div>
5
+  </ChartsContainer>
6
+</template>
7
+
8
+<script setup>
9
+import { onMounted, computed } from 'vue';
10
+import ChartsContainer from '../ChartsContainer.vue';
11
+import { useECharts } from '@/hooks/useEcharts.js';
12
+import { deptRanking } from '@/api/item/items'
13
+import { useTimeOut } from './useTimeOut'
14
+const content = ref(null)
15
+const datalist = ref([])
16
+const setChartsOptions = computed(() =>{
17
+  return {
18
+    backgroundColor: 'transparent',
19
+    grid: {
20
+      top: 50,
21
+      right: 20,
22
+      bottom: 30,
23
+      left: 40
24
+    },
25
+    legend: {
26
+      data: [ '已完成', '总任务数' ],
27
+      right: 'center',
28
+      top: 10,
29
+      textStyle: { color: '#fff' }
30
+    },
31
+    xAxis: {
32
+      type: 'category',
33
+      data: datalist.value.map(item => item.deptName),
34
+      axisLine: { lineStyle: { color: '#fff' } },
35
+      axisLabel: { color: '#fff' }
36
+    },
37
+    yAxis: {
38
+      type: 'value',
39
+      min: 0,
40
+      interval: 10,
41
+      max: Math.ceil(Math.max(datalist.value.map(item => item.totalTasks)) / 0.8),
42
+      axisLine: { lineStyle: { color: '#fff' } },
43
+      axisLabel: { color: '#fff' },
44
+      splitLine: { lineStyle: { color: '#fff' } }
45
+    },
46
+    tooltip: {
47
+      trigger: 'axis'
48
+    },
49
+    series: [
50
+      {
51
+        name: '已完成',
52
+        type: 'bar',
53
+        data: datalist.value.map(item => item.completedTasks),
54
+        itemStyle: { color: '#4CAF50' },
55
+        label: { show: true, position: 'top', color: '#fff' }
56
+      },
57
+      {
58
+        name: '总任务数',
59
+        type: 'bar',
60
+        data: datalist.value.map(item => item.totalTasks),
61
+        itemStyle: { color: '#2196F3' },
62
+        label: { show: true, position: 'top', color: '#fff' }
63
+      }
64
+    ]
65
+  }
66
+})
67
+
68
+useECharts(content, setChartsOptions);
69
+useTimeOut(() => {
70
+  deptRanking().then(res => {
71
+    datalist.value = res.data
72
+  })
73
+})
74
+
75
+</script>
76
+
77
+<style lang="scss" scoped></style>

+ 658 - 0
src/views/dataBigScreen/dashboard/components/pageItems/HomePageOverview.vue

@@ -0,0 +1,658 @@
1
+<template>
2
+  <ChartsContainer title="整体情况">
3
+    <template #title>
4
+      <div class="download-section">
5
+        <el-button type="primary" size="small" @click="handleDownloadReport" :loading="downloadLoading">
6
+          下载整体报表
7
+        </el-button>
8
+      </div>
9
+    </template>
10
+
11
+    <div class="overview-content">
12
+      <!-- 选择区域 -->
13
+      <div class="selection-section">
14
+        <div class="selection-row" v-for="(row, rowIndex) in selectionRows" :key="rowIndex">
15
+          <div v-for="(item, index) in row" :key="index" class="selection-item">
16
+
17
+            <CustomStyleSelect type="tree" :propsConfig="{ value: 'id', showPrefix: false }" v-model="item.selectedId"
18
+              :options="departments" @change="handleSelectionChange(rowIndex * 2 + index, $event)" />
19
+          </div>
20
+        </div>
21
+      </div>
22
+
23
+      <!-- 雷达图 -->
24
+      <div class="radar-chart" ref="radarChartRef"></div>
25
+
26
+      <!-- 遍历展示的数据 -->
27
+      <div class="data-list" v-if="dataItems.length > 0">
28
+        <div class="data-item" v-for="(item, index) in dataItems" :key="index">
29
+          <div class="data-title" :style="{ color: '#70D3EE' }">{{ item.title }}</div>
30
+          <div class="data-values">
31
+            <div class="value-label" v-for="(data, dataIndex) in item.list" :key="dataIndex">
32
+              <div class="value">{{ data.value }}</div>
33
+              <div class="label">{{ data.label }}</div>
34
+            </div>
35
+          </div>
36
+        </div>
37
+      </div>
38
+    </div>
39
+
40
+
41
+  </ChartsContainer>
42
+</template>
43
+
44
+<script setup>
45
+import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
46
+import * as echarts from 'echarts'
47
+import ChartsContainer from '../ChartsContainer.vue'
48
+import CustomStyleSelect from './CustomStyleSelect.vue'
49
+import { getDeptUserTree } from '@/api/item/items'
50
+import { getHomePageDetail, getHomeReportDownload } from '@/api/largeScreen/largeScreen'
51
+
52
+const radarChartRef = ref(null)
53
+let radarChart = null
54
+
55
+// 下载相关变量
56
+const downloadLoading = ref(false)
57
+
58
+// 定义props,从父组件接收数据
59
+const props = defineProps({
60
+  timeRange: {
61
+    type: String,
62
+    default: 'year'
63
+  },
64
+  startDate: {
65
+    type: String,
66
+    default: ''
67
+  },
68
+  endDate: {
69
+    type: String,
70
+    default: ''
71
+  },
72
+  selectedRole: {
73
+    type: String,
74
+    default: 'individual'
75
+  }
76
+})
77
+
78
+// 遍历展示的数据
79
+const dataItems = ref([])
80
+
81
+// 选择相关数据
82
+const selectionItems = ref([
83
+
84
+])
85
+
86
+// 计算属性,将选择项分成两行,每行两个
87
+const selectionRows = computed(() => {
88
+  const rows = []
89
+  for (let i = 0; i < selectionItems.value.length; i += 2) {
90
+    rows.push(selectionItems.value.slice(i, i + 2))
91
+  }
92
+  return rows
93
+})
94
+
95
+const departments = ref([])
96
+
97
+// 计算日期范围(移植自HomePage.vue)
98
+const calculateDateRange = (timeRange, customStartDate, customEndDate) => {
99
+  const today = new Date()
100
+  const yesterday = new Date(today)
101
+  yesterday.setDate(today.getDate() - 1)
102
+
103
+  let startDate = new Date(yesterday)
104
+  let endDate = new Date(yesterday)
105
+
106
+  switch (timeRange) {
107
+    case 'week':
108
+      startDate.setDate(yesterday.getDate() - 6)
109
+      break
110
+    case 'month':
111
+      startDate.setDate(yesterday.getDate() - 29)
112
+      break
113
+    case 'quarter':
114
+      startDate.setDate(yesterday.getDate() - 89)
115
+      break
116
+    case 'halfYear':
117
+      startDate.setDate(yesterday.getDate() - 179)
118
+      break
119
+    case 'year':
120
+      startDate.setDate(yesterday.getDate() - 364)
121
+      break
122
+    case 'custom':
123
+      if (customStartDate && customEndDate) {
124
+        startDate = new Date(customStartDate)
125
+        endDate = new Date(customEndDate)
126
+      }
127
+      break
128
+    default:
129
+      startDate.setDate(yesterday.getDate() - 364)
130
+  }
131
+
132
+  return {
133
+    startDate: formatDateForInput(startDate),
134
+    endDate: formatDateForInput(endDate)
135
+  }
136
+}
137
+
138
+// 格式化日期为输入框格式
139
+const formatDateForInput = (date) => {
140
+  const year = date.getFullYear()
141
+  const month = String(date.getMonth() + 1).padStart(2, '0')
142
+  const day = String(date.getDate()).padStart(2, '0')
143
+  return `${year}-${month}-${day}`
144
+}
145
+
146
+// 监听时间参数变化
147
+watch(() => [props.timeRange, props.startDate, props.endDate, props.selectedRole], () => {
148
+  // 当时间参数变化时,重新获取数据
149
+  if (selectionItems.value.some(item => item.selectedId)) {
150
+    fetchHomePageDetailData()
151
+  }
152
+}, { immediate: true })
153
+
154
+// 更新雷达图数据
155
+const updateChartWithData = (data) => {
156
+  if (!radarChartRef.value) {
157
+    console.warn('雷达图容器未找到')
158
+    return
159
+  }
160
+
161
+  if (!radarChart) {
162
+    radarChart = echarts.init(radarChartRef.value)
163
+  }
164
+
165
+  // 根据接口数据更新雷达图
166
+  if (data && data.length > 0) {
167
+    // 使用Graph后缀的数据来显示雷达图
168
+    const seriesData = data.map(item => ({
169
+      name: item.name,
170
+      value: [
171
+        item.seizureCountGraph || 0,
172
+        item.workingHoursGraph || 0,
173
+        item.checkPassRateGraph || 0,
174
+        item.answersAccuracyGraph || 0,
175
+        item.learningGrowthScoreGraph || 0
176
+      ]
177
+    }))
178
+
179
+    // 计算每个指标的最大值,用于雷达图的max值
180
+    const indicators = [
181
+      { name: '查获能力', max: Math.max(...seriesData.map(d => d.value[0])) },
182
+      { name: '在岗时长', max: Math.max(...seriesData.map(d => d.value[1])) },
183
+      { name: '巡检合格率', max: Math.max(...seriesData.map(d => d.value[2])) },
184
+      { name: '抽问抽答', max: Math.max(...seriesData.map(d => d.value[3])) },
185
+      { name: '培训答题', max: Math.max(...seriesData.map(d => d.value[4])) }
186
+    ]
187
+
188
+    const option = {
189
+      legend: {
190
+        type: 'scroll',
191
+        orient: 'horizontal',
192
+        bottom: 10,
193
+        textStyle: {
194
+          fontSize: 12,
195
+          color: '#fff'
196
+        },
197
+        itemWidth: 12,
198
+        itemHeight: 12,
199
+        data: seriesData.map(item => item.name)
200
+      },
201
+      radar: {
202
+        shape: 'circle',
203
+        indicator: indicators,
204
+        splitNumber: 4,
205
+        center: ['50%', '45%'],
206
+        radius: '65%',
207
+        axisName: {
208
+          color: '#fff',
209
+          fontSize: 12
210
+        },
211
+        splitLine: {
212
+          lineStyle: {
213
+            color: ['rgba(255, 255, 255, 0.3)', 'rgba(255, 255, 255, 0.2)', 'rgba(255, 255, 255, 0.1)']
214
+          }
215
+        },
216
+        splitArea: {
217
+          show: true,
218
+          areaStyle: {
219
+            color: ['rgba(255, 255, 255, 0.1)', 'rgba(255, 255, 255, 0.05)']
220
+          }
221
+        },
222
+        axisLine: {
223
+          lineStyle: {
224
+            color: 'rgba(255, 255, 255, 0.5)'
225
+          }
226
+        }
227
+      },
228
+      series: [
229
+        {
230
+          name: '能力对比',
231
+          type: 'radar',
232
+          data: seriesData.map((item, index) => ({
233
+            value: item.value,
234
+            name: item.name,
235
+            areaStyle: {
236
+              color: getChartColor(index, 0.3)
237
+            },
238
+            lineStyle: {
239
+              color: getChartColor(index),
240
+              width: 2
241
+            },
242
+            itemStyle: {
243
+              color: getChartColor(index)
244
+            }
245
+          }))
246
+        }
247
+      ],
248
+      tooltip: {
249
+        show: false,
250
+        // trigger: 'item',
251
+        // formatter: function (params) {
252
+        //   const data = params.data
253
+        //   const name = params.name
254
+        //   const value = params.value
255
+
256
+        //   let tooltipHtml = `<div style="font-weight: bold; margin-bottom: 8px;">${name}</div>`
257
+
258
+        //   const labels = ['查获数量', '在岗时长', '巡检合格率', '抽问抽答', '培训答题']
259
+
260
+        //   value.forEach((val, index) => {
261
+        //     tooltipHtml += `<div style="display: flex; justify-content: space-between; margin: 4px 0;">
262
+        //         <span>${labels[index]}:</span>
263
+        //         <span style="font-weight: bold;">${val}</span>
264
+        //       </div>`
265
+        //   })
266
+
267
+        //   return tooltipHtml
268
+        // }
269
+      }
270
+    }
271
+
272
+    radarChart.setOption(option)
273
+  }
274
+}
275
+
276
+// 获取图表颜色
277
+const getChartColor = (index, opacity = 1) => {
278
+  const colors = ['#8FA5EC', '#FF9F7F', '#6ECEB2', '#FFD700', '#BA55D3']
279
+  const color = colors[index % colors.length]
280
+  if (opacity < 1) {
281
+    // 转换为RGBA格式
282
+    const r = parseInt(color.slice(1, 3), 16)
283
+    const g = parseInt(color.slice(3, 5), 16)
284
+    const b = parseInt(color.slice(5, 7), 16)
285
+    return `rgba(${r}, ${g}, ${b}, ${opacity})`
286
+  }
287
+  return color
288
+}
289
+// 获取部门和人员树数据
290
+const fetchDepartments = async () => {
291
+
292
+  try {
293
+    const res = await getDeptUserTree()
294
+    departments.value = res.data
295
+     
296
+    // 数据加载完成后,查找deptType为BRIGADE的对象
297
+    if (res.data && res.data.length > 0) {
298
+      // 查找deptType为BRIGADE的对象
299
+      const findBrigadeItems = (items) => {
300
+        const brigadeItems = []
301
+        for (const item of items) {
302
+          if (item.deptType === 'BRIGADE') {
303
+            brigadeItems.push(item)
304
+          }
305
+          if (item.children && item.children.length > 0) {
306
+            const childBrigadeItems = findBrigadeItems(item.children)
307
+            brigadeItems.push(...childBrigadeItems)
308
+          }
309
+        }
310
+        return brigadeItems
311
+      }
312
+
313
+      const brigadeItems = findBrigadeItems(res.data)
314
+      
315
+      // 设置前4个BRIGADE对象作为默认选项
316
+      if (brigadeItems.length > 0) {
317
+        for (let i = 0; i < Math.min(brigadeItems.length, 4); i++) {
318
+          selectionItems.value[i] = {
319
+            ...brigadeItems[i],
320
+            selectedId: brigadeItems[i].id,
321
+            selectedName: brigadeItems[i].label || brigadeItems[i].name
322
+          }
323
+        }
324
+        // 触发数据请求
325
+        fetchHomePageDetailData()
326
+      }
327
+    }
328
+  } catch (error) {
329
+    console.error('获取部门和人员树失败:', error)
330
+  }
331
+}
332
+
333
+// 选择变化处理
334
+const handleSelectionChange = (index, selectedItem) => {
335
+  console.log(selectedItem, "selectedItem")
336
+  if (selectedItem && selectedItem.value) {
337
+    // 查找选中的部门或人员信息
338
+    const findSelectedItem = (items, targetId) => {
339
+      for (const item of items) {
340
+        if (item.id === targetId) {
341
+          return item
342
+        }
343
+        if (item.children && item.children.length > 0) {
344
+          const found = findSelectedItem(item.children, targetId)
345
+          if (found) return found
346
+        }
347
+      }
348
+      return null
349
+    }
350
+
351
+    const itemInfo = findSelectedItem(departments.value, selectedItem.value)
352
+    if (itemInfo) {
353
+      selectionItems.value[index] = {
354
+        ...selectionItems.value[index],
355
+        selectedId: selectedItem.value,
356
+        selectedName: itemInfo.label || itemInfo.name
357
+      }
358
+    }
359
+  } else {
360
+    // 清空选择
361
+    selectionItems.value[index] = {
362
+      ...selectionItems.value[index],
363
+      selectedId: '',
364
+      selectedName: ''
365
+    }
366
+  }
367
+  // 触发数据请求
368
+  fetchHomePageDetailData()
369
+}
370
+
371
+// 获取首页详情数据
372
+const fetchHomePageDetailData = async () => {
373
+  // 构建参数数组
374
+  const params = []
375
+
376
+  // 计算开始和结束日期
377
+  let startDate = props.startDate
378
+  let endDate = props.endDate
379
+
380
+  // 如果使用预设时间范围,计算日期
381
+  if (props.timeRange !== 'custom' && (!startDate || !endDate)) {
382
+    const dateRange = calculateDateRange(props.timeRange)
383
+    startDate = dateRange.startDate
384
+    endDate = dateRange.endDate
385
+  }
386
+
387
+  for (const item of selectionItems.value) {
388
+    if (item.selectedId) {
389
+      // 查找选中的项目类型
390
+      const findItemType = (items, targetId) => {
391
+        for (const item of items) {
392
+          if (item.id === targetId) {
393
+            return item.nodeType === 'user' ? 'USER' : 'DEPT'
394
+          }
395
+          if (item.children && item.children.length > 0) {
396
+            const type = findItemType(item.children, targetId)
397
+            if (type) return type
398
+          }
399
+        }
400
+
401
+      }
402
+
403
+
404
+      const type = findItemType(departments.value, item.selectedId)
405
+
406
+      params.push({
407
+        startDate: startDate,
408
+        endDate: endDate,
409
+        type: type,
410
+        id: item.selectedId,
411
+        name: item.selectedName,
412
+        dataSource: props.selectedRole
413
+      })
414
+    }
415
+  }
416
+  console.log(params)
417
+  if (params.length > 0) {
418
+    try {
419
+      const res = await getHomePageDetail(params)
420
+      console.log('首页详情数据:', res)
421
+      // 处理响应数据,更新图表和数据显示
422
+      handleHomePageDetailResponse(res)
423
+    } catch (error) {
424
+      console.error('获取首页详情数据失败:', error)
425
+    }
426
+  } else {
427
+    // 如果没有选择任何项,清空显示的数据
428
+    dataItems.value = []
429
+    if (radarChart) {
430
+      radarChart.clear()
431
+    }
432
+  }
433
+}
434
+
435
+// 处理首页详情响应数据
436
+const handleHomePageDetailResponse = (res) => {
437
+  if (res.data && Array.isArray(res.data)) {
438
+    // 更新数据展示
439
+    dataItems.value = res.data.map(item => {
440
+      return {
441
+        title: item.name + "数据明细",
442
+        list: [
443
+          { label: '人均查获数量', value: item.seizureCount || 0 },
444
+          { label: '人均在岗时长', value: item.workingHours || 0 },
445
+          { label: '巡检合格率', value: `${((item.checkPassRate * 100) || 0).toFixed(2)}%` },
446
+          { label: '抽问抽答正确率', value: `${((item.answersAccuracy * 100) || 0).toFixed(2)}%` },
447
+          { label: '培训答题平均分', value: item.learningGrowthScore || 0 }
448
+        ]
449
+      }
450
+    })
451
+
452
+    // 更新雷达图数据
453
+    updateChartWithData(res.data)
454
+  }
455
+}
456
+
457
+const handleResize = () => {
458
+  if (radarChart) {
459
+    radarChart.resize()
460
+  }
461
+}
462
+
463
+onMounted(() => {
464
+  window.addEventListener('resize', handleResize)
465
+  // 获取部门和人员树数据
466
+  fetchDepartments()
467
+})
468
+
469
+onUnmounted(() => {
470
+  if (radarChart) {
471
+    radarChart.dispose()
472
+  }
473
+  window.removeEventListener('resize', handleResize)
474
+})
475
+
476
+// 下载整体报表
477
+const handleDownloadReport = async () => {
478
+  try {
479
+    downloadLoading.value = true
480
+
481
+    // 构建下载参数
482
+    const downloadParams = {
483
+      startDate: props.startDate,
484
+      endDate: props.endDate,
485
+    }
486
+
487
+    // 调用下载接口
488
+    const response = await getHomeReportDownload(downloadParams)
489
+
490
+    // 处理下载响应 - 使用项目标准的blob处理方式
491
+    if (response) {
492
+      // 生成文件名
493
+      const fileName = `整体报表_${new Date().toISOString().slice(0, 10)}.xlsx`
494
+
495
+      // 使用file-saver的saveAs函数下载文件
496
+      const { saveAs } = await import('file-saver')
497
+      saveAs(response, fileName)
498
+    }
499
+
500
+  } catch (error) {
501
+    console.error('下载报表失败:', error)
502
+    // 这里可以添加错误提示
503
+  } finally {
504
+    downloadLoading.value = false
505
+  }
506
+}
507
+</script>
508
+
509
+<style lang="scss" scoped>
510
+.download-section {
511
+  display: flex;
512
+  justify-content: flex-end;
513
+  padding: 0 10px;
514
+
515
+
516
+  .el-button {
517
+    background: linear-gradient(135deg, #1CB6FF 0%, #0A8CD9 100%);
518
+    border: none;
519
+    border-radius: 4px;
520
+    font-size: 12px;
521
+    padding: 6px 12px;
522
+
523
+    &:hover {
524
+      background: linear-gradient(135deg, #0A8CD9 0%, #1CB6FF 100%);
525
+    }
526
+  }
527
+}
528
+
529
+.overview-content {
530
+  height: 100%;
531
+  display: flex;
532
+  flex-direction: column;
533
+  gap: 15px;
534
+
535
+  .selection-section {
536
+    display: flex;
537
+    flex-direction: column;
538
+    gap: 10px;
539
+    margin-bottom: 15px;
540
+
541
+    .selection-row {
542
+      display: flex;
543
+      gap: 15px;
544
+
545
+      .selection-item {
546
+        flex: 1;
547
+        display: flex;
548
+        flex-direction: column;
549
+        gap: 8px;
550
+
551
+        .selection-label {
552
+          font-size: 14px;
553
+          color: #70D3EE;
554
+          font-weight: bold;
555
+          text-align: center;
556
+        }
557
+
558
+        :deep(.custom-select) {
559
+          .el-input__wrapper {
560
+            background: #0C4F7A;
561
+            border: none;
562
+            box-shadow: none;
563
+
564
+            .el-input__inner {
565
+              color: #fff;
566
+              font-size: 12px;
567
+            }
568
+          }
569
+        }
570
+      }
571
+    }
572
+  }
573
+
574
+  .radar-chart {
575
+    height: 350px;
576
+    width: 100%;
577
+    display: flex;
578
+    justify-content: center;
579
+    align-items: center;
580
+  }
581
+
582
+  .data-list {
583
+    flex: 1;
584
+    display: flex;
585
+    flex-direction: column;
586
+    gap: 8px;
587
+    overflow-y: auto;
588
+
589
+    .data-item {
590
+
591
+      background: rgba(255, 255, 255, 0.05);
592
+      border-radius: 6px;
593
+      padding: 11px 25px;
594
+      background: url('../../../../../assets/images/cardBg.png') no-repeat !important;
595
+      background-size: 100% 105% !important;
596
+
597
+
598
+      .data-title {
599
+        font-size: 16px;
600
+        font-weight: bold;
601
+        margin-bottom: 10px;
602
+        color: #70D3EE;
603
+      }
604
+
605
+      .data-values {
606
+        display: flex;
607
+        justify-content: space-around;
608
+
609
+        .value-label {
610
+          display: flex;
611
+          flex-direction: column;
612
+          align-items: center;
613
+
614
+          .value {
615
+            font-size: 17px;
616
+            // font-weight: bold;
617
+            color: #70D3EE;
618
+
619
+            margin-bottom: 2px;
620
+          }
621
+
622
+          .label {
623
+            font-size: 13px;
624
+            color: rgba(255, 255, 255, 0.7);
625
+          }
626
+        }
627
+      }
628
+    }
629
+  }
630
+}
631
+
632
+
633
+
634
+
635
+
636
+// /* 响应式设计 */
637
+// @media (max-width: 1280px) {
638
+//   .overview-content {
639
+//     .data-list {
640
+//       flex-wrap: wrap;
641
+
642
+//       .data-item {
643
+//         flex: 0 0 calc(50% - 5px);
644
+//         min-width: 120px;
645
+//       }
646
+//     }
647
+//   }
648
+// }
649
+
650
+// @media (max-width: 768px) {
651
+//   .overview-content {
652
+//     .data-list {
653
+//       .data-item {
654
+//         flex: 0 0 100%;
655
+//       }
656
+//     }
657
+//   }
658
+// }</style>

+ 543 - 0
src/views/dataBigScreen/dashboard/components/pageItems/HomePageStats.vue

@@ -0,0 +1,543 @@
1
+<template>
2
+  <div class="stats-section">
3
+    <div v-for="(item, index) in props.typeDetailData" :key="index" class="stats-item">
4
+      <ChartsContainer :title="item.title">
5
+        <!-- 主要内容区域 -->
6
+        <div class="item-content">
7
+          <!-- 数量统计区域和问题整改区域 -->
8
+          <div class="top-section">
9
+            <!-- 数量统计区域 -->
10
+            <div class="stat-section">
11
+              <div class="stat-header">
12
+                <div class="stat-title">{{ item.statTitle }}</div>
13
+              </div>
14
+
15
+              <div class="data-grid">
16
+                <div v-for="(dataItem, dataIndex) in item.dataItems" :key="dataIndex" class="data-item">
17
+                  <div class="data-value">
18
+                    <img v-if="dataItem.isImage" :src="dataItem.value" class="data-image" />
19
+                    <span v-else :style="{ color: dataItem.color || '#fff' }">{{ dataItem.value }}</span>
20
+                  </div>
21
+                  <div class="data-label">{{ dataItem.label }}</div>
22
+
23
+                  <div v-if="dataIndex === item.dividerIndex" class="divider"></div>
24
+                </div>
25
+              </div>
26
+            </div>
27
+
28
+            <!-- 巡检部分的问题整改区域 -->
29
+            <div v-if="item.title === '巡检'" class="problem-rect-section">
30
+              <div class="problem-title">问题整改</div>
31
+              <div class="problem-grid">
32
+                <!-- 左边:已办结问题 -->
33
+                <div class="problem-item left-item">
34
+                  <div class="problem-value">{{ item.completed }}</div>
35
+                  <div class="problem-label">{{ '已办结问题' }}</div>
36
+                </div>
37
+                <!-- 右边:待办结问题 -->
38
+                <div class="problem-item right-item">
39
+                  <div class="problem-value">{{ item.pending }}</div>
40
+                  <div class="problem-label">{{ '待办结问题' }}</div>
41
+                </div>
42
+              </div>
43
+            </div>
44
+          </div>
45
+
46
+          <!-- 排名区域 -->
47
+          <div class="rank-section" v-if="item.rankList && item.rankList.length > 0">
48
+            <div v-for="(rank, i) in item.rankList" :key="i" class="rank-item">
49
+              <div class="rank-label">{{ rank.label }}</div>
50
+              <div class="rank-progress">
51
+                <!-- <div class="progress-bar1" :style="{ width: rank.percentage + '%' }"></div> -->
52
+                <el-progress :percentage="Number(rank.percentage || 0)" :show-text="false" :stroke-width="10"
53
+                  color="#F9B83A" />
54
+                <div class="rank-info">
55
+                  <span :style="{ color: rank.type === 'station' ? '#ED9E3E' : '#5580F7' }">{{ rank.current }}</span>/{{
56
+                    rank.total }}
57
+                </div>
58
+              </div>
59
+            </div>
60
+          </div>
61
+
62
+          <!-- 大队和班组合格率排行 -->
63
+          <div class="rank-section">
64
+            <!-- 大队排名区域 -->
65
+            <div class="department-rank-container">
66
+              <div class="custom-title">
67
+                <div class="arrow-icon"></div>
68
+                <span>{{ item.title === '查获上报' ? '大队查获总数排名' : item.title == '抽问抽答' ? '大队正确率排名' : '大队合格率排名' }}</span>
69
+              </div>
70
+              <div v-for="(dept, index) in item.departmentRank" :key="'dept-' + index" class="rank-item">
71
+                <div class="rank-label">{{ dept.name }}</div>
72
+                <div class="rank-progress">
73
+                  <el-progress
74
+                    :percentage="item.title !== '查获上报' ? Number(dept.passRate || 0) : getPercentage(dept.passRate, item.departmentRank)"
75
+                    :show-text="false" :stroke-width="10" color="#F9B83A" />
76
+                  <div class="rank-info">
77
+                    <span style="color: #999999">{{ dept.passRate || 0 }}</span>
78
+                  </div>
79
+                </div>
80
+              </div>
81
+            </div>
82
+
83
+            <!-- 班组合格率排行 -->
84
+            <div class="team-rank-section">
85
+              <div class="rank-header">
86
+                <div class="rank-header-title">
87
+                  <div class="arrow-icon"></div>
88
+                  <div class="rank-title">{{ item.title === '查获上报' ? '班组查获总数排名' : item.title == '抽问抽答' ? '班组正确率排名' :
89
+                    '班组合格率排名' }}</div>
90
+                </div>
91
+                <div class="sort-buttons">
92
+                  <div :class="['sort-btn', { 'active': item.teamSortType === 'asc' }]"
93
+                    @click="$emit('sort-change', item.title, 'team', 'asc')">正序</div>
94
+                  <div :class="['sort-btn', { 'active': item.teamSortType === 'desc' }]"
95
+                    @click="$emit('sort-change', item.title, 'team', 'desc')">倒序</div>
96
+                </div>
97
+              </div>
98
+
99
+              <!-- 班组排名显示 -->
100
+              <div class="team-rank-container">
101
+                <template v-if="item.teamSortType === 'asc'">
102
+                  <div v-for="(team, i) in (item.teamRank || []).slice(0, 5)" :key="'team-asc-' + i" class="rank-item">
103
+                    <div class="rank-label">{{ team.name }}</div>
104
+                    <div class="rank-progress">
105
+                      <el-progress
106
+                        :percentage="item.title !== '查获上报' ? Number(team.passRate || 0) : getPercentage(team.passRate, item.teamRank)"
107
+                        :show-text="false" :stroke-width="10" color="#A7C8FF" />
108
+                      <div class="rank-info">
109
+                        <span style="color: #999999">{{ team.passRate || 0 }}</span>
110
+                      </div>
111
+                    </div>
112
+                  </div>
113
+                </template>
114
+                <template v-else>
115
+                  <div v-for="(team, i) in (item.bottomTeamRank || []).slice(0, 5)" :key="'team-desc-' + i"
116
+                    class="rank-item">
117
+                    <div class="rank-label">{{ team.name }}</div>
118
+                    <div class="rank-progress">
119
+                      <el-progress
120
+                        :percentage="item.title !== '查获上报' ? Number(team.passRate || 0) : getPercentage(team.passRate, item.bottomTeamRank)"
121
+                        :show-text="false" :stroke-width="10" color="#A7C8FF" />
122
+                      <div class="rank-info">
123
+                        <span style="color: #999999">{{ team.passRate || 0 }}</span>
124
+                      </div>
125
+                    </div>
126
+                  </div>
127
+                </template>
128
+              </div>
129
+            </div>
130
+
131
+          </div>
132
+        </div>
133
+      </ChartsContainer>
134
+    </div>
135
+  </div>
136
+</template>
137
+
138
+<script setup>
139
+import ChartsContainer from '../ChartsContainer.vue'
140
+import { watch } from 'vue'
141
+import useUserStore from '@/store/modules/user'
142
+const userStore = useUserStore()
143
+const userRole = userStore.roles
144
+// 定义props,从父组件接收数据
145
+const props = defineProps({
146
+  typeDetailData: {
147
+    type: Array,
148
+    default: () => []
149
+  }
150
+})
151
+
152
+// 计算百分比:使用数组中的最大值作为分母
153
+const getPercentage = (value, array) => {
154
+  if (!array || !Array.isArray(array) || array.length === 0) {
155
+    return 0;
156
+  }
157
+
158
+  // 找到数组中的最大值
159
+  const maxValue = Math.max(...array.map(item => Number(item.passRate) || 0));
160
+
161
+  // 如果最大值为0,则返回0避免除以0
162
+  if (maxValue === 0) {
163
+    return 0;
164
+  }
165
+
166
+  // 计算百分比
167
+  const percentage = (Number(value) || 0) / maxValue * 100;
168
+
169
+  // 确保百分比在0-100之间
170
+  return Math.min(Math.max(percentage, 0), 100);
171
+}
172
+
173
+
174
+</script>
175
+
176
+<style lang="scss" scoped>
177
+.stats-section {
178
+  display: grid;
179
+  grid-template-columns: 1fr 1fr 1fr;
180
+  gap: 10px;
181
+  height: 100%;
182
+}
183
+
184
+.stats-item {
185
+  height: 100%;
186
+
187
+  .chartsContainer {
188
+    :deep(.chartsContainer-content) {
189
+      padding: 10px 0px;
190
+    
191
+    }
192
+  }
193
+
194
+}
195
+
196
+.item-content {
197
+  height: 100%;
198
+  display: flex;
199
+  flex-direction: column;
200
+
201
+  .top-section {
202
+    display: flex;
203
+    gap: 0;
204
+    margin-bottom: 15px;
205
+
206
+    .stat-section {
207
+      flex: 1;
208
+      border-radius: 8px;
209
+      padding: 10px 25px;
210
+      background: url('../../../../../assets/images/cardBg.png') no-repeat !important;
211
+      background-size: 100% 105% !important;
212
+    }
213
+
214
+    .problem-rect-section {
215
+      padding: 6px 10px;
216
+      flex: 0.6;
217
+      margin-top: 0;
218
+      background: url('../../../../../assets/images/cardBg.png') no-repeat !important;
219
+      background-size: 100% 105% !important;
220
+    }
221
+  }
222
+
223
+  .stat-header {
224
+    display: flex;
225
+    justify-content: space-between;
226
+    align-items: center;
227
+    margin-bottom: 15px;
228
+  }
229
+
230
+  .stat-title {
231
+    font-size: 16px;
232
+    color: #7DE6FF;
233
+    font-weight: bold;
234
+  }
235
+
236
+  .data-grid {
237
+    display: flex;
238
+    justify-content: space-evenly;
239
+  }
240
+
241
+  .data-item {
242
+    display: flex;
243
+    flex: 1;
244
+    flex-direction: column;
245
+    align-items: center;
246
+    text-align: center;
247
+    position: relative;
248
+  }
249
+
250
+  .data-value {
251
+    font-size: 18px;
252
+    font-weight: bold;
253
+    color: #fff;
254
+    margin-bottom: 5px;
255
+    display: flex;
256
+    align-items: center;
257
+    justify-content: center;
258
+    min-height: 30px;
259
+
260
+    .data-image {
261
+      width: 35px;
262
+      height: 35px;
263
+    }
264
+  }
265
+
266
+  .data-label {
267
+    font-size: 12px;
268
+    color: rgba(255, 255, 255, 0.7);
269
+  }
270
+
271
+  .divider {
272
+    position: absolute;
273
+    right: -4%;
274
+    top: 50%;
275
+    transform: translateY(-50%);
276
+    width: 1px;
277
+    height: 60%;
278
+    background: rgba(255, 255, 255, 0.3);
279
+  }
280
+
281
+  /* 问题整改区域样式 */
282
+  .problem-rect-section {
283
+    margin-top: 15px;
284
+  }
285
+
286
+  .problem-title {
287
+    font-size: 16px;
288
+    color: #7DE6FF;
289
+    font-weight: bold;
290
+    margin-bottom: 10px;
291
+  }
292
+
293
+  .problem-grid {
294
+    display: flex;
295
+    gap: 10px;
296
+  }
297
+
298
+  .problem-item {
299
+    flex: 1;
300
+    border-radius: 8px;
301
+    padding: 5px;
302
+    display: flex;
303
+    flex-direction: column;
304
+    align-items: center;
305
+    text-align: center;
306
+  }
307
+
308
+  // .left-item {
309
+  //   background: rgba(0, 174, 65, 0.2);
310
+  // }
311
+
312
+  // .right-item {
313
+  //   background: rgba(249, 96, 96, 0.2);
314
+  // }
315
+
316
+  .problem-value {
317
+    font-size: 20px;
318
+    font-weight: bold;
319
+    margin-bottom: 5px;
320
+    min-height: 30px;
321
+    display: flex;
322
+    align-items: center;
323
+    justify-content: center;
324
+  }
325
+
326
+  .left-item .problem-value {
327
+    color: #00AE41;
328
+  }
329
+
330
+  .right-item .problem-value {
331
+    color: #F96060;
332
+  }
333
+
334
+  .problem-label {
335
+    font-size: 12px;
336
+    color: rgba(255, 255, 255, 0.7);
337
+  }
338
+
339
+  /* 排名区域样式 */
340
+  .rank-section {
341
+    height: 100%;
342
+    padding: 0 15px;
343
+    // background: rgba(255, 255, 255, 0.05);
344
+    border-radius: 8px;
345
+
346
+  }
347
+
348
+  .custom-title {
349
+    display: flex;
350
+    align-items: center;
351
+    font-size: 15px;
352
+    color: #fff;
353
+    font-weight: bold;
354
+    margin-bottom: 10px;
355
+    margin-top: 15px;
356
+  }
357
+
358
+  .custom-title:first-child {
359
+    margin-top: 0;
360
+  }
361
+
362
+  .arrow-icon {
363
+    width: 0;
364
+    height: 0;
365
+    border-top: 8px solid transparent;
366
+    border-bottom: 8px solid transparent;
367
+    border-left: 10px solid #1CB6FF;
368
+    margin-right: 8px;
369
+  }
370
+
371
+  /* 排名头部样式 */
372
+  .rank-header {
373
+    display: flex;
374
+    justify-content: space-between;
375
+    align-items: center;
376
+    margin-bottom: 10px;
377
+    margin-top: 15px;
378
+
379
+    .rank-header-title {
380
+      display: flex;
381
+
382
+      align-items: center;
383
+
384
+      .rank-title {
385
+        font-size: 15px;
386
+      }
387
+    }
388
+  }
389
+
390
+  .rank-header:first-child {
391
+    margin-top: 0;
392
+  }
393
+
394
+  .rank-title {
395
+    font-size: 14px;
396
+    color: #fff;
397
+    font-weight: bold;
398
+  }
399
+
400
+  .arrow-icon {
401
+    width: 0;
402
+    height: 0;
403
+    border-top: 6px solid transparent;
404
+    border-bottom: 6px solid transparent;
405
+    border-left: 6px solid #35D8FF;
406
+    margin-right: 8px;
407
+  }
408
+
409
+  .sort-buttons {
410
+    display: flex;
411
+
412
+    div:nth-child(1) {
413
+      border-radius: 3px 0 0 3px;
414
+    }
415
+
416
+    div:nth-child(2) {
417
+      border-radius: 0 3px 3px 0;
418
+    }
419
+  }
420
+
421
+  .sort-btn {
422
+    padding: 3px 9px;
423
+    font-size: 13px;
424
+    color: #999;
425
+    background: rgba(255, 255, 255, 0.1);
426
+    border-radius: 3px;
427
+    cursor: pointer;
428
+    transition: all 0.3s;
429
+  }
430
+
431
+  .sort-btn.active {
432
+    color: #fff;
433
+    background: #1CB6FF;
434
+  }
435
+
436
+  .sort-btn:hover {
437
+    background: rgba(28, 182, 255, 0.3);
438
+  }
439
+
440
+  .rank-item {
441
+    display: flex;
442
+    align-items: center;
443
+    margin-bottom: 10px;
444
+    padding: 0 17px;
445
+  }
446
+
447
+  .rank-item:last-child {
448
+    margin-bottom: 0;
449
+  }
450
+
451
+  /* 大队排名容器样式 */
452
+  .department-rank-container {
453
+
454
+    overflow-y: auto;
455
+    padding-right: 5px;
456
+    margin-bottom: 15px;
457
+  }
458
+
459
+  /* 班组合格率排行区域样式 */
460
+  .team-rank-section {
461
+    margin-top: 15px;
462
+  }
463
+
464
+  /* 班组排名容器样式 */
465
+  .team-rank-container {
466
+    // max-height: 200px;
467
+    overflow-y: auto;
468
+    padding-right: 5px;
469
+  }
470
+
471
+  /* 自定义滚动条样式 */
472
+  .department-rank-container::-webkit-scrollbar,
473
+  .team-rank-container::-webkit-scrollbar {
474
+    width: 4px;
475
+  }
476
+
477
+  .department-rank-container::-webkit-scrollbar-track,
478
+  .team-rank-container::-webkit-scrollbar-track {
479
+    background: rgba(255, 255, 255, 0.1);
480
+    border-radius: 2px;
481
+  }
482
+
483
+  .department-rank-container::-webkit-scrollbar-thumb,
484
+  .team-rank-container::-webkit-scrollbar-thumb {
485
+    background: rgba(255, 255, 255, 0.3);
486
+    border-radius: 2px;
487
+  }
488
+
489
+  .department-rank-container::-webkit-scrollbar-thumb:hover,
490
+  .team-rank-container::-webkit-scrollbar-thumb:hover {
491
+    background: rgba(255, 255, 255, 0.5);
492
+  }
493
+
494
+  .rank-label {
495
+    font-size: 14px;
496
+    color: rgba(255, 255, 255, 0.8);
497
+    width: 60px;
498
+    margin-right: 10px;
499
+  }
500
+
501
+  .rank-progress {
502
+    flex: 1;
503
+    display: flex;
504
+    align-items: center;
505
+    position: relative;
506
+    gap: 10px;
507
+  }
508
+
509
+  :deep(.el-progress) {
510
+    flex: 1;
511
+    margin-right: 10px;
512
+  }
513
+
514
+  :deep(.el-progress-bar) {
515
+    padding-right: 0;
516
+  }
517
+
518
+  :deep(.el-progress-bar__outer) {
519
+    border-radius: 5px;
520
+    background-color: transparent !important;
521
+  }
522
+
523
+  :deep(.el-progress-bar__inner) {
524
+    border-radius: 5px;
525
+  }
526
+
527
+  .rank-info {
528
+    font-size: 13px;
529
+    color: #999;
530
+    min-width: 30px;
531
+    text-align: right;
532
+  }
533
+}
534
+
535
+// /* 响应式设计 */
536
+// @media (max-width: 1280px) {
537
+//   .team-rank-container {
538
+//     max-height: 200px;
539
+
540
+//   }
541
+
542
+// }
543
+</style>

+ 347 - 0
src/views/dataBigScreen/dashboard/components/pageItems/HomePageSystemStatus.vue

@@ -0,0 +1,347 @@
1
+<template>
2
+  <div class="system-status">
3
+    <!-- 搜索条件 -->
4
+    <div class="search-conditions">
5
+      <div class="time-range-switch">
6
+        <div class="time-range-label">切换时间范围:</div>
7
+        <div class="time-range-buttons">
8
+          <button v-for="range in timeRanges" :key="range.value" :class="{ active: activeTimeRange === range.value }"
9
+            @click="handleTimeRangeChange(range.value)">
10
+            {{ range.label }}
11
+          </button>
12
+        </div>
13
+      </div>
14
+
15
+      <div class="custom-time-range">
16
+        <div class="custom-time-label">自定义时间范围:</div>
17
+        <div class="date-pickers" :class="{ 'custom-time-range-active': !!activeTimeRange }">
18
+          <el-date-picker class="date-picker" v-model="startDate" type="date" style="width:110px" placeholder="开始时间"
19
+            @change="handleDateChange" />
20
+          <span class="date-separator">-</span>
21
+          <el-date-picker class="date-picker" v-model="endDate" type="date" style="width:110px" placeholder="结束时间"
22
+            @change="handleDateChange" />
23
+        </div>
24
+      </div>
25
+    </div>
26
+
27
+    <!-- 今日上岗科长 -->
28
+    <div class="today-duty-section" v-if="dutySectionMaster">
29
+      <div class="duty-label">今日上岗科长:</div>
30
+      <div class="duty-name">{{ dutySectionMaster }}</div>
31
+    </div>
32
+
33
+    <!-- 遍历数组展示 -->
34
+    <div class="quantity-overview">
35
+      <div class="quantity-overview-item" v-for="item in attendanceItems" :key="item.id">
36
+        <div>{{ item.label }}</div>
37
+        <div style="font-size: 32px;">{{ item.value }}{{ item.unit }}</div>
38
+      </div>
39
+    </div>
40
+  </div>
41
+</template>
42
+
43
+<script setup>
44
+import { ref, reactive, onMounted, computed } from 'vue'
45
+import { getAttendanceStats, getAccuracyStatistics, selectUserListByRoleKey } from '@/api/largeScreen/largeScreen'
46
+import useUserStore from '@/store/modules/user'
47
+
48
+// 定义事件
49
+const emit = defineEmits(['timeRangeChange', 'dateChange'])
50
+
51
+// 时间范围选项
52
+const timeRanges = [
53
+  { label: '近一周', value: 'week' },
54
+  { label: '近一月', value: 'month' },
55
+  { label: '近三月', value: 'quarter' },
56
+  { label: '近半年', value: 'halfYear' },
57
+  { label: '近一年', value: 'year' }
58
+]
59
+
60
+// 内部状态管理
61
+const activeTimeRange = ref('year')
62
+const startDate = ref('')
63
+const endDate = ref('')
64
+
65
+// 出勤相关数据
66
+const attendanceStats = ref({})
67
+const accuracyStatistics = ref({})
68
+const userStore = useUserStore()
69
+
70
+// 今日上岗科长数据
71
+const dutySectionMaster = ref('')
72
+
73
+// 获取今日上岗科长数据
74
+const fetchDutySectionMaster = async () => {
75
+  try {
76
+    // 调用接口获取今日上岗用户列表
77
+    const response = await selectUserListByRoleKey(['kezhang']) // 假设角色标识为'kezhang'
78
+
79
+    const userList = response.data || []
80
+
81
+    // 将数组中的userName拼接成字符串
82
+    if (userList.length > 0) {
83
+      const names = userList.map(user => user.nickName)
84
+      dutySectionMaster.value = names.join('、')
85
+    } else {
86
+      // 如果没有数据,使用默认值
87
+      dutySectionMaster.value = '暂无科长上岗'
88
+    }
89
+  } catch (error) {
90
+    console.error('获取今日上岗科长数据失败:', error)
91
+    // 出错时使用默认值
92
+    dutySectionMaster.value = ''
93
+  }
94
+}
95
+
96
+// 计算属性:用户角色
97
+const role = computed(() => {
98
+  return userStore.roles || []
99
+})
100
+
101
+// 计算属性:当前用户信息
102
+const currentUser = computed(() => {
103
+  return userStore.userInfo || {}
104
+})
105
+
106
+// 计算属性:是否为班组视图
107
+const isTeamView = computed(() => {
108
+  return role.value.includes('banzuzhang') ? false : role.value.includes('banzuzhang')
109
+})
110
+
111
+// 计算属性:是否为个人视图
112
+const isIndividualView = computed(() => {
113
+  return role.value.includes('banzuzhang') ? false : role.value.includes('SecurityCheck')
114
+})
115
+
116
+// 计算属性:出勤信息数组
117
+const attendanceItems = computed(() => {
118
+
119
+  let res = {}
120
+  if (role.value.includes('kezhang') || role.value.includes('test') || role.value.includes('zhijianke') || role.value.includes('admin')) {
121
+    res = attendanceStats.value.stationLeaderStats || {}
122
+    return [
123
+      { id: 1, label: `在岗大队`, value: res?.dutyDeptName, unit: '' },
124
+      { id: 2, label: '在岗班组', value: res?.onDutyTeamCount, unit: '' },
125
+      { id: 3, label: '在岗人员', value: res?.onDutyPersonnelCount, unit: '' },
126
+      { id: 4, label: '今日查获上报', value: res?.todaySeizureReportCount, unit: '' },
127
+      { id: 5, label: '今日巡检问题', value: res?.todayCheckCorrectionCount, unit: '' },
128
+      { id: 6, label: '今日抽问抽答', value: `${accuracyStatistics.value?.todayTaskCompletion?.completedCount || 0}/${accuracyStatistics.value?.todayTaskCompletion?.totalCount || 0}`, unit: '' }
129
+    ]
130
+  } else {
131
+    res = attendanceStats.value.teamLeaderStats || attendanceStats.value.securityCheckStats || {}
132
+
133
+    // 个人视图或非班组长角色
134
+    return [
135
+      { id: 1, label: '出勤班组', value: res?.attendanceTeamName, unit: '' },
136
+      { id: 2, label: '出勤通道', value: res?.attendanceChannel, unit: '' },
137
+      { id: 3, label: '出勤时间', value: res?.attendanceTime || res?.attendanceTimeTips, unit: '' }
138
+    ]
139
+  }
140
+})
141
+
142
+const handleTimeRangeChange = (range) => {
143
+  activeTimeRange.value = range
144
+  emit('timeRangeChange', range)
145
+}
146
+
147
+// 处理日期变化
148
+const handleDateChange = () => {
149
+  if (startDate.value && endDate.value) {
150
+    // 当选择自定义时间时,将activeTimeRange置为空
151
+    activeTimeRange.value = ''
152
+    emit('dateChange', {
153
+      startDate: startDate.value,
154
+      endDate: endDate.value
155
+    })
156
+  }
157
+}
158
+
159
+
160
+
161
+
162
+
163
+// 获取出勤统计数据
164
+const fetchAttendanceStats = async () => {
165
+  try {
166
+    const response = await getAttendanceStats({})
167
+    attendanceStats.value = response.data || {}
168
+  } catch (error) {
169
+    console.error('获取出勤统计数据失败:', error)
170
+  }
171
+}
172
+
173
+// 获取正确率统计数据
174
+const fetchAccuracyStatistics = async () => {
175
+  try {
176
+    const response = await getAccuracyStatistics({})
177
+    accuracyStatistics.value = response.data || {}
178
+  } catch (error) {
179
+    console.error('获取正确率统计数据失败:', error)
180
+  }
181
+}
182
+
183
+// 组件挂载时获取数据
184
+onMounted(() => {
185
+  fetchAttendanceStats()
186
+  fetchAccuracyStatistics()
187
+  fetchDutySectionMaster()
188
+})
189
+</script>
190
+
191
+<style lang="scss" scoped>
192
+.system-status {
193
+  height: 100%;
194
+  display: flex;
195
+  flex-direction: column;
196
+  gap: 10px;
197
+
198
+  .search-conditions {
199
+    justify-content: center;
200
+
201
+    display: flex;
202
+    align-items: center;
203
+    gap: 20px;
204
+    padding: 15px 15px 0 15px;
205
+    // background: rgba(255, 255, 255, 0.05);
206
+    border-radius: 8px;
207
+
208
+    .time-range-switch {
209
+      display: flex;
210
+      align-items: center;
211
+      gap: 10px;
212
+
213
+      .time-range-label {
214
+        font-size: 14px;
215
+        color: #fff;
216
+        white-space: nowrap;
217
+      }
218
+
219
+      .time-range-buttons {
220
+        display: flex;
221
+        gap: 4px;
222
+
223
+        button {
224
+          padding: 3px 9px;
225
+          border: none;
226
+          background: rgba(255, 255, 255, 0.1);
227
+          color: #797979;
228
+          background-color: #0A3551;
229
+          border-radius: 4px;
230
+          cursor: pointer;
231
+          font-size: 12px;
232
+          transition: all 0.3s;
233
+
234
+          // &:hover {
235
+          //   background: rgba(255, 255, 255, 0.2);
236
+          // }
237
+
238
+          &.active {
239
+            background: #0C4F7A;
240
+
241
+            color: #fff;
242
+          }
243
+        }
244
+      }
245
+    }
246
+
247
+    .custom-time-range {
248
+      display: flex;
249
+      align-items: center;
250
+      gap: 10px;
251
+
252
+      .custom-time-label {
253
+        font-size: 14px;
254
+        color: #fff;
255
+        white-space: nowrap;
256
+      }
257
+
258
+      .custom-time-range-active {
259
+        :deep(.el-input__wrapper) {
260
+          background-color: #0A3551 !important;
261
+          border: none;
262
+          box-shadow: none;
263
+
264
+          .el-input__inner {
265
+            color: #797979;
266
+          }
267
+        }
268
+      }
269
+
270
+      .date-pickers {
271
+        display: flex;
272
+        align-items: center;
273
+        gap: 8px;
274
+
275
+     
276
+
277
+        .date-separator {
278
+          color: #fff;
279
+          font-size: 12px;
280
+        }
281
+
282
+        :deep(.el-input__icon) {
283
+          display: none;
284
+        }
285
+
286
+        :deep(.el-input__wrapper) {
287
+          background-color: #0C4F7A;
288
+          border: none;
289
+          box-shadow: none;
290
+
291
+          .el-input__inner {
292
+            // color: #797979;
293
+          }
294
+        }
295
+      }
296
+    }
297
+  }
298
+
299
+  .today-duty-section {
300
+    display: flex;
301
+    align-items: center;
302
+    gap: 10px;
303
+    padding: 10px 15px;
304
+    background: rgba(255, 255, 255, 0.05);
305
+    border: 1px solid #044878;
306
+    border-radius: 8px;
307
+    color: #fff;
308
+    font-size: 16px;
309
+
310
+    .duty-label {
311
+      font-weight: bold;
312
+      color: #35D8FF;
313
+    }
314
+
315
+    .duty-name {
316
+      color: #70D3EE;
317
+      font-weight: bold;
318
+    }
319
+  }
320
+
321
+  .quantity-overview {
322
+    height: 100%;
323
+    width: 100%;
324
+    display: flex;
325
+    flex-wrap: wrap;
326
+    column-gap: 25px;
327
+    row-gap: 15px;
328
+    // padding: 20px 20px 10px;
329
+    box-sizing: border-box;
330
+    color: #fff;
331
+    font-size: 20px;
332
+
333
+    .quantity-overview-item {
334
+      flex: 0 0 calc(33.333% - 16.67px);
335
+      // min-height:100px;
336
+      background: #051E40;
337
+      display: flex;
338
+      flex-direction: column;
339
+      align-items: center;
340
+      justify-content: center;
341
+      row-gap: 10px;
342
+      border-radius: 5px;
343
+      font-weight: bold;
344
+    }
345
+  }
346
+}
347
+</style>

+ 149 - 0
src/views/dataBigScreen/dashboard/components/pageItems/HomePageWarning.vue

@@ -0,0 +1,149 @@
1
+<template>
2
+  <ChartsContainer title="紧急通知" class="home-page-warning">
3
+    <!-- 紧急通知 -->
4
+    <div class="urgent-notice" v-if="noticeList.length > 0">
5
+    
6
+  
7
+      <div class="notice-carousel">
8
+        <transition name="slide-up-down" mode="out-in">
9
+          <div :key="currentNotice && currentNotice.noticeId" class="notice-content">
10
+            {{ currentNotice && currentNotice.noticeTitle ? currentNotice.noticeTitle : '暂无通知' }}
11
+          </div>
12
+        </transition>
13
+      </div>
14
+    </div>
15
+  </ChartsContainer>
16
+</template>
17
+
18
+<script setup>
19
+import ChartsContainer from '../ChartsContainer.vue'
20
+import { ref, onMounted, onUnmounted } from 'vue'
21
+import { listNotice } from '@/api/system/notice'
22
+
23
+// 紧急通知数据
24
+const noticeList = ref([])
25
+const currentIndex = ref(0)
26
+const currentNotice = ref(null)
27
+const timer = ref(null)
28
+
29
+// 获取公告数据
30
+const fetchNoticeData = async () => {
31
+  try {
32
+    const response = await listNotice({
33
+      status: '0',
34
+      noticeType: 1,
35
+      pageSize: 999
36
+    })
37
+
38
+    // 假设response.rows包含公告列表
39
+    noticeList.value = response.rows || []
40
+    if (noticeList.value.length > 0) {
41
+      currentNotice.value = noticeList.value[0]
42
+      if (timer.value) {
43
+        clearInterval(timer.value)
44
+      }
45
+      // 数据加载完成后启动轮播
46
+      startCarousel()
47
+    }
48
+  } catch (error) {
49
+    console.error('获取公告数据失败:', error)
50
+  }
51
+}
52
+
53
+// 开始轮播
54
+const startCarousel = () => {
55
+  if (noticeList.value.length <= 1) return
56
+
57
+  timer.value = setInterval(() => {
58
+    currentIndex.value = (currentIndex.value + 1) % noticeList.value.length
59
+    currentNotice.value = noticeList.value[currentIndex.value]
60
+  }, 3000) // 3秒轮播一次
61
+}
62
+
63
+// 组件挂载时获取数据
64
+onMounted(() => {
65
+  fetchNoticeData()
66
+})
67
+
68
+// 组件卸载时清理定时器
69
+onUnmounted(() => {
70
+  if (timer.value) {
71
+    clearInterval(timer.value)
72
+  }
73
+})
74
+</script>
75
+
76
+<style lang="scss" scoped>
77
+.home-page-warning {
78
+  width: 80%; /* 缩小宽度到80% */
79
+  max-width: 400px; /* 设置最大宽度 */
80
+  
81
+  :deep(.chartsContainer) {
82
+    width: 100%;
83
+    height: 100%;
84
+    display: flex;
85
+    align-items: center;
86
+    justify-content: center;
87
+  }
88
+
89
+  .urgent-notice {
90
+    background: rgba(255, 255, 255, 0.05);
91
+    border: 2px solid #044878;
92
+    border-radius: 8px;
93
+    padding: 10px;
94
+    display: flex;
95
+    align-items: center;
96
+    gap: 15px;
97
+    width: 100%;
98
+
99
+    .notice-title {
100
+      font-size: 16px;
101
+      font-weight: bold;
102
+      color: #35D8FF;
103
+      white-space: nowrap;
104
+    }
105
+
106
+    .divider {
107
+      width: 1px;
108
+      height: 20px;
109
+      background: #35D8FF;
110
+    }
111
+
112
+    .notice-carousel {
113
+      flex: 1;
114
+      overflow: hidden;
115
+      height: 24px;
116
+      line-height: 24px;
117
+      position: relative;
118
+
119
+      .notice-content {
120
+        position: absolute;
121
+        top: 0;
122
+        left: 0;
123
+        width: 100%;
124
+        white-space: nowrap;
125
+        overflow: hidden;
126
+        text-overflow: ellipsis;
127
+        font-size: 14px;
128
+        color: #35D8FF;
129
+      }
130
+
131
+      // 上下滚动动画
132
+      .slide-up-down-enter-active,
133
+      .slide-up-down-leave-active {
134
+        transition: all 0.5s ease;
135
+      }
136
+
137
+      .slide-up-down-enter-from {
138
+        transform: translateY(24px);
139
+        opacity: 0;
140
+      }
141
+
142
+      .slide-up-down-leave-to {
143
+        transform: translateY(-24px);
144
+        opacity: 0;
145
+      }
146
+    }
147
+  }
148
+}
149
+</style>

+ 270 - 0
src/views/dataBigScreen/dashboard/components/pageItems/InspectionTask.vue

@@ -0,0 +1,270 @@
1
+<template>
2
+  <div class="chartsContainer">
3
+    <div class="chartsContainer-title">巡检计划</div>
4
+    <div class="chartsContainer-list">
5
+      <div class="chartsContainer-content">
6
+        <div class="chartsContainer-content-top">
7
+          <div class="chartsContainer-content-name">计划安排总览</div>
8
+        </div>
9
+        <div class="chartsContainer-content-content" ref="lineChart" />
10
+      </div>
11
+
12
+      <div class="chartsContainer-content">
13
+        <div class="chartsContainer-content-top">
14
+          <div class="chartsContainer-content-name">日常任务检查指标累计分布</div>
15
+        </div>
16
+        <div class="chartsContainer-content-content" ref="radar" />
17
+      </div>
18
+
19
+      <div class="chartsContainer-content">
20
+        <div class="chartsContainer-content-top">
21
+          <div class="chartsContainer-content-name">任务明细统计</div>
22
+        </div>
23
+        <div class="chartsContainer-content-content">
24
+          <el-table v-auto-scroll :data="tableData" border style="width: 100%" height="100%">
25
+            <el-table-column prop="checkStartTime" label="开始时间" min-width="120" />
26
+            <el-table-column prop="taskName" label="任务名称" />
27
+            <el-table-column prop="checkedLevelDesc" label="检查级别" min-width="80" />
28
+            <el-table-column prop="ruleTypeDesc" label="执行频率" min-width="100">
29
+              <template #default=" scope ">
30
+                {{ scope.row.ruleTypeDesc }}{{ scope.row.ruleTypeNum || 0 }}次
31
+              </template>
32
+            </el-table-column>
33
+            <el-table-column prop="checkEndTime" label="结束时间" min-width="120" />
34
+          </el-table>
35
+        </div>
36
+      </div>
37
+    </div>
38
+  </div>
39
+</template>
40
+
41
+<script setup>
42
+import { planOverview, planDistribution, planStatistics } from '@/api/item/items'
43
+import { useTimeOut } from './useTimeOut';
44
+import { useECharts } from '@/hooks/useEcharts.js';
45
+import { ref, computed, reactive, inject } from 'vue'
46
+const provideParams = inject('provideParams')
47
+const lineChart = ref(null)
48
+const lineChartData = ref({})
49
+const setChartsOptions = computed(() => {
50
+  return {
51
+    tooltip: {
52
+      trigger: 'axis'
53
+    },
54
+    xAxis: {
55
+      type: 'category',
56
+      boundaryGap: false,
57
+      data: Object.values(lineChartData.value)[0] ? Object.values(lineChartData.value)[0].map(item => item.date) : [],
58
+      axisLabel: {
59
+        color: '#fff',
60
+        rotate: 25,
61
+        fontSize: 12,
62
+      }
63
+    },
64
+    legend: {
65
+      data: Object.keys(lineChartData.value),
66
+      textStyle: {
67
+        color: '#fff'
68
+      }
69
+    },
70
+    yAxis: {
71
+      type: 'value',
72
+      axisLabel: {
73
+        color: '#fff',
74
+      }
75
+    },
76
+    series: Object.entries(lineChartData.value).map(([ key, vlaue ]) => ({
77
+      data: vlaue.map(item => item.total),
78
+      type: 'line',
79
+      smooth: true,
80
+      name: key
81
+    })),
82
+    grid: {
83
+      left: '5%',
84
+      right: '0%',
85
+      bottom: '10%',
86
+      top: '15%',
87
+      containLabel: true
88
+    }
89
+  };
90
+})
91
+useECharts(lineChart, setChartsOptions);
92
+
93
+const radar = ref(null)
94
+const radarData = reactive({
95
+  grounp: [],
96
+  data: [],
97
+  legend: []
98
+})
99
+const setRadarOptions = computed(() => {
100
+  return {
101
+    tooltip: {
102
+      trigger: 'item'
103
+    },
104
+    grid: {
105
+      top: '5%',
106
+      bottom: '5%',
107
+      left: '5%',
108
+      right: '5%'
109
+    },
110
+    radar: {
111
+      indicator: radarData.grounp,
112
+      axisName: {
113
+        color: '#fff'
114
+      },
115
+      radius: '60%',
116
+    },
117
+    series: [
118
+      {
119
+        type: 'radar',
120
+        data: radarData.data,
121
+        color: '#408CFF'
122
+      }
123
+    ]
124
+  }
125
+})
126
+useECharts(radar, setRadarOptions);
127
+
128
+const permutationRadarDataHandler = (result = []) => {
129
+  radarData.grounp = result.map(item => ({ text: item.name + ' ' + item.total }))
130
+  radarData.data = [{
131
+    name: '日常任务检查',
132
+    value: result.map(attr => attr.total),
133
+    // // 为每个数据系列设置不同的颜色
134
+    areaStyle: {
135
+      opacity: 0.5
136
+    },
137
+    lineStyle: {
138
+      width: 2
139
+    },
140
+    itemStyle: {
141
+      borderWidth: 2
142
+    }
143
+  }]
144
+}
145
+const tableData = ref([])
146
+useTimeOut(() => {
147
+  provideParams.value.deptId && planOverview({
148
+    startDate: provideParams.value.startDate,
149
+    endDate: provideParams.value.endDate,
150
+    deptId: provideParams.value.deptId === 'ALL' ? '' : provideParams.value.deptId
151
+  }).then(res => {
152
+    lineChartData.value = (res.data || []).reduce((cur, acc) => {
153
+      if (cur[ acc.typeDesc ]) {
154
+        cur[ acc.typeDesc ].push(acc)
155
+      } else {
156
+        cur[ acc.typeDesc ] = [ acc ]
157
+      }
158
+      return cur
159
+    }, {})
160
+  })
161
+  provideParams.value.deptId && planDistribution({
162
+    startDate: provideParams.value.startDate,
163
+    endDate: provideParams.value.endDate,
164
+    deptId: provideParams.value.deptId === 'ALL' ? '' : provideParams.value.deptId
165
+  }).then(res => {
166
+    permutationRadarDataHandler(res.data)
167
+  })
168
+  provideParams.value.deptId && planStatistics({
169
+    startDate: provideParams.value.startDate,
170
+    endDate: provideParams.value.endDate,
171
+    deptId: provideParams.value.deptId === 'ALL' ? '' : provideParams.value.deptId
172
+  }).then(res => {
173
+    tableData.value = res.data
174
+  })
175
+}, [provideParams])
176
+
177
+
178
+
179
+</script>
180
+
181
+<style lang="scss" scoped>
182
+.chartsContainer {
183
+  width: 100%;
184
+  height: 100%;
185
+  position: relative;
186
+  background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
187
+  border-radius: 4px;
188
+  overflow: hidden;
189
+  display: flex;
190
+  flex-direction: column;
191
+
192
+  .chartsContainer-title {
193
+    height: 42px;
194
+    width: 100%;
195
+    background: linear-gradient(to right, #004387 0%, #090B18 100%);
196
+    border-left: 1px solid #1892CE;
197
+    line-height: 42px;
198
+    color: #fff;
199
+    font-weight: 900;
200
+    font-size: 20px;
201
+    text-indent: 1em;
202
+  }
203
+
204
+  .chartsContainer-list {
205
+    padding: 20px 15px 10px;
206
+    box-sizing: border-box;
207
+    display: flex;
208
+    flex-direction: column;
209
+    row-gap: 10px;
210
+    flex: 1;
211
+    overflow: hidden;
212
+  }
213
+
214
+  .chartsContainer-content {
215
+    display: flex;
216
+    flex-direction: column;
217
+    row-gap: 10px;
218
+    flex: 1;
219
+    overflow: hidden;
220
+
221
+    .chartsContainer-content-top {
222
+      display: flex;
223
+      align-items: center;
224
+      color: #78DEF5;
225
+
226
+      .chartsContainer-content-name {
227
+        color: #fff;
228
+        font-weight: bold;
229
+        font-size: 16px;
230
+        display: flex;
231
+        align-items: center;
232
+
233
+        &::before {
234
+          content: '';
235
+          display: inline-block;
236
+          border-left: 9px solid #1CB6FF;
237
+          border-top: 8px solid transparent;
238
+          border-right: 9px solid transparent;
239
+          border-bottom: 8px solid transparent;
240
+        }
241
+      }
242
+    }
243
+
244
+    .chartsContainer-content-describe {
245
+      color: #78DEF5;
246
+      font-size: 12px;
247
+      font-weight: bold;
248
+      display: flex;
249
+      align-items: center;
250
+    }
251
+
252
+    .chartsContainer-content-content {
253
+      flex: 1;
254
+      width: 100%;
255
+      --el-bg-color: transparent;
256
+      --el-fill-color-blank: transparent;
257
+      --el-border-color-lighter: #1887A6;
258
+      --el-text-color-regular: #fff;
259
+      --el-fill-color-light: #5e93a1;
260
+
261
+      :deep(.el-table) {
262
+        .el-table__header-wrapper th {
263
+          background-color: #1CA7C8 !important;
264
+          color: #fff;
265
+        }
266
+      }
267
+    }
268
+  }
269
+}
270
+</style>

+ 304 - 0
src/views/dataBigScreen/dashboard/components/pageItems/IssueRectification.vue

@@ -0,0 +1,304 @@
1
+<template>
2
+  <ChartsContainer title="问题整改">
3
+    <div class="content">
4
+      <div :class="type === 'station' ? 'content-left' : 'content-content'">
5
+        <div class="content-left-item" v-for=" item of correctionData " :key="item.title"
6
+          :style="`--color:${item.color}`">
7
+          <div class="content-left-item-title">{{ item.label }}</div>
8
+          <div class="content-left-item-number">{{ item.value }}</div>
9
+        </div>
10
+      </div>
11
+      <div v-if=" type === 'station' " class="content-right" ref="categoryStack">
12
+
13
+      </div>
14
+    </div>
15
+  </ChartsContainer>
16
+</template>
17
+
18
+<script setup>
19
+import ChartsContainer from '../ChartsContainer.vue';
20
+import { useTimeOut } from './useTimeOut';
21
+import { useECharts } from '@/hooks/useEcharts.js';
22
+import { ref, computed, reactive, inject } from 'vue'
23
+import { correction, correctionDistribution } from '@/api/item/items'
24
+const props = defineProps({
25
+  type: {
26
+    type: String,
27
+    default: 'station'
28
+  }
29
+})
30
+const provideParams = inject('provideParams')
31
+const correctionData = ref([])
32
+const categoryStack = ref(null)
33
+const categoryStackData = reactive({
34
+  yAxis: [],
35
+  onTimeCompletedCount: [],
36
+  overTimeCompletedCount: [],
37
+  onTimeUnfinishedCount: [],
38
+  overTimeUnfinishedCount: []
39
+})
40
+const setCategoryStackOptions = computed(() => {
41
+  return {
42
+    tooltip: {
43
+      trigger: 'axis',
44
+      axisPointer: {
45
+        type: 'shadow'
46
+      },
47
+      formatter: function (params) {
48
+        return params[ 0 ].name + '<br/>' +
49
+          params[ 0 ].marker + params[ 0 ].seriesName + ': ' + (params[ 0 ].value || 0) + '<br/>' +
50
+          params[ 1 ].marker + params[ 1 ].seriesName + ': ' + (params[ 1 ].value || 0) + '<br/>' +
51
+          params[ 2 ].marker + params[ 2 ].seriesName + ': ' + (params[ 2 ].value || 0) + '<br/>' +
52
+          params[ 3 ].marker + params[ 3 ].seriesName + ': ' + (params[ 3 ].value || 0)
53
+      }
54
+    },
55
+    grid: {
56
+      left: '15%',
57
+      right: '10%',
58
+      bottom: '10%',
59
+      top: '5%',
60
+    },
61
+    xAxis: {
62
+      type: 'value',
63
+      show: true,
64
+      axisLabel: {
65
+        color: '#fff'
66
+      },
67
+      axisLine: {
68
+        show: true,
69
+      },
70
+      minInterval: 1,
71
+    },
72
+    yAxis: {
73
+      type: 'category',
74
+      data: categoryStackData.yAxis,
75
+      axisLabel: {
76
+        color: '#fff'
77
+      }
78
+    },
79
+    series: [
80
+      {
81
+        name: '按期已完成',
82
+        type: 'bar',
83
+        stack: 'total',
84
+        label: {
85
+          show: true
86
+        },
87
+        color: '#408CFF',
88
+        emphasis: {
89
+          focus: 'series'
90
+        },
91
+        data: categoryStackData.onTimeCompletedCount.map(item => item || undefined)
92
+      },
93
+      {
94
+        name: '超期已完成',
95
+        type: 'bar',
96
+        stack: 'total',
97
+        label: {
98
+          show: true
99
+        },
100
+        color: '#58A55C',
101
+        emphasis: {
102
+          focus: 'series'
103
+        },
104
+        data: categoryStackData.overTimeCompletedCount.map(item => item || undefined)
105
+      },
106
+      {
107
+        name: '按期整改中',
108
+        type: 'bar',
109
+        stack: 'total',
110
+        label: {
111
+          show: true
112
+        },
113
+        color: '#FFC061',
114
+        emphasis: {
115
+          focus: 'series'
116
+        },
117
+        data: categoryStackData.onTimeUnfinishedCount.map(item => item || undefined)
118
+      },
119
+      {
120
+        name: '超期整改中',
121
+        type: 'bar',
122
+        stack: 'total',
123
+        label: {
124
+          show: true
125
+        },
126
+        color: '#DD4D43',
127
+        emphasis: {
128
+          focus: 'series'
129
+        },
130
+        data: categoryStackData.overTimeUnfinishedCount.map(item => item || undefined)
131
+      },
132
+    ]
133
+  }
134
+})
135
+
136
+props.type === 'station' && useECharts(categoryStack, setCategoryStackOptions)
137
+
138
+useTimeOut(() => {
139
+  provideParams.value.checkedDepartmentId && correction({
140
+    startDate: provideParams.value.startDate,
141
+    endDate: provideParams.value.endDate,
142
+    checkedDepartmentId: provideParams.value.checkedDepartmentId === 'ALL' ? '' : provideParams.value.checkedDepartmentId
143
+  }).then(res => {
144
+    const data = res.data || {}
145
+    correctionData.value = [
146
+      {
147
+        label: '按期已完成',
148
+        value: data.onTimeCompletedCount || '0',
149
+        color: '#408CFF'
150
+      },
151
+      {
152
+        label: '超期已完成',
153
+        value: data.overTimeCompletedCount || '0',
154
+        color: '#58A55C'
155
+      },
156
+      {
157
+        label: '按期整改中',
158
+        value: data.onTimeUnfinishedCount || '0',
159
+        color: '#FFC061'
160
+      },
161
+      {
162
+        label: '超期整改中',
163
+        value: data.overTimeUnfinishedCount || '0',
164
+        color: '#DD4D43'
165
+      }
166
+    ]
167
+  })
168
+  props.type === 'station' && correctionDistribution({
169
+    startDate: provideParams.value.startDate,
170
+    endDate: provideParams.value.endDate,
171
+    checkedDepartmentId: provideParams.value.checkedDepartmentId === 'ALL' ? '' : provideParams.value.checkedDepartmentId
172
+  }).then(res => {
173
+    const yAxis = [];
174
+    const onTimeCompletedCount = [];
175
+    const overTimeCompletedCount = [];
176
+    const onTimeUnfinishedCount = [];
177
+    const overTimeUnfinishedCount = [];
178
+    (res.data || []).forEach(item => {
179
+      yAxis.push(item.deptName)
180
+      onTimeCompletedCount.push(item.onTimeCompletedCount)
181
+      overTimeCompletedCount.push(item.overTimeCompletedCount)
182
+      onTimeUnfinishedCount.push(item.onTimeUnfinishedCount)
183
+      overTimeUnfinishedCount.push(item.overTimeUnfinishedCount)
184
+    });
185
+    categoryStackData.yAxis = yAxis
186
+    categoryStackData.onTimeCompletedCount = onTimeCompletedCount
187
+    categoryStackData.overTimeCompletedCount = overTimeCompletedCount
188
+    categoryStackData.onTimeUnfinishedCount = onTimeUnfinishedCount
189
+    categoryStackData.overTimeUnfinishedCount = overTimeUnfinishedCount
190
+  })
191
+}, [ provideParams ])
192
+
193
+</script>
194
+
195
+<style lang="scss" scoped>
196
+.content {
197
+  width: 100%;
198
+  height: 100%;
199
+  overflow: hidden;
200
+  display: flex;
201
+
202
+  .content-left {
203
+    display: flex;
204
+    flex-direction: column;
205
+    row-gap: 10px;
206
+
207
+    .content-left-item {
208
+      display: flex;
209
+      flex: 1;
210
+      flex-direction: column;
211
+      justify-content: space-evenly;
212
+      color: #fff;
213
+      background: #051E40;
214
+      border-radius: 8px;
215
+      width: 140px;
216
+      background: url('../../../../../assets/images/cardBg.png') no-repeat !important;
217
+      background-size: 100% 105% !important;
218
+
219
+      .content-left-item-title {
220
+        font-size: 14px;
221
+        display: flex;
222
+        font-weight: bold;
223
+        align-items: center;
224
+        justify-content: center;
225
+
226
+        &::before {
227
+          content: '';
228
+          width: 12px;
229
+          height: 12px;
230
+          display: block;
231
+          border-radius: 50%;
232
+          margin-right: 10px;
233
+          background: var(--color);
234
+        }
235
+      }
236
+
237
+      .content-left-item-number {
238
+        font-weight: bold;
239
+        font-size: 24px;
240
+        width: 100%;
241
+        text-align: center;
242
+        color: var(--color);
243
+      }
244
+    }
245
+  }
246
+
247
+  .content-content {
248
+    display: flex;
249
+    column-gap: 15px;
250
+    flex-wrap: wrap;
251
+    width: 100%;
252
+    height: 100%;
253
+    overflow: hidden;
254
+    justify-content: center;
255
+    align-items: center;
256
+
257
+
258
+    .content-left-item {
259
+      display: flex;
260
+      width: calc(35%);
261
+      height: calc(30%);
262
+      flex-direction: column;
263
+      justify-content: space-evenly;
264
+      color: #fff;
265
+      border-radius: 8px;
266
+      padding: 10px 0;
267
+      background: url('../../../../../assets/images/cardBg.png') no-repeat !important;
268
+      background-size: 100% 105% !important;
269
+      overflow: hidden;
270
+
271
+      .content-left-item-title {
272
+        font-size: 14px;
273
+        display: flex;
274
+        font-weight: bold;
275
+        align-items: center;
276
+        justify-content: center;
277
+
278
+        &::before {
279
+          content: '';
280
+          width: 12px;
281
+          height: 12px;
282
+          display: block;
283
+          border-radius: 50%;
284
+          margin-right: 10px;
285
+          background: var(--color);
286
+        }
287
+      }
288
+
289
+      .content-left-item-number {
290
+        font-weight: bold;
291
+        font-size: 24px;
292
+        width: 100%;
293
+        text-align: center;
294
+        color: var(--color);
295
+      }
296
+    }
297
+  }
298
+
299
+  .content-right {
300
+    flex: 1;
301
+    height: 100%;
302
+  }
303
+}
304
+</style>

+ 101 - 0
src/views/dataBigScreen/dashboard/components/pageItems/LearningGrowth.vue

@@ -0,0 +1,101 @@
1
+<template>
2
+  <ChartsContainer title="学习成长">
3
+    <div ref="content" style="height: 100%;">
4
+      <div class="content-row" v-if=" type ">
5
+        <div class="content-row-title">{{ rowTableItem.name }}</div>
6
+        <div class="content-row-list">
7
+          <div class="content-row-item">
8
+            <div>平均分</div>
9
+            <div>{{ rowTableItem.averageScore }}</div>
10
+          </div>
11
+          <div class="content-row-item">
12
+            <div>最高分</div>
13
+            <div>{{ rowTableItem.highestScore }}</div>
14
+          </div>
15
+          <div class="content-row-item">
16
+            <div>最低分</div>
17
+            <div>{{ rowTableItem.lowestScore }}</div>
18
+          </div>
19
+        </div>
20
+      </div>
21
+      <el-table v-auto-scroll v-else :data="tableData" border style="width: 100%" height="100%">
22
+        <el-table-column prop="name" label="部门" />
23
+        <el-table-column prop="averageScore" label="平均分" min-width="80" />
24
+        <el-table-column prop="highestScore" label="最高分" min-width="80" />
25
+        <el-table-column prop="lowestScore" label="最低分" min-width="80" />
26
+      </el-table>
27
+    </div>
28
+  </ChartsContainer>
29
+</template>
30
+
31
+<script setup>
32
+import ChartsContainer from '../ChartsContainer.vue';
33
+import { ref } from 'vue';
34
+import { getGrowthPortrait } from '@/api/item/items'
35
+import { useTimeOut } from '../pageItems/useTimeOut'
36
+const props = defineProps({
37
+  type: {
38
+    type: String,
39
+    default: '' // team, department, station
40
+  }
41
+})
42
+const tableData = ref([])
43
+const rowTableItem = ref({})
44
+const params = inject('provideParams')
45
+useTimeOut(() => {
46
+  let apiParams = {}
47
+  if (props.type === 'team') {
48
+    apiParams = { teamId: params.value.deptId }
49
+  } else if (props.type === 'department') {
50
+    apiParams = { departmentId: params.value.deptId }
51
+  } else if (props.type === 'personal') {
52
+    apiParams = { userId: params.value.deptId }
53
+  } else {
54
+    apiParams = { deptId: params.value.deptId }
55
+  }
56
+  params.value.deptId && getGrowthPortrait(apiParams).then(res => {
57
+    tableData.value = res.data
58
+    rowTableItem.value = (res.data || []).find(item => item.id == params.value.deptId) || {}
59
+  })
60
+}, [ params ])
61
+
62
+
63
+</script>
64
+
65
+<style lang="scss" scoped>
66
+.content-row {
67
+  display: flex;
68
+  flex-direction: column;
69
+  height: 100%;
70
+
71
+  .content-row-title {
72
+    font-size: 14px;
73
+    color: #77DBF4;
74
+    height: 30px;
75
+  }
76
+
77
+  .content-row-list {
78
+    display: flex;
79
+    flex: 1;
80
+    flex-direction: column;
81
+    justify-content: space-evenly;
82
+    row-gap: 15px;
83
+  }
84
+
85
+  .content-row-item {
86
+    flex: 1;
87
+    background: url('../../../../../assets/images/rowBg.png') no-repeat;
88
+    background-position: -10px;
89
+    background-size: calc(100% + 20px) 100%;
90
+    overflow: hidden;
91
+    display: flex;
92
+    justify-content: space-between;
93
+    align-items: center;
94
+    color: #fff;
95
+    font-size: 16px;
96
+    border-radius: 5px;
97
+    padding: 0 15px;
98
+    font-weight: bold;
99
+  }
100
+}
101
+</style>

+ 44 - 0
src/views/dataBigScreen/dashboard/components/pageItems/PersonalTaskCompletionRank.vue

@@ -0,0 +1,44 @@
1
+<template>
2
+  <ChartsContainer  name="个人任务完成排行(Top10)">
3
+    <div ref="content" style="height: 100%;">
4
+      <el-table v-auto-scroll :data="tableData" border style="width: 100%" height="100%">
5
+        <el-table-column prop="date" label="排行" width="55" align="center">
6
+          <template #default="scope">
7
+            {{ scope.$index + 1 }}
8
+          </template>
9
+        </el-table-column>
10
+        <el-table-column prop="userName" label="姓名" width="90"/>
11
+        <el-table-column prop="deptName" label="部门" width="90" />
12
+        <el-table-column prop="completedTasks" label="完成任务数" width="90" />
13
+        <el-table-column prop="avgScore" label="平均得分" width="90" />
14
+        <el-table-column prop="completionRate" label="完成率">
15
+          <template #default="scope">
16
+            <el-progress
17
+              :text-inside="true"
18
+              :stroke-width="16"
19
+              :percentage="scope.row.completionRate"
20
+              style="--el-border-color-lighter: #fff"
21
+              color="#58A55C"
22
+            />
23
+          </template>
24
+        </el-table-column>
25
+      </el-table>
26
+    </div>
27
+  </ChartsContainer>
28
+</template>
29
+
30
+<script setup>
31
+import { onMounted } from 'vue';
32
+import ChartsContainer from '../ChartsContainer.vue';
33
+import { userRanking } from '@/api/item/items'
34
+import { useTimeOut } from './useTimeOut'
35
+const tableData = ref([])
36
+useTimeOut(() => {
37
+  userRanking().then(res => {
38
+    tableData.value = res.data
39
+  })
40
+})
41
+
42
+</script>
43
+
44
+<style lang="scss" scoped></style>

+ 170 - 0
src/views/dataBigScreen/dashboard/components/pageItems/PersonnelOnDutySituation.vue

@@ -0,0 +1,170 @@
1
+<template>
2
+  <ChartsContainer title="人员在岗情况">
3
+    <div ref="pie" style="height: 60%;"></div>
4
+    <div ref="bar" style="height: 40%;"></div>
5
+  </ChartsContainer>
6
+</template>
7
+
8
+<script setup>
9
+import { computed } from 'vue';
10
+import ChartsContainer from '../ChartsContainer.vue';
11
+import { useECharts } from '@/hooks/useEcharts.js';
12
+
13
+const pie = ref(null)
14
+const bar = ref(null)
15
+const props = defineProps({
16
+  personnelCountInfo: {
17
+    type: Object,
18
+    default: () => ({
19
+      allData: 0, // 全部通道
20
+      openRate: 0, // 开放率
21
+      openData: 0,  // 开放通道数
22
+      closeData: 0, // 未开放通道数
23
+      barGrounp: [ 'T2航站楼', 'T1航站楼' ],
24
+      barOpenData: [ 0, 0 ],
25
+      barCloseData: [ -0, -0 ],
26
+      hasValidData: true
27
+    })
28
+  }
29
+})
30
+
31
+
32
+
33
+const setChartsOptions = computed(() => {
34
+  return {
35
+    graphic: [ {
36
+      type: 'text',
37
+      left: '52%',
38
+      top: '40%',
39
+      style: {
40
+        text: '在岗率',
41
+        font: 'bold 20px sans-serif',
42
+        fill: '#fff'
43
+      }
44
+    }, {
45
+      type: 'text',
46
+      left: '54%',
47
+      top: '50%',
48
+      style: {
49
+        text: (props.personnelCountInfo.openRate || 0) + '%',
50
+        font: 'bold 22px sans-serif',
51
+        fill: '#fff'
52
+      }
53
+    } ],
54
+    legend: {
55
+      data: [ "在岗", "空闲" ],
56
+      left: "left",
57
+      top: "center",
58
+      orient: "vertical",
59
+      textStyle: { color: "#fff" },
60
+    },
61
+    tooltip: {
62
+      trigger: 'item'
63
+    },
64
+    series: [ {
65
+      type: "pie",
66
+      radius: [ "50%", "70%" ],
67
+      center: [ "60%", "50%" ],
68
+      label: {
69
+        formatter: '{b}\n{c}',
70
+        color: '#fff',
71
+        fontSize: 14,
72
+      },
73
+      labelLine: {
74
+        length: 3
75
+      },
76
+      data: [
77
+        { value: props.personnelCountInfo.openData, name: "在岗", itemStyle: { color: "#408CFF" } },
78
+        { value: props.personnelCountInfo.closeData, name: "空闲", itemStyle: { color: "#A7C8FF" } }
79
+      ]
80
+    } ],
81
+    animation: false,
82
+    grid: {
83
+      top: 10,
84
+      bottom: 50,
85
+      left: 20,
86
+      right: 20
87
+    },
88
+  }
89
+})
90
+const setChartsOptionsBar = computed(() => {
91
+  return {
92
+    grid: {
93
+      top: 20,
94
+      bottom: 20,
95
+      left: 80,
96
+      right: 20
97
+    },
98
+    legend: { show: false },
99
+    xAxis: {
100
+      type: 'value',
101
+      position: 'bottom',
102
+      axisLine: { show: true, lineStyle: { color: '#fff' } },
103
+      axisTick: { show: false },
104
+      axisLabel: { show: false },
105
+      splitLine: { show: false },
106
+      min: -props.personnelCountInfo.allData || -10,
107
+      max: props.personnelCountInfo.allData || 10
108
+    },
109
+    yAxis: {
110
+      type: 'category',
111
+      data: props.personnelCountInfo.barGrounp,
112
+      axisLine: { show: false },
113
+      axisTick: { show: false },
114
+      axisLabel: {
115
+        color: '#fff',
116
+        fontSize: 14,
117
+        margin: 10
118
+      }
119
+    },
120
+    series: [
121
+      {
122
+        name: '在岗',
123
+        type: 'bar',
124
+        stack: 'total',
125
+        barWidth: 20,
126
+        itemStyle: { color: '#408CFF' },
127
+        label: {
128
+          show: true,
129
+          position: 'left',
130
+          color: '#fff',
131
+          formatter: function (params) {
132
+            return Math.abs(params.value)
133
+          }
134
+        },
135
+        data: props.personnelCountInfo.barOpenData // T2航站楼开放70,T1航站楼开放30
136
+      },
137
+      {
138
+        name: '空闲',
139
+        type: 'bar',
140
+        stack: 'total',
141
+        barWidth: 20,
142
+        itemStyle: { color: '#A7C8FF' },
143
+        label: {
144
+          show: true,
145
+          position: 'right',
146
+          color: '#fff',
147
+          formatter: function (params) {
148
+            return Math.abs(params.value)
149
+          }
150
+        },
151
+        data: props.personnelCountInfo.barCloseData  // T2航站楼未开放30,T1航站楼未开放70
152
+      }
153
+    ],
154
+    tooltip: {
155
+      trigger: 'axis',
156
+      axisPointer: { type: 'shadow' },
157
+      formatter: function (params) {
158
+        return params[ 0 ].name + '<br/>' +
159
+          params[ 0 ].seriesName + ': ' + Math.abs(params[ 0 ].value) + '<br/>' +
160
+          params[ 1 ].seriesName + ': ' + Math.abs(params[ 1 ].value);
161
+      }
162
+    }
163
+  }
164
+})
165
+
166
+useECharts(pie, setChartsOptions);
167
+useECharts(bar, setChartsOptionsBar);
168
+</script>
169
+
170
+<style lang="scss" scoped></style>

+ 252 - 0
src/views/dataBigScreen/dashboard/components/pageItems/ProblemDiscovery.vue

@@ -0,0 +1,252 @@
1
+<template>
2
+  <div class="chartsContainer">
3
+    <div class="chartsContainer-title">问题发现</div>
4
+    <div class="chartsContainer-list">
5
+      <div class="chartsContainer-content">
6
+        <div class="chartsContainer-content-top">
7
+          <div class="chartsContainer-content-name">总体问题分布</div>
8
+        </div>
9
+        <div class="chartsContainer-content-content" ref="pie" />
10
+      </div>
11
+
12
+      <div class="chartsContainer-content">
13
+        <div class="chartsContainer-content-top">
14
+          <div class="chartsContainer-content-name">问题分布对比</div>
15
+        </div>
16
+        <div class="chartsContainer-content-content" ref="radar" />
17
+      </div>
18
+    </div>
19
+  </div>
20
+</template>
21
+
22
+<script setup>
23
+import { problemDistribution, problemComparison } from '@/api/item/items'
24
+import { useTimeOut } from './useTimeOut';
25
+import { useECharts } from '@/hooks/useEcharts.js';
26
+import { ref, computed, reactive, inject } from 'vue'
27
+const provideParams = inject('provideParams')
28
+const pie = ref(null)
29
+const pieData = ref([])
30
+const setChartsOptions = computed(() => {
31
+  return {
32
+    tooltip: {
33
+      trigger: 'item',
34
+      formatter: '{a} <br/>{b}: {c} ({d}%)'
35
+    },
36
+    legend: {
37
+      show: false
38
+    },
39
+    series: [
40
+      {
41
+        name: '问题分布',
42
+        type: 'pie',
43
+        radius: [ '0%', '50%' ], // 环形图设置
44
+        center: [ '50%', '50%' ],
45
+        data: pieData.value.map(item => {
46
+          return {
47
+            name: item.name,
48
+            value: item.total
49
+          }
50
+        }),
51
+        label: {
52
+          show: true,
53
+          formatter: function (params) {
54
+            return params.name + '\n' + params.percent + '%'
55
+          },
56
+          color: '#fff',
57
+          fontSize: 14,
58
+        },
59
+        labelLine: {
60
+          length: 10
61
+        },
62
+        emphasis: {
63
+          itemStyle: {
64
+            shadowBlur: 10,
65
+            shadowOffsetX: 0,
66
+            shadowColor: 'rgba(0, 0, 0, 0.5)'
67
+          }
68
+        }
69
+      }
70
+    ]
71
+  }
72
+})
73
+useECharts(pie, setChartsOptions);
74
+
75
+const radar = ref(null)
76
+const radarData = reactive({
77
+  grounp: [],
78
+  data: [],
79
+  legend: []
80
+})
81
+const setRadarOptions = computed(() => {
82
+  return {
83
+    tooltip: {
84
+      trigger: 'item'
85
+    },
86
+    legend: {
87
+      data: radarData.legend,
88
+      bottom: 0,
89
+      textStyle: { color: "#fff" },
90
+    },
91
+    grid: {
92
+      top: '5%',
93
+      bottom: '5%',
94
+      left: '5%',
95
+      right: '5%'
96
+    },
97
+    radar: {
98
+      indicator: radarData.grounp,
99
+      axisName: {
100
+        color: '#fff'
101
+      },
102
+       radius: '50%',
103
+    },
104
+    series: [
105
+      {
106
+        type: 'radar',
107
+        data: radarData.data,
108
+      }
109
+    ]
110
+  }
111
+})
112
+useECharts(radar, setRadarOptions);
113
+
114
+
115
+const permutationRadarDataHandler = (result = []) => {
116
+  const grounpObj = result.reduce((cur, acc) => {
117
+    if (cur[ acc.deptName ]) {
118
+      cur[ acc.deptName ].push(acc)
119
+    } else {
120
+      cur[ acc.deptName ] = [ acc ]
121
+    }
122
+    return cur
123
+  }, {})
124
+
125
+  const grounp = Object.entries(grounpObj)
126
+
127
+  radarData.legend = grounp.map(item => {
128
+    return item[ 0 ]
129
+  })
130
+
131
+
132
+  radarData.grounp = grounp[ 0 ] && grounp[ 0 ][ 1 ] ? grounp[ 0 ][ 1 ].map(item => ({ text: item.name })) : []
133
+  const color = ['#408CFF', '#58A55C', '#DD4D43', '#FAC858']
134
+  radarData.data = grounp.map((item, index) => {
135
+    return {
136
+      name: item[ 0 ],
137
+      value: item[ 1 ].map(attr => attr.total),
138
+      // // 为每个数据系列设置不同的颜色
139
+      color: color[ index % color.length ],
140
+      areaStyle: {
141
+        color: color[ index % color.length ],
142
+        opacity: 0.5
143
+      },
144
+      lineStyle: {
145
+        color: color[ index % color.length ],
146
+        width: 2
147
+      },
148
+      itemStyle: {
149
+        color: color[ index % color.length ],
150
+        borderWidth: 2
151
+      }
152
+    }
153
+  })
154
+}
155
+
156
+useTimeOut(() => {
157
+  provideParams.value.checkedDepartmentId &&  problemDistribution({
158
+    startDate: provideParams.value.startDate,
159
+    endDate: provideParams.value.endDate,
160
+    checkedDepartmentId: provideParams.value.checkedDepartmentId === 'ALL' ? '' : provideParams.value.checkedDepartmentId
161
+  }).then(res => {
162
+    pieData.value = res.data || []
163
+  })
164
+  provideParams.value.checkedDepartmentId &&  problemComparison({
165
+    startDate: provideParams.value.startDate,
166
+    endDate: provideParams.value.endDate,
167
+    checkedDepartmentId: provideParams.value.checkedDepartmentId === 'ALL' ? '' : provideParams.value.checkedDepartmentId
168
+  }).then(res => {
169
+    permutationRadarDataHandler(res.data)
170
+  })
171
+}, [provideParams])
172
+
173
+
174
+
175
+</script>
176
+
177
+<style lang="scss" scoped>
178
+.chartsContainer {
179
+  width: 100%;
180
+  height: 100%;
181
+  position: relative;
182
+  background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
183
+  border-radius: 4px;
184
+  overflow: hidden;
185
+  display: flex;
186
+  flex-direction: column;
187
+
188
+  .chartsContainer-title {
189
+    height: 42px;
190
+    width: 100%;
191
+    background: linear-gradient(to right, #004387 0%, #090B18 100%);
192
+    border-left: 1px solid #1892CE;
193
+    line-height: 42px;
194
+    color: #fff;
195
+    font-weight: 900;
196
+    font-size: 20px;
197
+    text-indent: 1em;
198
+  }
199
+
200
+  .chartsContainer-list {
201
+    padding: 20px 15px 10px;
202
+    box-sizing: border-box;
203
+    display: flex;
204
+    column-gap: 20px;
205
+    flex: 1;
206
+    overflow: hidden;
207
+  }
208
+
209
+  .chartsContainer-content {
210
+    display: flex;
211
+    flex-direction: column;
212
+    row-gap: 10px;
213
+    flex: 1;
214
+    overflow: hidden;
215
+
216
+    .chartsContainer-content-top {
217
+      display: flex;
218
+      align-items: center;
219
+      color: #78DEF5;
220
+
221
+      .chartsContainer-content-name {
222
+        color: #fff;
223
+        font-weight: bold;
224
+        font-size: 16px;
225
+        display: flex;
226
+        align-items: center;
227
+
228
+        &::before {
229
+          content: '';
230
+          display: inline-block;
231
+          border-left: 9px solid #1CB6FF;
232
+          border-top: 8px solid transparent;
233
+          border-right: 9px solid transparent;
234
+          border-bottom: 8px solid transparent;
235
+        }
236
+      }
237
+    }
238
+
239
+    .chartsContainer-content-describe {
240
+      color: #78DEF5;
241
+      font-size: 12px;
242
+      font-weight: bold;
243
+      display: flex;
244
+      align-items: center;
245
+    }
246
+
247
+    .chartsContainer-content-content {
248
+      flex: 1;
249
+    }
250
+  }
251
+}
252
+</style>

+ 202 - 0
src/views/dataBigScreen/dashboard/components/pageItems/QualificationCapability.vue

@@ -0,0 +1,202 @@
1
+<template>
2
+  <ChartsContainer title="资质能力">
3
+    <div style="height: 100%; overflow-y: auto;">
4
+      <div class="content-item">
5
+        <div class="item-label">资质等级:</div>
6
+        <div class="item-value">{{ localSystemData.qualificationLevelSum }}</div>
7
+      </div>
8
+      <div class="content-item">
9
+        <div class="item-label">可上岗岗位数:</div>
10
+        <div class="item-value">{{ localSystemData.availablePositionsSum }}</div>
11
+      </div>
12
+      <div class="content-item">
13
+        <div class="item-label">政治面貌:</div>
14
+        <div class="item-value">{{ localSystemData.politicalStatusSum }}</div>
15
+      </div>
16
+      <div class="content-item">
17
+        <div class="item-label">学历:</div>
18
+        <div class="item-value">{{ localSystemData.educationSum }}</div>
19
+      </div>
20
+      <div class="content-item">
21
+        <div class="item-label">平均年龄:</div>
22
+        <div class="item-value">{{ localSystemData.averageAge }}岁&nbsp;最高{{ localSystemData.maxAge }}岁&nbsp;最低{{
23
+          localSystemData.minAge }}岁</div>
24
+      </div>
25
+      <div class="content-item">
26
+        <div class="item-label">通过政审人数:</div>
27
+        <div class="item-value">{{ localSystemData.passedKoroughCountSum }}人</div>
28
+      </div>
29
+      <div class="content-item">
30
+        <div class="item-label">身体健康人数:</div>
31
+        <div class="item-value">{{ localSystemData.healthyCountSum }}人</div>
32
+      </div>
33
+      <div class="content-item">
34
+        <div class="item-label">行政处罚人数:</div>
35
+        <div class="item-value">{{ localSystemData.penalizedCountSum }}人</div>
36
+      </div>
37
+    </div>
38
+  </ChartsContainer>
39
+  <ChartsContainer title="工作履历">
40
+    <div style="height: 100%;  overflow-y: auto;">
41
+      <div class="content-item">
42
+        <div class="item-label">平均工作年限:</div>
43
+        <div class="item-value">{{ localSystemData && localSystemData.workYearsStats ? localSystemData.workYearsStats.averageWorkYears
44
+          || 0 : 0 }}</div>
45
+      </div>
46
+      <div class="content-item">
47
+        <div class="item-label">平均安检工作年限:</div>
48
+        <div class="item-value">{{ localSystemData && localSystemData.securityWorkYearsStats ?
49
+          localSystemData.securityWorkYearsStats.averageWorkYears || 0 : 0 }}</div>
50
+      </div>
51
+      <div class="content-item">
52
+        <div class="item-label">曾担任过班组长人数:</div>
53
+        <div class="item-value">{{ localSystemData && localSystemData.securityWorkPositionStats ?
54
+          localSystemData.securityWorkPositionStats.teamLeaderCount || 0 : 0 }}</div>
55
+      </div>
56
+      <div class="content-item">
57
+        <div class="item-label">工作奖励次数:</div>
58
+        <div class="item-value">{{ localSystemData && localSystemData.workRewardsStats ?
59
+          localSystemData.workRewardsStats.totalPersonTimes || 0 : 0 }}</div>
60
+      </div>
61
+      <div class="content-item">
62
+        <div class="item-label">工作处罚次数:</div>
63
+        <div class="item-value">{{ localSystemData && localSystemData.workPenaltiesStats ?
64
+          localSystemData.workPenaltiesStats.totalPersonTimes || 0 : 0 }}</div>
65
+      </div>
66
+    </div>
67
+  </ChartsContainer>
68
+</template>
69
+
70
+<script setup>
71
+import ChartsContainer from '../ChartsContainer.vue';
72
+import { ref, inject } from 'vue';
73
+import { getModuleMetrics } from '@/api/item/items'
74
+import { useTimeOut } from '../pageItems/useTimeOut'
75
+import { useDict } from '@/utils/dict'
76
+
77
+const {
78
+  sys_user_political_status,
79
+  sys_user_qualification_level,
80
+} = useDict('sys_user_political_status', 'sys_user_qualification_level')
81
+
82
+const localSystemData = ref({})
83
+const params = inject('provideParams')
84
+
85
+const handleDeptData = async (newVal) => {
86
+  // 检查newVal是否为null或undefined
87
+  if (!newVal || Object.values(newVal).length === 0) {
88
+    localSystemData.value = {};
89
+    return;
90
+  }
91
+  
92
+  const res = { ...newVal };
93
+  const { qualificationLevelStats, positionCompetencyStats, politicalStatusStats, educationStats, ageStats, politicalReviewStats, physicalHealthStats, administrativePenaltyStats } = newVal
94
+
95
+  // 检查qualificationLevelStats是否存在且是数组
96
+  if (!qualificationLevelStats || !Array.isArray(qualificationLevelStats)) {
97
+    return;
98
+  }
99
+
100
+  // 使用useDict获取资质等级字典
101
+  const qualificationDict = sys_user_qualification_level.value || []
102
+  let strArr = [];
103
+  qualificationLevelStats.map(item => {
104
+    strArr.push(`${item?.levelName} ${item.count}人`)
105
+  })
106
+  res.qualificationLevelSum = strArr.join('、')
107
+  // 按照字典顺序遍历,确保从一级开始显示
108
+  // for (const dictItem of qualificationDict) {
109
+  //   const statItem = qualificationLevelStats.find(item => item.levelName === dictItem.value)
110
+  //   if (statItem) {
111
+  //     res.qualificationLevelSum = `${res.qualificationLevelSum ? res.qualificationLevelSum + '、' : ''}${dictItem.label} ${statItem.count}人`
112
+  //   }
113
+  // }
114
+
115
+  let availablePositionsSum = []
116
+  for (const item of positionCompetencyStats) {
117
+    availablePositionsSum.push(`${item.postName} ${item.competentCount}人`)
118
+  }
119
+  res.availablePositionsSum = availablePositionsSum.join('、')
120
+
121
+  let politicalStatusArr = []
122
+  for (const item of politicalStatusStats) {
123
+    if (item.politicalStatus) {
124
+      let politicalStatus = sys_user_political_status.value.find(attr => attr.value === item.politicalStatus) || {}
125
+      politicalStatusArr.push({
126
+        politicalStatus: politicalStatus.label,
127
+        totalCount: item.totalCount
128
+      })
129
+    }
130
+  }
131
+  let politicalStatusObj = politicalStatusArr.find(item => item.politicalStatus === "中共党员")
132
+  if (politicalStatusObj) {
133
+    res.politicalStatusSum = `${politicalStatusObj.politicalStatus} ${politicalStatusObj.totalCount}人`
134
+  } else {
135
+    res.politicalStatusSum = '-';
136
+  }
137
+
138
+  // 处理学历统计信息
139
+  if (educationStats) {
140
+    const educationParts = [];
141
+
142
+    // 按学历层次从高到低排序显示
143
+    if (educationStats.masterCount > 0) {
144
+      educationParts.push(`硕士 ${educationStats.masterCount}人`);
145
+    }
146
+    if (educationStats.bachelorCount > 0) {
147
+      educationParts.push(`本科 ${educationStats.bachelorCount}人`);
148
+    }
149
+    if (educationStats.collegeCount > 0) {
150
+      educationParts.push(`专科 ${educationStats.collegeCount}人`);
151
+    }
152
+    if (educationStats.highSchoolCount > 0) {
153
+      educationParts.push(`高中 ${educationStats.highSchoolCount}人`);
154
+    }
155
+    if (educationStats.middleSchoolCount > 0) {
156
+      educationParts.push(`初中 ${educationStats.middleSchoolCount}人`);
157
+    }
158
+    if (educationStats.primarySchoolCount > 0) {
159
+      educationParts.push(`小学 ${educationStats.primarySchoolCount}人`);
160
+    }
161
+
162
+    res.educationSum = educationParts.length > 0 ? educationParts.join('、') : '-';
163
+  } else {
164
+    res.educationSum = '-';
165
+  }
166
+  res.averageAge = ageStats && ageStats.averageAge ? ageStats.averageAge : '-'
167
+  res.minAge = ageStats && ageStats.minAge ? ageStats.minAge : '-'
168
+  res.maxAge = ageStats && ageStats.maxAge ? ageStats.maxAge : '-'
169
+  res.passedKoroughCountSum = politicalReviewStats && politicalReviewStats.passedCount || 0;
170
+  res.healthyCountSum = physicalHealthStats && physicalHealthStats.healthyCount || 0;
171
+  res.penalizedCountSum = administrativePenaltyStats && administrativePenaltyStats.penalizedCount || 0;
172
+  localSystemData.value = res;
173
+}
174
+useTimeOut(() => {
175
+  params.value.deptId && sys_user_political_status.value && sys_user_qualification_level.value && getModuleMetrics({ deptId: params.value.deptId, userTypeStr: 'station' }).then(res => {
176
+    const { moduleResults = { system: {} } } = res.data
177
+    handleDeptData(moduleResults.system)
178
+  })
179
+}, [params, sys_user_political_status, sys_user_qualification_level])
180
+
181
+</script>
182
+
183
+<style lang="scss" scoped>
184
+.content-item {
185
+  display: flex;
186
+  column-gap: 5px;
187
+  margin-bottom: 8px;
188
+  font-size: 16px;
189
+
190
+  .item-label {
191
+    color: #fff;
192
+    font-weight: bold;
193
+    width: 10em;
194
+  }
195
+
196
+  .item-value {
197
+    color: #fefefe;
198
+    flex: 1;
199
+    text-align: end;
200
+  }
201
+}
202
+</style>

+ 129 - 0
src/views/dataBigScreen/dashboard/components/pageItems/QualificationCapabilityPersonal.vue

@@ -0,0 +1,129 @@
1
+<template>
2
+  <ChartsContainer title="资质能力">
3
+    <div style="height: 100%; overflow-y: auto;">
4
+      <div class="content-item">
5
+        <div class="item-label">资质等级:</div>
6
+        <div class="item-value">{{ getLabel(localSystemData.qualificationLevel, sys_user_qualification_level) }}</div>
7
+      </div>
8
+      <div class="content-item">
9
+        <div class="item-label">可上岗岗位:</div>
10
+        <div class="item-value">{{ localSystemData.availablePositions }}</div>
11
+      </div>
12
+      <div class="content-item">
13
+        <div class="item-label">政治面貌:</div>
14
+        <div class="item-value">{{ getLabel(localSystemData.politicalStatus, sys_user_political_status) }}</div>
15
+      </div>
16
+      <div class="content-item">
17
+        <div class="item-label">学历:</div>
18
+        <div class="item-value">{{ getLabel(localSystemData.education, sys_user_schooling) }}</div>
19
+      </div>
20
+      <div class="content-item">
21
+        <div class="item-label">年龄:</div>
22
+        <div class="item-value">{{ localSystemData.age }}岁</div>
23
+      </div>
24
+      <div class="content-item">
25
+        <div class="item-label">是否通过政审:</div>
26
+        <div class="item-value">{{ localSystemData.isPoliticalReviewPassed ? '是' : '否' }}</div>
27
+      </div>
28
+      <div class="content-item">
29
+        <div class="item-label">身体健康情况:</div>
30
+        <div class="item-value">{{ !localSystemData.isHealthy ? '健康' : localSystemData.isHealthy }}</div>
31
+      </div>
32
+      <div class="content-item">
33
+        <div class="item-label">有无行政处罚:</div>
34
+        <div class="item-value">{{ !localSystemData.hasAdministrativePenalty ? '无' : localSystemData.hasAdministrativePenalty }}</div>
35
+      </div>
36
+    </div>
37
+  </ChartsContainer>
38
+  <ChartsContainer title="工作履历">
39
+    <div style="height: 100%;  overflow-y: auto;">
40
+      <div class="content-item">
41
+        <div class="item-label">工作年限:</div>
42
+        <div class="item-value">{{ localSystemData.workYears || 0 }}</div>
43
+      </div>
44
+      <div class="content-item">
45
+        <div class="item-label">安检工作年限:</div>
46
+        <div class="item-value">{{ localSystemData.securityWorkYears || 0 }}</div>
47
+      </div>
48
+      <div class="content-item">
49
+        <div class="item-label">曾任安检最高职务:</div>
50
+        <div class="item-value">{{ localSystemData.securityWorkPosition || '-' }}</div>
51
+      </div>
52
+      <div class="content-item">
53
+        <div class="item-label">工作奖励次数:</div>
54
+        <div class="item-value">{{ localSystemData.workRewards || 0  }}</div>
55
+      </div>
56
+      <div class="content-item">
57
+        <div class="item-label">工作处罚次数:</div>
58
+        <div class="item-value">{{ localSystemData.workPenalties || 0 }}</div>
59
+      </div>
60
+    </div>
61
+  </ChartsContainer>
62
+</template>
63
+
64
+<script setup>
65
+import ChartsContainer from '../ChartsContainer.vue';
66
+import { ref, inject } from 'vue';
67
+import { getModuleMetrics } from '@/api/item/items'
68
+import { useTimeOut } from '../pageItems/useTimeOut'
69
+import { useDict } from '@/utils/dict'
70
+const props = defineProps({
71
+  personal: {
72
+    type: Boolean,
73
+    default: false
74
+  }
75
+})
76
+const {
77
+  sys_user_political_status,
78
+  sys_user_qualification_level,
79
+  sys_user_schooling
80
+} = useDict('sys_user_political_status', 'sys_user_qualification_level', 'sys_user_schooling')
81
+
82
+const getLabel = (value, list = []) => {
83
+  const item = list.find(item => item.value.includes(value)) || {}
84
+  return item.label
85
+}
86
+
87
+const localSystemData = ref({})
88
+const params = inject('provideParams')
89
+
90
+const handleDeptData = async (newVal) => {
91
+  // 检查newVal是否为null或undefined
92
+  if (!newVal) {
93
+    localSystemData.value = {};
94
+    return;
95
+  }
96
+  localSystemData.value = {
97
+    ...newVal,
98
+    availablePositions: newVal.availablePositions.map(item => item.postName).join('、')
99
+  };
100
+}
101
+useTimeOut(() => {
102
+  params.value.deptId && getModuleMetrics({ userId: params.value.deptId, userTypeStr: 'personal' }).then(res => {
103
+    const { moduleResults = { system: {} } } = res.data
104
+    handleDeptData(moduleResults.system)
105
+  })
106
+}, [params])
107
+
108
+</script>
109
+
110
+<style lang="scss" scoped>
111
+.content-item {
112
+  display: flex;
113
+  column-gap: 5px;
114
+  margin-bottom: 8px;
115
+  font-size: 16px;
116
+
117
+  .item-label {
118
+    color: #fff;
119
+    font-weight: bold;
120
+    width: 10em;
121
+  }
122
+
123
+  .item-value {
124
+    color: #fefefe;
125
+    flex: 1;
126
+    text-align: end;
127
+  }
128
+}
129
+</style>

+ 57 - 0
src/views/dataBigScreen/dashboard/components/pageItems/SectionTaskDetails.vue

@@ -0,0 +1,57 @@
1
+<template>
2
+  <ChartsContainer name="科级任务明细">
3
+    <template #other>
4
+      <span style="margin-right: 10px; font-size: 14px; white-space: nowrap;">{{ tableData.taskName || '' }} 每日{{ tableData.ruleTypeNum || 1
5
+        }}次</span>
6
+      <CustomStyleSelect :options="selectList" v-model="selectActive" />
7
+    </template>
8
+    <template #describe>
9
+      完成情况:{{ `${tableData.notStartedCount || 0}/${tableData.inProgressCount || 0}/${tableData.completedCount || 0}` }}
10
+    </template>
11
+    <div ref="content" style="height: 100%;">
12
+      <el-table v-auto-scroll :data="tableData.checkLargeScreenInspectionExecuteItemDtoList" border style="width: 100%" height="100%">
13
+        <el-table-column prop="userName" label="姓名" width="90" />
14
+        <el-table-column prop="completionPercentage" label="今日完成比例">
15
+          <template #default=" scope ">
16
+            <el-progress
17
+              :text-inside="true"
18
+              :stroke-width="16"
19
+              :percentage="scope.row.completionPercentage"
20
+              style="--el-border-color-lighter: #fff"
21
+              color="#58A55C" />
22
+          </template>
23
+        </el-table-column>
24
+        <el-table-column prop="checkOrderCount" label="整改单数" width="90" />
25
+        <el-table-column prop="rectificationOrderCount" label="不合格项数" width="100" />
26
+      </el-table>
27
+    </div>
28
+  </ChartsContainer>
29
+</template>
30
+
31
+<script setup>
32
+import ChartsContainer from '../ChartsContainer.vue';
33
+import CustomStyleSelect from './CustomStyleSelect.vue'
34
+import { computed, ref } from 'vue';
35
+
36
+const props = defineProps({
37
+  tableData: {
38
+    type: Array,
39
+    default: []
40
+  }
41
+})
42
+const selectActive = ref('')
43
+const selectList = computed(() => {
44
+  const options = props.tableData.map(item => {
45
+    return ({ label: item.taskName, value: item.taskCode })
46
+  })
47
+  selectActive.value = options[ 0 ] ? options[ 0 ].value : ''
48
+  return options || []
49
+})
50
+
51
+const tableData = computed(() => {
52
+  return props.tableData.find(item => item.taskCode === selectActive.value) || {}
53
+})
54
+
55
+</script>
56
+
57
+<style lang="scss" scoped></style>

+ 354 - 0
src/views/dataBigScreen/dashboard/components/pageItems/SeizeDetails.vue

@@ -0,0 +1,354 @@
1
+<template>
2
+  <div class="chartsContainer">
3
+    <div class="chartsContainer-title">查获明细</div>
4
+    <div class="chartsContainer-list">
5
+      <div class="chartsContainer-content">
6
+        <div class="chartsContainer-content-top">
7
+          <div class="chartsContainer-content-name">违禁品类别</div>
8
+        </div>
9
+        <div class="chartsContainer-content-content" ref="pieCategory" />
10
+      </div>
11
+
12
+       <div class="chartsContainer-content">
13
+        <div class="chartsContainer-content-top">
14
+          <div class="chartsContainer-content-name">查获部位</div>
15
+        </div>
16
+        <div class="chartsContainer-content-content" ref="pieAppPosition" />
17
+      </div>
18
+
19
+       <div class="chartsContainer-content">
20
+        <div class="chartsContainer-content-top">
21
+          <div class="chartsContainer-content-name">安检岗位</div>
22
+        </div>
23
+        <div class="chartsContainer-content-content" ref="pieLargeScreenPost" />
24
+      </div>
25
+      
26
+      <div class="chartsContainer-content">
27
+        <div class="chartsContainer-content-top">
28
+          <div class="chartsContainer-content-name">查获时间分布</div>
29
+        </div>
30
+        <div class="chartsContainer-content-content" ref="lineChart" />
31
+      </div>
32
+    </div>
33
+  </div>
34
+</template>
35
+
36
+<script setup>
37
+import { category, appPosition, largeScreenPost, appTimeSpan } from '@/api/item/items'
38
+import { useTimeOut } from './useTimeOut';
39
+import { useECharts } from '@/hooks/useEcharts.js';
40
+import { ref, computed, inject } from 'vue'
41
+const provideParams = inject('provideParams')
42
+const pieCategory = ref(null)
43
+const pieCategoryData = ref([])
44
+const setChartsOptions = computed(() => {
45
+  return {
46
+    tooltip: {
47
+      trigger: 'item',
48
+      formatter: '{a} <br/>{b}: {c} ({d}%)'
49
+    },
50
+    legend: {
51
+      show: false
52
+    },
53
+    series: [
54
+      {
55
+        name: '违禁品类别',
56
+        type: 'pie',
57
+        radius: [ '0%', '50%' ], // 环形图设置
58
+        center: [ '50%', '50%' ],
59
+        data: pieCategoryData.value.map(item => {
60
+          return {
61
+            name: item.name,
62
+            value: item.total
63
+          }
64
+        }),
65
+        label: {
66
+          show: true,
67
+          formatter: function (params) {
68
+            return params.name + '\n' + params.percent + '%'
69
+          },
70
+          color: '#fff',
71
+          fontSize: 14,
72
+        },
73
+        labelLine: {
74
+          length: 15
75
+        },
76
+        emphasis: {
77
+          itemStyle: {
78
+            shadowBlur: 10,
79
+            shadowOffsetX: 0,
80
+            shadowColor: 'rgba(0, 0, 0, 0.5)'
81
+          }
82
+        }
83
+      }
84
+    ]
85
+  }
86
+})
87
+useECharts(pieCategory, setChartsOptions);
88
+
89
+const pieAppPosition = ref(null)
90
+const pieAppPositionData = ref([])
91
+const setpieAppPositionOptions = computed(() => {
92
+  return {
93
+    tooltip: {
94
+      trigger: 'item',
95
+      formatter: '{a} <br/>{b}: {c} ({d}%)'
96
+    },
97
+    legend: {
98
+      show: false
99
+    },
100
+    series: [
101
+      {
102
+        name: '查获部位',
103
+        type: 'pie',
104
+        radius: [ '0%', '50%' ], // 环形图设置
105
+        center: [ '50%', '50%' ],
106
+        data: pieAppPositionData.value.map(item => {
107
+          return {
108
+            name: item.name,
109
+            value: item.total
110
+          }
111
+        }),
112
+        label: {
113
+          show: true,
114
+          formatter: function (params) {
115
+            return params.name + '\n' + params.percent + '%'
116
+          },
117
+          color: '#fff',
118
+          fontSize: 14,
119
+        },
120
+        labelLine: {
121
+          length: 15
122
+        },
123
+        emphasis: {
124
+          itemStyle: {
125
+            shadowBlur: 10,
126
+            shadowOffsetX: 0,
127
+            shadowColor: 'rgba(0, 0, 0, 0.5)'
128
+          }
129
+        }
130
+      }
131
+    ]
132
+  }
133
+})
134
+useECharts(pieAppPosition, setpieAppPositionOptions);
135
+
136
+const pieLargeScreenPost = ref(null)
137
+const pieLargeScreenPostData = ref([])
138
+const setpieLargeScreenPostOptions = computed(() => {
139
+  return {
140
+    tooltip: {
141
+      trigger: 'item',
142
+      formatter: '{a} <br/>{b}: {c} ({d}%)'
143
+    },
144
+    legend: {
145
+      show: false
146
+    },
147
+    series: [
148
+      {
149
+        name: '安检岗位',
150
+        type: 'pie',
151
+        radius: [ '0%', '50%' ], // 环形图设置
152
+        center: [ '50%', '50%' ],
153
+        data: pieLargeScreenPostData.value.map(item => {
154
+          return {
155
+            name: item.name,
156
+            value: item.total
157
+          }
158
+        }),
159
+        label: {
160
+          show: true,
161
+          formatter: function (params) {
162
+            return params.name + '\n' + params.percent + '%'
163
+          },
164
+          color: '#fff',
165
+          fontSize: 14,
166
+        },
167
+        labelLine: {
168
+          length: 15
169
+        },
170
+        emphasis: {
171
+          itemStyle: {
172
+            shadowBlur: 10,
173
+            shadowOffsetX: 0,
174
+            shadowColor: 'rgba(0, 0, 0, 0.5)'
175
+          }
176
+        }
177
+      }
178
+    ]
179
+  }
180
+})
181
+useECharts(pieLargeScreenPost, setpieLargeScreenPostOptions);
182
+
183
+
184
+const lineChart = ref(null)
185
+const lineChartData = ref([])
186
+const setlineChartOptions = computed(() => {
187
+  return {
188
+    tooltip: {
189
+      trigger: 'axis'
190
+    },
191
+    xAxis: {
192
+      type: 'category',
193
+      boundaryGap: false,
194
+      data: lineChartData.value.map(item => item.hourOfDay),
195
+      axisLabel: {
196
+        color: '#fff',
197
+        rotate: 25,
198
+        fontSize: 12,
199
+      }
200
+    },
201
+    yAxis: {
202
+      type: 'value',
203
+      minInterval: 1,
204
+      max: Math.max(...lineChartData.value.map(item => item.total)) * 2,
205
+      axisLabel: {
206
+        color: '#fff',
207
+      }
208
+    },
209
+    series: {
210
+      data: lineChartData.value.map(item => item.total),
211
+      type: 'line',
212
+      smooth: true,
213
+    },
214
+    grid: {
215
+      left: '5%',
216
+      right: '0%',
217
+      bottom: '10%',
218
+      top: '5%',
219
+      containLabel: true
220
+    }
221
+  };
222
+})
223
+useECharts(lineChart, setlineChartOptions);
224
+
225
+useTimeOut(() => {
226
+  provideParams.value.inspectDepartmentId && category({
227
+    startDate: provideParams.value.startDate,
228
+    endDate: provideParams.value.endDate,
229
+    inspectDepartmentId: provideParams.value.inspectDepartmentId === 'ALL' ? '' : provideParams.value.inspectDepartmentId
230
+  }).then(res => {
231
+    pieCategoryData.value = (res.data || [])
232
+  })
233
+  provideParams.value.inspectDepartmentId && appPosition({
234
+    startDate: provideParams.value.startDate,
235
+    endDate: provideParams.value.endDate,
236
+    inspectDepartmentId: provideParams.value.inspectDepartmentId === 'ALL' ? '' : provideParams.value.inspectDepartmentId
237
+  }).then(res => {
238
+    pieAppPositionData.value = (res.data || [])
239
+  })
240
+  provideParams.value.inspectDepartmentId && largeScreenPost({
241
+    startDate: provideParams.value.startDate,
242
+    endDate: provideParams.value.endDate,
243
+    inspectDepartmentId: provideParams.value.inspectDepartmentId === 'ALL' ? '' : provideParams.value.inspectDepartmentId
244
+  }).then(res => {
245
+    pieLargeScreenPostData.value = (res.data || [])
246
+  })
247
+ provideParams.value.inspectDepartmentId &&  appTimeSpan({
248
+    startDate: provideParams.value.startDate,
249
+    endDate: provideParams.value.endDate,
250
+    inspectDepartmentId: provideParams.value.inspectDepartmentId === 'ALL' ? '' : provideParams.value.inspectDepartmentId
251
+  }).then(res => {
252
+    lineChartData.value = Array.isArray(res.data) ? Array.from({length: 24}, (_, index) => {
253
+      const item = res.data.find(item => Number(item.hourOfDay) === index)
254
+      if (item) {
255
+        return {
256
+          total: item.total,
257
+          hourOfDay: `${index}:00`
258
+        }
259
+      } else {
260
+        return {
261
+          total: 0,
262
+          hourOfDay: `${index}:00`
263
+        }
264
+      }
265
+    }) : []
266
+    
267
+  })
268
+}, [provideParams])
269
+
270
+
271
+
272
+</script>
273
+
274
+<style lang="scss" scoped>
275
+.chartsContainer {
276
+  width: 100%;
277
+  height: 100%;
278
+  position: relative;
279
+  background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
280
+  border-radius: 4px;
281
+  overflow: hidden;
282
+  display: flex;
283
+  flex-direction: column;
284
+
285
+  .chartsContainer-title {
286
+    height: 42px;
287
+    width: 100%;
288
+    background: linear-gradient(to right, #004387 0%, #090B18 100%);
289
+    border-left: 1px solid #1892CE;
290
+    line-height: 42px;
291
+    color: #fff;
292
+    font-weight: 900;
293
+    font-size: 20px;
294
+    text-indent: 1em;
295
+  }
296
+
297
+  .chartsContainer-list {
298
+    padding: 20px 15px 10px;
299
+    box-sizing: border-box;
300
+    display: flex;
301
+    column-gap: 20px;
302
+    row-gap: 20px;
303
+    flex: 1;
304
+    overflow: hidden;
305
+    flex-wrap: wrap;
306
+  }
307
+
308
+  .chartsContainer-content {
309
+    display: flex;
310
+    flex-direction: column;
311
+    row-gap: 10px;
312
+    width: calc(50% - 10px);
313
+    height: calc(50% - 10px);
314
+    overflow: hidden;
315
+
316
+    .chartsContainer-content-top {
317
+      display: flex;
318
+      align-items: center;
319
+      color: #78DEF5;
320
+
321
+      .chartsContainer-content-name {
322
+        color: #fff;
323
+        font-weight: bold;
324
+        font-size: 16px;
325
+        display: flex;
326
+        align-items: center;
327
+
328
+        &::before {
329
+          content: '';
330
+          display: inline-block;
331
+          border-left: 9px solid #1CB6FF;
332
+          border-top: 8px solid transparent;
333
+          border-right: 9px solid transparent;
334
+          border-bottom: 8px solid transparent;
335
+        }
336
+      }
337
+    }
338
+
339
+    .chartsContainer-content-describe {
340
+      color: #78DEF5;
341
+      font-size: 12px;
342
+      font-weight: bold;
343
+      display: flex;
344
+      align-items: center;
345
+    }
346
+
347
+    .chartsContainer-content-content {
348
+      flex: 1;
349
+      width: 100%;
350
+      overflow: hidden;
351
+    }
352
+  }
353
+}
354
+</style>

+ 355 - 0
src/views/dataBigScreen/dashboard/components/pageItems/StandardExecution.vue

@@ -0,0 +1,355 @@
1
+<template>
2
+  <ChartsContainer title="标准执行">
3
+    <div style="height: 100%; flex-direction: column; display: flex;">
4
+      <div class="cards-container">
5
+        <div class="card-item" v-for=" item in cardData " :key="item.label">
6
+          <div>{{ item.label }}</div>
7
+          <div :style="'--color:' + item.color">{{ item.value }}</div>
8
+        </div>
9
+      </div>
10
+      <div class="charts-container">
11
+        <div class="charts-container-item">
12
+          <div class="charts-container-name">问题分布项</div>
13
+          <div class="charts-container-content" ref="problem" v-show="radarData.grounp.length > 1" />
14
+          <div class="charts-container-content" v-show="radarData.grounp.length < 1" style="margin-top: 80px; text-align: center; color: #fff">
15
+            {{ radarData.grounp.length < 1 ? '暂无数据' : '' }}
16
+          </div>
17
+        </div>
18
+        <div class="charts-container-item">
19
+          <div class="charts-container-name">测试总错题数</div>
20
+          <div class="charts-container-content" ref="wrongAnswer"/>
21
+        </div>
22
+      </div>
23
+    </div>
24
+  </ChartsContainer>
25
+</template>
26
+
27
+<script setup>
28
+import ChartsContainer from '../ChartsContainer.vue';
29
+import { computed, ref, reactive } from 'vue';
30
+import { useTimeOut } from './useTimeOut';
31
+import { getSiteProfile, getDeptProfile, getUserProfile, getPortrait } from '@/api/item/items'
32
+import { useECharts } from '@/hooks/useEcharts.js';
33
+const props = defineProps({
34
+  type: {
35
+    type: String,
36
+    default: '' // team, department, station
37
+  }
38
+})
39
+const problem = ref(null)
40
+const wrongAnswer = ref(null)
41
+const portraitData = ref({
42
+})
43
+const profileData = ref({
44
+  rankingStats: {}
45
+})
46
+const params = inject('provideParams')
47
+const cardData = computed(() => {
48
+  return [
49
+    props.type ? { 
50
+      label: '巡检排名', 
51
+      value: `${portraitData.value.stationRanking || 0} / ${portraitData.value.stationTotal || 0}`, 
52
+      color: '#35D8FF' 
53
+    } : undefined,
54
+    { label: '巡检总问题数', value: portraitData.value.sumCount || 0, color: '#35D8FF' },
55
+    { label: '按期整改', value: portraitData.value.onTimeCompletedCount || 0, color: '#FFC061' },
56
+    { label: '超期整改', value: portraitData.value.overTimeCompletedCount || 0, color: '#DD4D43' },
57
+    props.type ? {
58
+      label: '测试排名',
59
+      value: `${profileData.value.rankingStats.siteRanking || 0} / ${profileData.value.rankingStats.siteTotalDepts || profileData.value.rankingStats.siteTotalUsers || 0 }`, 
60
+      color: '#35D8FF'
61
+    } : undefined,
62
+    !props.type ? {
63
+      label: '测试平均分',
64
+      value: profileData.value.scoreStats && profileData.value.scoreStats.totalAvgScore
65
+        || profileData.value.avgScore || 0, color: '#35D8FF'
66
+    } : undefined,
67
+    !props.type ? { label: '测试总错题数', value: profileData.value.totalErrors || 0, color: '#35D8FF' } : undefined,
68
+  ].filter(Boolean)
69
+})
70
+const radarData = reactive({
71
+  legend: [],
72
+  grounp: [],
73
+  data: []
74
+})
75
+const problemOption = computed(() => {
76
+  return {
77
+
78
+    legend: props.type ? {
79
+      show: false
80
+    } : {
81
+      data: radarData.legend,
82
+      top: 10,
83
+      textStyle: {
84
+        color: '#fff',
85
+        fontSize: 12
86
+      }
87
+    },
88
+    radar: {
89
+      indicator: radarData.grounp,
90
+      shape: 'polygon',
91
+      splitNumber: 4,
92
+      radius: '50%',
93
+      center: [ '50%', radarData.legend.length ? '60%' : '50%' ], // 缩小雷达图并居中,为label留出空间
94
+      axisName: {
95
+        color: '#fff',
96
+        fontSize: 12
97
+      },
98
+    },
99
+    series: [ {
100
+      type: 'radar',
101
+      data: radarData.data,
102
+      emphasis: {
103
+        lineStyle: {
104
+          width: 4
105
+        }
106
+      }
107
+    } ],
108
+    tooltip: {
109
+      trigger: 'item'
110
+    }
111
+  };
112
+})
113
+useECharts(problem, problemOption)
114
+
115
+const wrongAnswerData = reactive({
116
+  legend: [],
117
+  grounp: [
118
+    { text: '操作执行' },
119
+    { text: '传达学习' },
120
+    { text: '班组管理' },
121
+    { text: '工作纪律' },
122
+    { text: '设施设备' },
123
+    { text: '通道样貌' }
124
+  ],
125
+  data: []
126
+})
127
+const wrongAnswerOption = computed(() => {
128
+  return {
129
+    legend: {
130
+      data: wrongAnswerData.legend,
131
+      top: 15,
132
+      textStyle: {
133
+        color: '#fff',
134
+        fontSize: 12
135
+      }
136
+    },
137
+    radar: {
138
+      indicator: wrongAnswerData.grounp,
139
+      shape: 'polygon',
140
+      splitNumber: 4,
141
+      radius: '50%',
142
+      center: [ '50%', wrongAnswerData.legend.length ? '60%' : '50%' ], // 缩小雷达图并居中,为label留出空间
143
+      axisName: {
144
+        color: '#fff',
145
+        fontSize: 12
146
+      },
147
+    },
148
+    series: [ {
149
+      type: 'radar',
150
+      data: wrongAnswerData.data,
151
+      emphasis: {
152
+        lineStyle: {
153
+          width: 4
154
+        }
155
+      }
156
+    } ],
157
+    tooltip: {
158
+      trigger: 'item'
159
+    }
160
+  };
161
+})
162
+useECharts(wrongAnswer, wrongAnswerOption)
163
+const color = ['#408CFF', '#58A55C', '#DD4D43', '#FAC858']
164
+const problemRadarDataHandler = (result = []) => {
165
+  const grounpObj = result.reduce((cur, acc) => {
166
+    if (cur[ acc.deptName ]) {
167
+      cur[ acc.deptName ].push(acc)
168
+    } else {
169
+      cur[ acc.deptName ] = [ acc ]
170
+    }
171
+    return cur
172
+  }, {})
173
+
174
+  const grounp = Object.entries(grounpObj)
175
+
176
+  radarData.legend =grounp.length > 1 ? grounp.map(item => {
177
+    return item[ 0 ]
178
+  }) : []
179
+
180
+
181
+  radarData.grounp = grounp[ 0 ] && grounp[ 0 ][ 1 ] ? grounp[ 0 ][ 1 ].map(item => ({ text: item.name })) : []
182
+  radarData.data = grounp.map((item, index) => {
183
+    return {
184
+      name: props.type === 'team' ? '问题分布项' : item[ 0 ],
185
+      value: item[ 1 ].map(attr => attr.total),
186
+      // // 为每个数据系列设置不同的颜色
187
+      color: color[ index % color.length ],
188
+      areaStyle: {
189
+        opacity: 0.5,
190
+        color: color[ index % color.length ],
191
+      },
192
+      lineStyle: {
193
+        width: 2
194
+      },
195
+      itemStyle: {
196
+        borderWidth: 2,
197
+        color: color[ index % color.length ],
198
+      }
199
+    }
200
+  })
201
+}
202
+
203
+
204
+useTimeOut(() => {
205
+  if (!props.type) {
206
+    params.value.deptId && getSiteProfile({ siteId: params.value.deptId }).then(res => {
207
+      profileData.value = res.data
208
+      const { deptCategoryErrors = [] } = res.data
209
+
210
+      wrongAnswerData.legend = deptCategoryErrors.map(item => item.deptName)      
211
+      wrongAnswerData.data = deptCategoryErrors.map((item, index) => {
212
+        return {
213
+          name: item.deptName,
214
+          value: wrongAnswerData.grounp.map(attr => {
215
+            const rowData = item.categoryErrors.find(row => row.categoryName === attr.text) || { errorCount: 0 }
216
+            return rowData.errorCount
217
+          }),
218
+          areaStyle: {
219
+            opacity: 0.5,
220
+            color: color[ index % color.length ],
221
+          },
222
+          lineStyle: {
223
+            width: 2
224
+          },
225
+          itemStyle: {
226
+            borderWidth: 2,
227
+            color: color[ index % color.length ],
228
+          }
229
+        }
230
+      })
231
+    })
232
+  } else {
233
+    const invokerApi = props.type === 'department' ? 
234
+      params.value.deptId && getDeptProfile({ deptId: params.value.deptId || '' }) : 
235
+      props.type === 'personal' ? getUserProfile({ userId: params.value.deptId || '' }) : getUserProfile({ deptId: params.value.deptId || '' }) 
236
+    params.value.deptId  && invokerApi.then(res => {
237
+      profileData.value = res.data
238
+      const deptCategoryErrors = [res.data]
239
+
240
+      wrongAnswerData.legend = []
241
+      wrongAnswerData.data = deptCategoryErrors.map((item, index) => {
242
+        return {
243
+          name: item.deptName,
244
+          value: wrongAnswerData.grounp.map((attr, index) => {
245
+            const rowData = item.categoryErrors.find(row => row.categoryName === attr.text) || { errorCount: 0 }
246
+            return rowData.errorCount
247
+          }),
248
+          color: color[ index % color.length ],
249
+          areaStyle: {
250
+            opacity: 0.5,
251
+            color: color[ index % color.length ],
252
+          },
253
+          lineStyle: {
254
+            width: 2
255
+          },
256
+          itemStyle: {
257
+            borderWidth: 2,
258
+            color: color[ index % color.length ],
259
+          }
260
+        }
261
+      })
262
+    })
263
+  }
264
+  const portraitParams = {}
265
+  if (props.type === 'department') {
266
+    portraitParams.checkedDepartmentId = params.value.deptId
267
+  } else if (props.type === 'team') {
268
+    portraitParams.checkedTeamId = params.value.deptId
269
+  } else if (props.type === 'personal') {
270
+    portraitParams.checkedUserId = params.value.deptId
271
+  } else {
272
+    portraitParams.checkedSiteId = params.value.deptId
273
+  }
274
+
275
+  params.value.deptId && getPortrait(portraitParams).then(res => {
276
+    portraitData.value = res.data || {}
277
+    const { checkLargeScreenCommonDtoList = [] } = res.data || {}
278
+    problemRadarDataHandler(checkLargeScreenCommonDtoList.filter(item => item && item.deptName !== '总数'))
279
+  })
280
+}, [ params ])
281
+
282
+</script>
283
+
284
+<style lang="scss" scoped>
285
+.cards-container {
286
+  display: flex;
287
+  align-items: center;
288
+  column-gap: 15px;
289
+  padding-bottom: 15px;
290
+
291
+  .card-item {
292
+    flex: 1;
293
+    color: #fff;
294
+    font-weight: bold;
295
+    font-size: 18px;
296
+    text-align: center;
297
+    background: url('../../../../../assets/images/cardBg.png') no-repeat;
298
+    background-size: 100% 100%;
299
+    display: flex;
300
+    flex-direction: column;
301
+    align-items: center;
302
+    justify-content: center;
303
+    row-gap: 10px;
304
+    border-radius: 5px;
305
+    font-weight: bold;
306
+    padding: 10px 5px;
307
+
308
+    div:nth-child(1) {
309
+      white-space: nowrap;
310
+      text-overflow: ellipsis;
311
+      overflow: hidden;
312
+      min-width: 0;
313
+    }
314
+
315
+    div:nth-child(2) {
316
+      font-size: 20px;
317
+      color: var(--color)
318
+    }
319
+  }
320
+}
321
+
322
+.charts-container {
323
+  display: flex;
324
+  flex: 1;
325
+
326
+  .charts-container-item {
327
+    flex: 1;
328
+    height: 100%;
329
+    display: flex;
330
+    flex-direction: column;
331
+  }
332
+
333
+  .charts-container-name {
334
+    color: #fff;
335
+    font-weight: bold;
336
+    font-size: 16px;
337
+    display: flex;
338
+    align-items: center;
339
+
340
+    &::before {
341
+      content: '';
342
+      display: inline-block;
343
+      border-left: 9px solid #1CB6FF;
344
+      border-top: 8px solid transparent;
345
+      border-right: 9px solid transparent;
346
+      border-bottom: 8px solid transparent;
347
+    }
348
+  }
349
+
350
+  .charts-container-content {
351
+    flex: 1;
352
+    width: 100%;
353
+  }
354
+}
355
+</style>

+ 58 - 0
src/views/dataBigScreen/dashboard/components/pageItems/StationTaskDetails.vue

@@ -0,0 +1,58 @@
1
+<template>
2
+  <ChartsContainer title="任务执行情况" name="站级任务明细">
3
+    <template #other>
4
+      <span style="margin-right: 10px; font-size: 14px; white-space: nowrap;">{{ tableData.taskName || '' }} 每日{{ tableData.ruleTypeNum || 1 }}次</span>
5
+      <CustomStyleSelect :options="selectList" v-model="selectActive"/>
6
+      
7
+    </template>
8
+    <template #describe>
9
+      完成情况:{{ `${tableData.notStartedCount || 0}/${tableData.inProgressCount || 0}/${tableData.completedCount || 0}` }}
10
+    </template>
11
+    <div ref="content" style="height: 100%;">
12
+      <el-table v-auto-scroll :data="tableData.checkLargeScreenInspectionExecuteItemDtoList" border style="width: 100%" height="100%">
13
+        <el-table-column prop="userName" label="姓名" width="90" />
14
+        <el-table-column prop="completionPercentage" label="今日完成比例">
15
+          <template #default="scope">
16
+            <el-progress
17
+              :text-inside="true"
18
+              :stroke-width="16"
19
+              :percentage="scope.row.completionPercentage"
20
+              style="--el-border-color-lighter: #fff"
21
+              color="#58A55C"
22
+            />
23
+          </template>
24
+        </el-table-column>
25
+        <el-table-column prop="checkOrderCount" label="整改单数" width="90" />
26
+        <el-table-column prop="rectificationOrderCount" label="不合格项数" width="100" />
27
+      </el-table>
28
+    </div>
29
+  </ChartsContainer>
30
+</template>
31
+
32
+<script setup>
33
+import CustomStyleSelect from './CustomStyleSelect.vue'
34
+import ChartsContainer from '../ChartsContainer.vue';
35
+import { computed,  ref} from 'vue';
36
+
37
+const props = defineProps({
38
+  tableData: {
39
+    type: Array,
40
+    default: []
41
+  }
42
+})
43
+const selectActive = ref('')
44
+const selectList = computed(() => { 
45
+  const options = props.tableData.map(item => {
46
+    return ({ label: item.taskName, value: item.taskCode })
47
+  }) 
48
+  selectActive.value = options[0] ? options[0].value : ''
49
+  return options || []
50
+})
51
+
52
+const tableData = computed(() => {
53
+  return props.tableData.find(item => item.taskCode === selectActive.value) || {}
54
+})
55
+
56
+</script>
57
+
58
+<style lang="scss" scoped></style>

+ 209 - 0
src/views/dataBigScreen/dashboard/components/pageItems/SubjectiveImpression.vue

@@ -0,0 +1,209 @@
1
+<template>
2
+  <ChartsContainer title="主观印象">
3
+    <div v-if=" props.type === 'personal' " style="height: 100%;">
4
+      <div class="chartsContainer-content" v-for=" ( item, index ) in pageData " :key="item.name">
5
+        <div class="chartsContainer-content-top">
6
+          <div class="chartsContainer-content-name">{{ item.name }}</div>
7
+        </div>
8
+        <div class="chartsContainer-content-content">
9
+          <el-tag
10
+            v-for=" attr in item.children "
11
+            :key="attr.name"
12
+            :color="colorList[ index % 5 ]"
13
+            size="large"
14
+            effect="dark">
15
+            {{ attr.name }} ( {{ attr.count }} )
16
+          </el-tag>
17
+        </div>
18
+      </div>
19
+    </div>
20
+    <div v-else ref="content" style="height: 100%;" />
21
+  </ChartsContainer>
22
+</template>
23
+
24
+<script setup>
25
+import ChartsContainer from '../ChartsContainer.vue';
26
+import { computed, ref } from 'vue';
27
+import { useTimeOut } from './useTimeOut';
28
+import { useECharts } from '@/hooks/useEcharts.js';
29
+import { getCollaborationProfile } from '@/api/item/items'
30
+const props = defineProps({
31
+  type: {
32
+    type: String,
33
+    default: '' // team, department, station
34
+  }
35
+})
36
+const content = ref(null)
37
+const pageData = ref([])
38
+const colorList = [
39
+  '#509430',
40
+  '#B99136',
41
+  '#157BC3',
42
+  '#9A60B4',
43
+  '#FC8452'
44
+]
45
+const chartData = ref({
46
+  outerData: [],
47
+  innerData: []
48
+})
49
+const params = inject('provideParams')
50
+const setOptions = computed(() => {
51
+  return {
52
+    tooltip: {
53
+      trigger: 'item',
54
+      formatter: '{b}: {c}次 ({d}%)'
55
+    },
56
+    legend: {
57
+      show: false
58
+    },
59
+    series: [
60
+      {
61
+        type: 'pie',
62
+        selectedMode: 'single',
63
+        radius: [ 0, '50%' ],
64
+        label: {
65
+          position: 'inner',
66
+          fontSize: 14,
67
+          fontWeight: 'bold',
68
+          show: true,
69
+          formatter: function (params) {
70
+            // 对四个字的中文分类名称进行折行处理
71
+            const name = params.name;
72
+            if (name && name.length === 4) {
73
+              return name.substring(0, 2) + '\n' + name.substring(2, 4);
74
+            }
75
+            return name;
76
+          }
77
+        },
78
+        labelLine: {
79
+          show: false
80
+        },
81
+        itemStyle: {
82
+          borderWidth: 2,
83
+          borderColor: '#fff'
84
+        },
85
+        data: chartData.value.innerData
86
+      },
87
+      {
88
+        type: 'pie',
89
+        radius: [ '55%', '75%' ],
90
+        label: {
91
+          position: 'outer',
92
+          show: true,
93
+          color: '#fff',
94
+          fontWeight: 'bold',
95
+          formatter: '{b}: {c}'
96
+        },
97
+        labelLine: {
98
+          show: true,
99
+          length: 10,
100
+          length2: 10
101
+        },
102
+        itemStyle: {
103
+          borderWidth: 2,
104
+          borderColor: '#fff'
105
+        },
106
+        data: chartData.value.outerData
107
+      }
108
+    ]
109
+  }
110
+})
111
+props.type !== 'personal' && useECharts(content, setOptions)
112
+useTimeOut(() => {
113
+  if (!params.value.deptId) return
114
+  if (props.type === 'personal') {
115
+    getCollaborationProfile({ userId: params.value.deptId }).then(res => {
116
+      const { subjectiveImpressionList = [] } = res.data
117
+      pageData.value = subjectiveImpressionList
118
+    })
119
+  } else {
120
+    getCollaborationProfile({ deptId: params.value.deptId }).then(res => {
121
+      const { subjectiveImpressionList = [] } = res.data
122
+      chartData.value = subjectiveImpressionList.reduce((cur, acc) => {
123
+        cur.innerData.push({
124
+          value: acc.count || 0,
125
+          name: acc.name
126
+        })
127
+        // 子项数据
128
+        if (acc.children && acc.children.length > 0) {
129
+          acc.children.forEach(child => {
130
+            cur.outerData.push({
131
+              value: child.count || 0,
132
+              name: child.name
133
+            });
134
+          });
135
+        }
136
+        return cur
137
+      }, {
138
+        outerData: [],
139
+        innerData: []
140
+      })
141
+    })
142
+  }
143
+
144
+}, [ params ])
145
+</script>
146
+
147
+<style lang="scss" scoped>
148
+.chartsContainer-content {
149
+  padding: 15px;
150
+  box-sizing: border-box;
151
+  display: flex;
152
+  width: 100%;
153
+  flex: 1;
154
+  flex-direction: column;
155
+  row-gap: 10px;
156
+  overflow: hidden;
157
+
158
+  .chartsContainer-content-top {
159
+    display: flex;
160
+    align-items: center;
161
+    justify-content: space-between;
162
+    color: #78DEF5;
163
+
164
+    .chartsContainer-content-name {
165
+      color: #fff;
166
+      font-weight: bold;
167
+      font-size: 16px;
168
+      display: flex;
169
+      align-items: center;
170
+
171
+      &::before {
172
+        content: '';
173
+        display: inline-block;
174
+        border-left: 9px solid #1CB6FF;
175
+        border-top: 8px solid transparent;
176
+        border-right: 9px solid transparent;
177
+        border-bottom: 8px solid transparent;
178
+      }
179
+    }
180
+
181
+    .chartsContainer-content-other {
182
+      display: flex;
183
+      align-items: center;
184
+    }
185
+  }
186
+
187
+  .chartsContainer-content-describe {
188
+    color: #78DEF5;
189
+    font-size: 12px;
190
+    font-weight: bold;
191
+    display: flex;
192
+    align-items: center;
193
+  }
194
+
195
+  .chartsContainer-content-content {
196
+    flex: 1;
197
+    width: 100%;
198
+    overflow-y: auto;
199
+    overflow-x: hidden;
200
+    display: flex;
201
+    flex-wrap: wrap;
202
+    column-gap: 10px;
203
+    row-gap: 10px;
204
+    :deep(.el-tag__content) {
205
+      font-weight: bold;
206
+    }
207
+  }
208
+}
209
+</style>

+ 150 - 0
src/views/dataBigScreen/dashboard/components/pageItems/WorkDetails.vue

@@ -0,0 +1,150 @@
1
+<template>
2
+  <div class="chartsContainer">
3
+    <div class="chartsContainer-title">明细情况</div>
4
+    <div class="chartsContainer-list">
5
+      <div class="chartsContainer-content" v-for="(item, index) in detailsInfo " :key="item.name">
6
+        <div class="chartsContainer-content-top">
7
+          <div class="chartsContainer-content-name">{{ item.name }}</div>
8
+        </div>
9
+        <div class="chartsContainer-content-describe">
10
+          <CustomStyleSegmented v-model="item.stateActive" :options="item.stateOptions" />
11
+          <CustomStyleSelect style="margin-left: auto; margin-right: 10px; width: 140px;" :options="item.options"
12
+            v-model="item.active" @change="(row) => dataChange(row, index)"/>
13
+          <span>开放数量:{{ item.onDutyChannelCount || 0 }}</span>
14
+        </div>
15
+        <div class="chartsContainer-content-content">
16
+          <div style="height: 100%; overflow: hidden;" v-if="item.stateActive === item.stateOptions[0]">
17
+            <el-table v-auto-scroll :data="item.data"
18
+              border style="width: 100%" height="100%">
19
+              <el-table-column prop="channelName" label="通道名称" width="90">
20
+                <template #default=" scope ">
21
+                  {{ scope.row.channelName.split( '/' ).at( -1 ) }}
22
+                </template>
23
+              </el-table-column>
24
+              <el-table-column prop="onDutyTeamName" label="上岗班组" />
25
+              <el-table-column prop="onDutyTime" label="上岗时间" width="130" />
26
+              <el-table-column prop="onDutyCount" label="在岗人数" width="80" />
27
+            </el-table>
28
+          </div>
29
+          <div style="height: 100%; overflow: hidden;" v-if="item.stateActive === item.stateOptions[1]">
30
+            <el-table v-auto-scroll :data="item.waitList"
31
+              border style="width: 100%" height="100%">
32
+              <el-table-column prop="waitTeamName" label="待岗班组" width="100"/>
33
+              <el-table-column prop="waitCount" label="待岗人数" width="80" />
34
+              <el-table-column prop="checkOutTime" label="下通道时间" />
35
+            </el-table>
36
+          </div>
37
+        </div>
38
+      </div>
39
+    </div>
40
+  </div>
41
+</template>
42
+
43
+<script setup>
44
+import CustomStyleSelect from './CustomStyleSelect.vue'
45
+import CustomStyleSegmented from './CustomStyleSegmented.vue'
46
+
47
+
48
+const detailsInfo = defineModel({
49
+  default: []
50
+})
51
+
52
+const dataChange = (row, index) => {
53
+  detailsInfo.value[index].data = row.data
54
+}
55
+
56
+</script>
57
+
58
+<style lang="scss" scoped>
59
+.chartsContainer {
60
+  width: 100%;
61
+  height: 100%;
62
+  position: relative;
63
+  background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
64
+  border-radius: 4px;
65
+  overflow: hidden;
66
+  display: flex;
67
+  flex-direction: column;
68
+
69
+  .chartsContainer-title {
70
+    height: 42px;
71
+    width: 100%;
72
+    background: linear-gradient(to right, #004387 0%, #090B18 100%);
73
+    border-left: 1px solid #1892CE;
74
+    line-height: 42px;
75
+    color: #fff;
76
+    font-weight: 900;
77
+    font-size: 20px;
78
+    text-indent: 1em;
79
+  }
80
+
81
+  .chartsContainer-list {
82
+    padding: 20px 15px 10px;
83
+    box-sizing: border-box;
84
+    display: flex;
85
+    column-gap: 20px;
86
+    flex: 1;
87
+    overflow: hidden;
88
+  }
89
+
90
+  .chartsContainer-content {
91
+    display: flex;
92
+    flex-direction: column;
93
+    row-gap: 10px;
94
+    flex: 1;
95
+    overflow: hidden;
96
+
97
+    .chartsContainer-content-top {
98
+      display: flex;
99
+      align-items: center;
100
+      color: #78DEF5;
101
+
102
+      .chartsContainer-content-name {
103
+        color: #fff;
104
+        font-weight: bold;
105
+        font-size: 16px;
106
+        display: flex;
107
+        align-items: center;
108
+
109
+        &::before {
110
+          content: '';
111
+          display: inline-block;
112
+          border-left: 9px solid #1CB6FF;
113
+          border-top: 8px solid transparent;
114
+          border-right: 9px solid transparent;
115
+          border-bottom: 8px solid transparent;
116
+        }
117
+      }
118
+    }
119
+
120
+    .chartsContainer-content-describe {
121
+      color: #78DEF5;
122
+      font-size: 12px;
123
+      font-weight: bold;
124
+      display: flex;
125
+      align-items: center;
126
+    }
127
+
128
+    .chartsContainer-content-content {
129
+      flex: 1;
130
+      overflow: hidden;
131
+      --el-bg-color: transparent;
132
+      --el-fill-color-blank: transparent;
133
+      --el-border-color-lighter: #1887A6;
134
+      --el-text-color-regular: #fff;
135
+      --el-fill-color-light: #5e93a1;
136
+
137
+      :deep(.el-table) {
138
+        .el-table__header-wrapper th {
139
+          background-color: #1CA7C8 !important;
140
+          color: #fff;
141
+        }
142
+
143
+        .cell {
144
+          font-weight: bold;
145
+        }
146
+      }
147
+    }
148
+  }
149
+}
150
+</style>

+ 141 - 0
src/views/dataBigScreen/dashboard/components/pageItems/WorkOutput.vue

@@ -0,0 +1,141 @@
1
+<template>
2
+  <ChartsContainer title="工作产出">
3
+    <div class="content-row" v-if=" type ">
4
+      <div class="content-row-list">
5
+        <div class="content-row-item">
6
+          <div>查获排名</div>
7
+          <div>{{ rank.rank || 0 }} / {{ rank.rankCount || 0 }}</div>
8
+        </div>
9
+        <div class="content-row-item">
10
+          <div>有效查获总数</div>
11
+          <div>{{ rowTableItem.effectiveSeizureCount.totalCount }}</div>
12
+        </div>
13
+        <div class="content-row-item">
14
+          <div>移交公安总数</div>
15
+          <div>{{ rowTableItem.referToPoliceCount.count }}</div>
16
+        </div>
17
+        <div class="content-row-item">
18
+          <div>故意隐匿总数</div>
19
+          <div>{{ rowTableItem.willfulConcealmentCount.count }}</div>
20
+        </div>
21
+      </div>
22
+    </div>
23
+    <div v-else ref="content" style="height: 100%;">
24
+      <el-table v-auto-scroll :data="tableData" border style="width: 100%" height="100%">
25
+        <el-table-column prop="deptName" label="统计维度" min-width="100" />
26
+        <el-table-column prop="effectiveSeizureCount" label="有效查获总数" />
27
+        <el-table-column prop="policeTotal" label="移交公安总数" min-width="100" />
28
+        <el-table-column prop="concealTotal" label="故意隐匿总数" min-width="100" />
29
+      </el-table>
30
+    </div>
31
+  </ChartsContainer>
32
+</template>
33
+
34
+<script setup>
35
+import ChartsContainer from '../ChartsContainer.vue';
36
+import { ref } from 'vue';
37
+import { getSiteStatistics, getModuleMetrics, getRankInfo } from '@/api/item/items'
38
+import { useTimeOut } from '../pageItems/useTimeOut'
39
+const props = defineProps({
40
+  type: {
41
+    type: String,
42
+    default: '', // department | team
43
+  },
44
+})
45
+const tableData = ref([])
46
+const rowTableItem = ref({
47
+  effectiveSeizureCount: {},
48
+  referToPoliceCount: {},
49
+  willfulConcealmentCount: {},
50
+})
51
+const rank = ref({})
52
+const params = inject('provideParams')
53
+useTimeOut(() => {
54
+  !props.type && params.value.deptId && getSiteStatistics({ deptId: params.value.deptId }).then(res => {
55
+    tableData.value = res.data
56
+  })
57
+  let apiParams = {}
58
+  if (props.type === 'personal') {
59
+    apiParams = { userId: params.value.deptId }
60
+  } else {
61
+    apiParams = { deptId: params.value.deptId }
62
+  }
63
+  props.type && params.value.deptId && getModuleMetrics({ ...apiParams, userTypeStr: props.type }).then(res => {
64
+    const { moduleResults } = res.data
65
+    if (!moduleResults || Object.keys(moduleResults).length === 0) {
66
+      moduleResults = {
67
+        item: {
68
+          effectiveSeizureCount: {},
69
+          referToPoliceCount: {},
70
+          willfulConcealmentCount: {},
71
+        }
72
+      }
73
+    }
74
+
75
+    if (props.type === 'personal') {
76
+      rowTableItem.value = {
77
+        effectiveSeizureCount: {
78
+          totalCount: moduleResults.item.effectiveSeizureCount
79
+        },
80
+        referToPoliceCount: {
81
+          count: moduleResults.item.referToPoliceCount
82
+        },
83
+        willfulConcealmentCount: {
84
+          count: moduleResults.item.willfulConcealmentCount
85
+        },
86
+      }
87
+    } else {
88
+      rowTableItem.value = moduleResults.item
89
+
90
+    }
91
+
92
+  })
93
+  props.type && params.value.deptId && getRankInfo({ ...apiParams, level: 'station' }).then(res => {
94
+    rank.value = res.data || {};
95
+  })
96
+}, [ params ])
97
+
98
+
99
+</script>
100
+
101
+<style lang="scss" scoped>
102
+.content-row {
103
+  display: flex;
104
+  flex-direction: column;
105
+  height: 100%;
106
+
107
+  .content-row-title {
108
+    font-size: 14px;
109
+    color: #77DBF4;
110
+    height: 30px;
111
+  }
112
+
113
+  .content-row-list {
114
+    display: flex;
115
+    flex: 1;
116
+    row-gap: 30px;
117
+    column-gap: 30px;
118
+    flex-wrap: wrap;
119
+  }
120
+
121
+  .content-row-item {
122
+    width: calc(50% - 15px);
123
+    height: calc(50% - 15px);
124
+    background: url('../../../../../assets/images/cardBg.png') no-repeat;
125
+    background-size: 100% 100%;
126
+    display: flex;
127
+    flex-direction: column;
128
+    justify-content: space-evenly;
129
+    align-items: center;
130
+    color: #fff;
131
+    font-size: 16px;
132
+    border-radius: 5px;
133
+    padding: 0 15px;
134
+    font-weight: bold;
135
+
136
+    &>div:nth-child(2) {
137
+      font-size: 24px;
138
+    }
139
+  }
140
+}
141
+</style>

+ 540 - 0
src/views/dataBigScreen/dashboard/components/pageItems/WorkPortrait.vue

@@ -0,0 +1,540 @@
1
+<template>
2
+  <div class="chartsContainer">
3
+    <div class="chartsContainer-title">工作画像</div>
4
+    <div class="chartsContainer-list">
5
+      <div class="chartsContainer-content">
6
+        <div class="chartsContainer-content-top">
7
+          <div class="chartsContainer-content-name">通道开放趋势图</div>
8
+        </div>
9
+        <div class="chartsContainer-content-content" ref="channelChart" />
10
+      </div>
11
+
12
+      <div class="chartsContainer-content">
13
+        <div class="chartsContainer-content-top">
14
+          <div class="chartsContainer-content-name">查获趋势图</div>
15
+        </div>
16
+        <div class="chartsContainer-content-content" ref="seizure" />
17
+      </div>
18
+
19
+      <div class="chartsContainer-content">
20
+        <div class="chartsContainer-content-top">
21
+          <div class="chartsContainer-content-name">抽问抽答完成率趋势图</div>
22
+        </div>
23
+        <div class="chartsContainer-content-content" ref="levelRateChart" />
24
+      </div>
25
+
26
+      <div class="chartsContainer-content">
27
+        <div class="chartsContainer-content-top">
28
+          <div class="chartsContainer-content-name">查获审批时长统计</div>
29
+        </div>
30
+        <div class="chartsContainer-content-content">
31
+          <el-table v-auto-scroll :data="tableData" border style="width: 100%" height="100%">
32
+            <el-table-column prop="empty" label="指标" min-width="140" />
33
+            <el-table-column prop="duration" label="时长" />
34
+          </el-table>
35
+        </div>
36
+      </div>
37
+    </div>
38
+  </div>
39
+</template>
40
+
41
+<script setup>
42
+import { getChannelOpenTrendChart, getDurationChart, getSeizureTrendChart, getStationLevelRate, getDepartmentLevelRate } from '@/api/item/items'
43
+import { useTimeOut } from './useTimeOut';
44
+import { useECharts } from '@/hooks/useEcharts.js';
45
+import { ref, computed, reactive, inject } from 'vue'
46
+const dataZoom = [
47
+  {
48
+    type: 'slider', // 滑动条型数据区域缩放组件
49
+    show: true,
50
+    xAxisIndex: [ 0 ],
51
+    start: 0,
52
+    end: 100,
53
+    height: 20,
54
+    bottom: 10,
55
+    backgroundColor: '#f5f5f5',
56
+    dataBackground: {
57
+      lineStyle: {
58
+        color: '#00BCD4',
59
+        width: 1,
60
+        opacity: 0.5
61
+      },
62
+      areaStyle: {
63
+        color: 'rgba(0, 188, 212, 0.1)'
64
+      }
65
+    },
66
+    fillerColor: 'rgba(0, 188, 212, 0.2)',
67
+    borderColor: '#ddd',
68
+    textStyle: {
69
+      color: '#666'
70
+    }
71
+  },
72
+  {
73
+    type: 'inside', // 内置型数据区域缩放组件
74
+    xAxisIndex: [ 0 ],
75
+    start: 0,
76
+    end: 100,
77
+    zoomOnMouseWheel: true, // 支持鼠标滚轮缩放
78
+    moveOnMouseMove: true, // 支持鼠标拖拽平移
79
+    moveOnMouseWheel: false
80
+  }
81
+]
82
+const provideParams = inject('provideParams')
83
+const channelChart = ref(null)
84
+const channelChartData = ref([])
85
+const setChannelChartOptions = computed(() => {
86
+  return {
87
+    tooltip: {
88
+      trigger: 'axis',
89
+      formatter: function (params) {
90
+        let result = `日期: ${params[ 0 ].name}<br/>`
91
+        params.forEach(param => {
92
+          let value = param.value
93
+          if (param.seriesName === '开放时长') {
94
+            value = `${param.value}小时`
95
+          } else if (param.seriesName === '平均线') {
96
+            value = `平均值: ${param.value}`
97
+          }
98
+          result += `${param.marker} ${param.seriesName}: ${value}<br/>`
99
+        })
100
+        return result
101
+      }
102
+    },
103
+    legend: {
104
+      data: [ '通道开放数', '开放时长', '平均线' ],
105
+      left: 'center',
106
+      textStyle: {
107
+        fontSize: 12,
108
+        color: '#fff'
109
+      }
110
+    },
111
+    xAxis: {
112
+      type: 'category',
113
+      data: channelChartData.value.map(item => item.date),
114
+      axisLabel: {
115
+        rotate: 45, // 日期标签旋转45度,避免重叠
116
+        fontSize: 12,
117
+        color: '#fff'
118
+      }
119
+    },
120
+    yAxis: [
121
+      {
122
+        type: 'value',
123
+        name: '通道数',
124
+        position: 'left',
125
+        min: 0,
126
+        axisLine: {
127
+          lineStyle: {
128
+            color: '#fff'
129
+          }
130
+        },
131
+        axisLabel: {
132
+          formatter: '{value}',
133
+          color: "#fff"
134
+        }
135
+      },
136
+      {
137
+        type: 'value',
138
+        name: '时长(小时)',
139
+        position: 'right',
140
+        axisLine: {
141
+          lineStyle: {
142
+            color: '#fff'
143
+          }
144
+        },
145
+        axisLabel: {
146
+          formatter: '{value}',
147
+          color: "#fff"
148
+        }
149
+      }
150
+    ],
151
+    grid: {
152
+      left: '5%',
153
+      right: '5%',
154
+      top: '20%', // 增加顶部间距,降低y轴高度
155
+      bottom: '15%', // 为缩放控件留出空间
156
+      containLabel: true
157
+    },
158
+    dataZoom: dataZoom,
159
+    series: [
160
+      {
161
+        name: '通道开放数',
162
+        data: channelChartData.value.map(item => item.openCount || 0),
163
+        type: 'line',
164
+        smooth: true,
165
+        yAxisIndex: 0,
166
+        lineStyle: {
167
+          color: '#4CAF50',
168
+          width: 2
169
+        },
170
+        itemStyle: {
171
+          color: '#4CAF50'
172
+        },
173
+        areaStyle: {
174
+          color: {
175
+            type: 'linear',
176
+            x: 0,
177
+            y: 0,
178
+            x2: 0,
179
+            y2: 1,
180
+            colorStops: [ {
181
+              offset: 0,
182
+              color: 'rgba(76, 175, 80, 0.3)'
183
+            }, {
184
+              offset: 1,
185
+              color: 'rgba(76, 175, 80, 0.1)'
186
+            } ]
187
+          }
188
+        }
189
+      },
190
+      {
191
+        name: '开放时长',
192
+        data: channelChartData.value.map(item => item.openDuration || 0),
193
+        type: 'line',
194
+        smooth: true,
195
+        yAxisIndex: 1,
196
+        lineStyle: {
197
+          color: '#2196F3',
198
+          width: 2
199
+        },
200
+        itemStyle: {
201
+          color: '#2196F3'
202
+        }
203
+      },
204
+      {
205
+        name: '平均线',
206
+        data: channelChartData.value.map(item => item.avg), // 为每个数据点创建相同的平均值
207
+        type: 'line',
208
+        yAxisIndex: 0,
209
+        lineStyle: {
210
+          color: '#FF9800',
211
+          width: 2,
212
+          type: 'dashed' // 虚线表示平均线
213
+        },
214
+        itemStyle: {
215
+          color: '#FF9800'
216
+        },
217
+        symbol: 'none', // 不显示数据点
218
+        label: {
219
+          show: true,
220
+          position: 'end',
221
+          formatter: `平均值: {c}`
222
+        }
223
+      }
224
+    ]
225
+  }
226
+})
227
+useECharts(channelChart, setChannelChartOptions);
228
+
229
+const seizure = ref(null)
230
+const seizureData = ref([])
231
+const setSeizureOptions = computed(() => {
232
+  return {
233
+    tooltip: {
234
+      trigger: 'axis',
235
+      formatter: function (params) {
236
+        const data = params[ 0 ]
237
+        return `日期: ${data.name}<br/>查获数量: ${data.value}`
238
+      }
239
+    },
240
+    xAxis: {
241
+      type: 'category',
242
+      data: seizureData.value.map(item => item.date),
243
+      axisLine: {
244
+        lineStyle: {
245
+          color: '#fff'
246
+        }
247
+      },
248
+      axisLabel: {
249
+        color: '#fff',
250
+        rotate: 45,
251
+        fontSize: 12,
252
+      }
253
+    },
254
+    yAxis: {
255
+      name: '查获数量',
256
+      type: 'value',
257
+      min: 0,
258
+      max: function () {
259
+        // 计算最大值并向上取整到最近的整数,确保五等分
260
+        const maxTotal = Math.max(...seizureData.value.map(item => item.total || 0), 5);
261
+        return Math.ceil(maxTotal / 5) * 5;
262
+      }(),
263
+      interval: function () {
264
+        // 固定为五等分
265
+        const maxTotal = Math.max(...seizureData.value.map(item => item.total || 0), 5);
266
+        return Math.ceil(maxTotal / 5);
267
+      }(),
268
+      axisLine: {
269
+        lineStyle: {
270
+          color: '#fff'
271
+        }
272
+      },
273
+      axisLabel: {
274
+        color: '#fff'
275
+      },
276
+      splitLine: {
277
+        lineStyle: {
278
+          color: '#fff'
279
+        }
280
+      }
281
+    },
282
+    grid: {
283
+      left: '5%',
284
+      right: '5%',
285
+      bottom: '15%', // 为缩放控件留出空间
286
+      containLabel: true
287
+    },
288
+    dataZoom: dataZoom,
289
+    series: [ {
290
+      name: '查获数量',
291
+      type: 'line',
292
+      smooth: true,
293
+      data: seizureData.value.map(item => item.total || 0),
294
+      lineStyle: {
295
+        width: 3,
296
+        color: '#D45046'
297
+      },
298
+      itemStyle: {
299
+        color: '#D45046'
300
+      },
301
+      areaStyle: {
302
+        color: {
303
+          type: 'linear',
304
+          x: 0,
305
+          y: 0,
306
+          x2: 0,
307
+          y2: 1,
308
+          colorStops: [ {
309
+            offset: 0,
310
+            color: 'rgba(255, 107, 107, 0.3)'
311
+          }, {
312
+            offset: 1,
313
+            color: 'rgba(255, 107, 107, 0.1)'
314
+          } ]
315
+        }
316
+      }
317
+    } ]
318
+  }
319
+})
320
+useECharts(seizure, setSeizureOptions);
321
+
322
+const selectedLevelType = ref('station')
323
+const levelRateChart = ref(null)
324
+const levelRateChartData = ref([])
325
+const setLevelRateChartOptions = computed(() => {
326
+  return {
327
+    tooltip: {
328
+      trigger: 'axis',
329
+      formatter: function (params) {
330
+        const data = params[ 0 ]
331
+        return `${data.name}<br/>已完成任务: ${data.value}`
332
+      }
333
+    },
334
+    xAxis: {
335
+      type: 'category',
336
+      data: levelRateChartData.value.map(item => item.date),
337
+      axisLabel: {
338
+        color: '#fff',
339
+        rotate: 45,
340
+        fontSize: 12,
341
+      },
342
+      axisTick: {
343
+        alignWithLabel: true
344
+      }
345
+    },
346
+    yAxis: {
347
+      name: '已完成任务',
348
+      type: 'value',
349
+      axisLine: {
350
+        lineStyle: {
351
+          color: '#fff'
352
+        }
353
+      },
354
+      axisLabel: {
355
+        color: '#fff'
356
+      },
357
+      splitLine: {
358
+        lineStyle: {
359
+          color: '#fff'
360
+        }
361
+      }
362
+    },
363
+    dataZoom: dataZoom,
364
+    series: [ {
365
+      name: '已完成任务',
366
+      data: levelRateChartData.value.map(item => item.completedTasks),
367
+      type: 'line',
368
+      smooth: true,
369
+      lineStyle: {
370
+        color: '#1CA7C8',
371
+        width: 3
372
+      },
373
+      itemStyle: {
374
+        color: '#1CA7C8'
375
+      },
376
+      areaStyle: {
377
+        color: 'rgba(0, 188, 212, 0.3)'
378
+      }
379
+    } ],
380
+    grid: {
381
+      left: '5%',
382
+      right: '5%',
383
+      bottom: '15%', // 增加底部空间给数据缩放组件
384
+      containLabel: true
385
+    }
386
+  }
387
+})
388
+useECharts(levelRateChart, setLevelRateChartOptions);
389
+
390
+//获取抽问抽答完成率
391
+const getLevelRateData = (paramsObj) => {
392
+  let params = {
393
+    startDate: paramsObj.startDate,
394
+    endDate: paramsObj.endDate,
395
+    deptId: paramsObj.inspectDepartmentId
396
+  }
397
+  selectedLevelType.value = paramsObj.inspectDepartmentId && String(paramsObj.inspectDepartmentId) !== 'ALL' ? '' : 'station' 
398
+  let api = selectedLevelType.value == 'station' ? getStationLevelRate : getDepartmentLevelRate
399
+  if (selectedLevelType.value == 'station') {
400
+    delete params.deptId
401
+    params.siteId = 100
402
+  }
403
+  api(params).then(res => {
404
+    levelRateChartData.value = res.data || []
405
+  })
406
+}
407
+
408
+const tableData = ref([])
409
+useTimeOut(() => {
410
+  provideParams.value.deptId && getChannelOpenTrendChart({
411
+    startTime: provideParams.value.startDate,
412
+    endTime: provideParams.value.endDate,
413
+    deptId: provideParams.value.deptId === 'ALL' ? '' : provideParams.value.deptId
414
+  }).then(res => {
415
+    channelChartData.value = (res.trend || []).map(item => {
416
+      return {
417
+        ...item,
418
+        avg: res.avg || 0
419
+      }
420
+    })
421
+  })
422
+  provideParams.value.deptId && getSeizureTrendChart({
423
+    startTime: provideParams.value.startDate,
424
+    endTime: provideParams.value.endDate,
425
+    deptId: provideParams.value.deptId === 'ALL' ? '' : provideParams.value.deptId
426
+  }).then(res => {
427
+    seizureData.value = res.data || []
428
+  })
429
+  provideParams.value.deptId && getLevelRateData(provideParams.value)
430
+  provideParams.value.deptId && getDurationChart({
431
+    startTime: provideParams.value.startDate,
432
+    endTime: provideParams.value.endDate,
433
+    deptId: provideParams.value.deptId === 'ALL' ? '' : provideParams.value.deptId
434
+  }).then(res => {
435
+    const data = res.data || {}
436
+    tableData.value = [
437
+      { empty: '平均审批时长', duration: data.averageDurationText || '-' },
438
+      { empty: '最长审批时常', duration: data.maxDurationText || '-' },
439
+      { empty: '最短审批时常', duration: data.minDurationText || '-' }
440
+    ]
441
+  })
442
+}, [provideParams])
443
+
444
+
445
+
446
+</script>
447
+
448
+<style lang="scss" scoped>
449
+.chartsContainer {
450
+  width: 100%;
451
+  height: 100%;
452
+  position: relative;
453
+  background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
454
+  border-radius: 4px;
455
+  overflow: hidden;
456
+  display: flex;
457
+  flex-direction: column;
458
+
459
+  .chartsContainer-title {
460
+    height: 42px;
461
+    width: 100%;
462
+    background: linear-gradient(to right, #004387 0%, #090B18 100%);
463
+    border-left: 1px solid #1892CE;
464
+    line-height: 42px;
465
+    color: #fff;
466
+    font-weight: 900;
467
+    font-size: 20px;
468
+    text-indent: 1em;
469
+  }
470
+
471
+  .chartsContainer-list {
472
+    padding: 20px 15px 10px;
473
+    box-sizing: border-box;
474
+    display: flex;
475
+    column-gap: 20px;
476
+    flex: 1;
477
+    overflow: hidden;
478
+  }
479
+
480
+  .chartsContainer-content {
481
+    display: flex;
482
+    flex-direction: column;
483
+    row-gap: 10px;
484
+    flex: 1;
485
+    overflow: hidden;
486
+
487
+    .chartsContainer-content-top {
488
+      display: flex;
489
+      align-items: center;
490
+      color: #78DEF5;
491
+
492
+      .chartsContainer-content-name {
493
+        color: #fff;
494
+        font-weight: bold;
495
+        font-size: 16px;
496
+        display: flex;
497
+        align-items: center;
498
+
499
+        &::before {
500
+          content: '';
501
+          display: inline-block;
502
+          border-left: 9px solid #1CB6FF;
503
+          border-top: 8px solid transparent;
504
+          border-right: 9px solid transparent;
505
+          border-bottom: 8px solid transparent;
506
+        }
507
+      }
508
+    }
509
+
510
+    .chartsContainer-content-describe {
511
+      color: #78DEF5;
512
+      font-size: 12px;
513
+      font-weight: bold;
514
+      display: flex;
515
+      align-items: center;
516
+    }
517
+
518
+    .chartsContainer-content-content {
519
+      flex: 1;
520
+      width: 100%;
521
+      --el-bg-color: transparent;
522
+      --el-fill-color-blank: transparent;
523
+      --el-border-color-lighter: #1887A6;
524
+      --el-text-color-regular: #fff;
525
+      --el-fill-color-light: #5e93a1;
526
+
527
+      :deep(.el-table) {
528
+        .el-table__header-wrapper th {
529
+          background-color: #1CA7C8 !important;
530
+          color: #fff;
531
+        }
532
+
533
+        .cell {
534
+          font-weight: bold;
535
+        }
536
+      }
537
+    }
538
+  }
539
+}
540
+</style>

+ 61 - 0
src/views/dataBigScreen/dashboard/components/pageItems/index.js

@@ -0,0 +1,61 @@
1
+import StationTaskDetails from './StationTaskDetails.vue'
2
+import SectionTaskDetails from './SectionTaskDetails.vue'
3
+import ClassTaskDetails from './ClassTaskDetails.vue'
4
+import DataViewLeft from './DataViewLeft.vue'
5
+import ChannelsAndPersonnel from './ChannelsAndPersonnel.vue'
6
+import ChannelOpeningSituation from './ChannelOpeningSituation.vue'
7
+import PersonnelOnDutySituation from './PersonnelOnDutySituation.vue'
8
+import WorkDetails from './WorkDetails.vue'
9
+import ClassTaskCompletionRank from './ClassTaskCompletionRank.vue'
10
+import DepartmentalTaskCompletionRank from './DepartmentalTaskCompletionRank.vue'
11
+import PersonalTaskCompletionRank from './PersonalTaskCompletionRank.vue'
12
+import InspectionTask from './InspectionTask.vue'
13
+import SeizeDetails from './SeizeDetails.vue'
14
+import ProblemDiscovery from './ProblemDiscovery.vue'
15
+import IssueRectification from './IssueRectification.vue'
16
+import WorkPortrait from './WorkPortrait.vue'
17
+
18
+import QualificationCapability from './QualificationCapability.vue'
19
+import QualificationCapabilityPersonal from './QualificationCapabilityPersonal.vue'
20
+import Collaboration from './Collaboration.vue'
21
+import CollaborationPersonal from './CollaborationPersonal.vue'
22
+import StandardExecution from './StandardExecution.vue'
23
+import SubjectiveImpression from './SubjectiveImpression.vue'
24
+import Attendance from './Attendance.vue'
25
+import WorkOutput from './WorkOutput.vue'
26
+import LearningGrowth from './LearningGrowth.vue'
27
+import DepartmentInfo from './DepartmentInfo.vue'
28
+import CustomStyleSelect from './CustomStyleSelect.vue'
29
+export {
30
+  // dataViewTabs1
31
+  StationTaskDetails, // 站级任务明细
32
+  SectionTaskDetails, // 科级任务明细
33
+  ClassTaskDetails, // 班组级任务明细
34
+  DataViewLeft, // 任务执行情况工作画像
35
+  ChannelOpeningSituation, // 通道开放情况
36
+  PersonnelOnDutySituation, // 人员在岗情况
37
+  ChannelsAndPersonnel, // 通道及人员情况
38
+  WorkDetails, // 明细情况
39
+  ClassTaskCompletionRank, // 班组任务完成排行
40
+  DepartmentalTaskCompletionRank, // 部门任务完成排行
41
+  PersonalTaskCompletionRank, // 个人任务完成排行
42
+  // dataViewTabs2
43
+  InspectionTask, // 巡检任务
44
+  SeizeDetails, // 查获明细
45
+  ProblemDiscovery, // 问题发现
46
+  IssueRectification, // 问题整改
47
+  WorkPortrait, // 工作画像
48
+  // dataViewTabs3
49
+  QualificationCapability, // 资质能力
50
+  QualificationCapabilityPersonal,
51
+  Collaboration, // 协同配合
52
+  CollaborationPersonal, //
53
+  StandardExecution, //标准执行
54
+  SubjectiveImpression, //主观印象
55
+  Attendance, // 出勤投入
56
+  WorkOutput, // 工作产出
57
+  LearningGrowth, // 学习成长
58
+  DepartmentInfo, // 大队明细
59
+  // components
60
+  CustomStyleSelect
61
+}

+ 33 - 0
src/views/dataBigScreen/dashboard/components/pageItems/useTimeOut.js

@@ -0,0 +1,33 @@
1
+
2
+import { ref, onMounted, onUnmounted, watch } from 'vue'
3
+
4
+export const useTimeOut = (func, depend = [], once = true) => {
5
+  const timeOutSelf = ref(null)
6
+
7
+  const invoker = () => {
8
+    
9
+    if (once) {
10
+      func()
11
+    } else {
12
+      func()
13
+      timeOutSelf.value = setTimeout(() => {
14
+        invoker()
15
+      }, 1000 * 60 * 5)
16
+    }
17
+  }
18
+
19
+  onMounted(() => {
20
+    if (depend.length) {
21
+      depend.forEach(item => {
22
+        watch(() => item.value, () => {
23
+          invoker()
24
+        }, { deep: true, immediate: true })
25
+      })
26
+    } else {
27
+      invoker()
28
+    }
29
+  })
30
+  onUnmounted(() => {
31
+    clearTimeout(timeOutSelf.value)
32
+  })
33
+}

+ 700 - 0
src/views/dataBigScreen/dashboard/components/pageView/HomePage.vue

@@ -0,0 +1,700 @@
1
+<template>
2
+  <div class="content">
3
+    <div class="grid-container">
4
+      <!-- 左边区域 -->
5
+      <div class="left-section">
6
+        <!-- 上层:预警提示和系统状态左右布局 -->
7
+        <div class="top-section">
8
+          <HomePageWarning />
9
+          <HomePageSystemStatus @time-range-change="handleTimeRangeChange" @date-change="handleDateChange" />
10
+        </div>
11
+
12
+        <!-- <div class="bottom-section"> -->
13
+          <!-- 下层:统计卡片 -->
14
+          <HomePageStats :type-detail-data="typeDetailData" @sort-change="handleSortChange" />
15
+        <!-- </div> -->
16
+      </div>
17
+
18
+      <!-- 右边区域 -->
19
+      <div class="right-section">
20
+        <HomePageOverview :time-range="selectedTimeRange" :start-date="startDate" :end-date="endDate"
21
+          :selected-role="selectedRole" />
22
+      </div>
23
+    </div>
24
+  </div>
25
+</template>
26
+
27
+<script setup>
28
+import { ref, reactive, onMounted, computed } from 'vue'
29
+import HomePageWarning from '../pageItems/HomePageWarning.vue'
30
+import HomePageSystemStatus from '../pageItems/HomePageSystemStatus.vue'
31
+import HomePageStats from '../pageItems/HomePageStats.vue'
32
+import HomePageOverview from '../pageItems/HomePageOverview.vue'
33
+import useUserStore from '@/store/modules/user'
34
+import oneIcon from '@/assets/icons/one.png'
35
+import twoIcon from '@/assets/icons/two.png'
36
+import threeIcon from '@/assets/icons/three.png'
37
+import {
38
+  getHomePage,
39
+  getSeizureReport,
40
+  getAttendanceStats,
41
+  getAccuracyStatistics,
42
+  getSeizureRanking,
43
+  getCheckRanking
44
+} from '@/api/largeScreen/largeScreen'
45
+
46
+const userStore = useUserStore()
47
+const userRole = userStore.roles
48
+const currentUser = userStore.userInfo
49
+
50
+// 角色选择相关(移植自home-new页面)
51
+const selectedRole = ref('individual')
52
+
53
+// 时间选择相关数据
54
+const selectedTimeRange = ref('year')
55
+const startDate = ref('')
56
+const endDate = ref('')
57
+
58
+// 数据存储
59
+const inspectionData = ref({})
60
+const seizeData = ref({})
61
+const attendanceStats = ref({})
62
+const accuracyStatistics = ref({})
63
+const rankData = ref({
64
+  seizeRank: 0,
65
+  seizeModalType: 0,
66
+  answerRank: 0,
67
+  answerModalType: 0,
68
+  checkRank: 0,
69
+  checkModalType: 0
70
+})
71
+
72
+// 排序状态管理
73
+const sortStates = ref({
74
+  '查获上报': { teamSortType: 'asc', deptSortType: 'asc' },
75
+  '抽问抽答': { teamSortType: 'asc', deptSortType: 'asc' },
76
+  '巡检': { teamSortType: 'asc', deptSortType: 'asc' }
77
+})
78
+
79
+// 角色判断逻辑(移植自home-new页面)
80
+const isTeamView = computed(() => {
81
+  return userRole.includes('banzuzhang')
82
+})
83
+
84
+const isIndividualView = computed(() => {
85
+  return userRole.includes('SecurityCheck')
86
+})
87
+
88
+// 判断是否为科长角色
89
+const isKezhang = computed(() => {
90
+  return userRole.includes('kezhang')
91
+})
92
+
93
+// 判断是否为测试角色
94
+const isTestRole = computed(() => {
95
+
96
+  return userRole.includes('test') || userRole.includes('zhijianke') || userRole.includes('kezhang') || userRole.includes('admin')
97
+})
98
+
99
+// TypeDetail组件数据(移植自home-new页面)
100
+const typeDetailData = computed(() => {
101
+  return [
102
+    {
103
+      title: '查获上报',
104
+      linkText: isTestRole.value ? "" : seizePendingCount.value ? `${seizePendingCount.value}条数据待处理` : '',
105
+      // link: isTeamView.value || isKezhang.value ? '/pages/myToDoList/index' : '/pages/voiceSubmissionDraft/index',
106
+      statTitle: '查获数量',
107
+      dividerIndex: 0,
108
+      dataItems: getSeizeDataItems(),
109
+      // rankList: getSeizeRankList(),
110
+      departmentRank: seizeData.value.stationMasterData && seizeData.value.stationMasterData.brigadeRankings?.map(item => ({ ...item,passRate: item.seizureCount?.toFixed(2), name: item.brigadeName })) || [],
111
+      teamRank: seizeData.value.stationMasterData && seizeData.value.stationMasterData.topThreeTeamRankings?.map(item => ({ ...item, passRate: item.seizureCount?.toFixed(2), name: item.teamName })) || [],
112
+      bottomTeamRank: [{ passRate: 0, name: '无' }],
113
+      teamSortType: sortStates.value['查获上报'].teamSortType,
114
+      deptSortType: sortStates.value['查获上报'].deptSortType,
115
+      bottomTeamRank: seizeData.value.stationMasterData && seizeData.value.stationMasterData.botomThreeTeamRankings.map(item => ({ ...item, passRate: item.seizureCount.toFixed(2), name: item.teamName })),
116
+    },
117
+    {
118
+      title: '抽问抽答',
119
+      linkText: isIndividualView.value ? accuracyStatistics.value.hasPendingTask ? '待答题' : '' : '',
120
+      // link: '/pages/daily-exam/task-list/index',
121
+      statTitle: '正确率',
122
+      dividerIndex: 0,
123
+      dataItems: getQuestionDataItems(),
124
+      // rankList: getQuestionRankList(),
125
+      departmentRank: accuracyStatistics.value.topDepts && accuracyStatistics.value.topDepts?.map(item => ({ ...item, passRate: item.accuracy?.toFixed(2) })) || [],
126
+      teamRank: accuracyStatistics.value.topTeamsInSite && accuracyStatistics.value.topTeamsInSite?.map(item => ({ ...item, passRate: item.accuracy?.toFixed(2) })) || [],
127
+      bottomTeamRank: accuracyStatistics.value.bottomTeamsInSite && accuracyStatistics.value.bottomTeamsInSite?.map(item => ({ ...item, passRate: item.accuracy?.toFixed(2)})) || [],
128
+      teamSortType: sortStates.value['抽问抽答'].teamSortType,
129
+      deptSortType: sortStates.value['抽问抽答'].deptSortType
130
+    },
131
+    {
132
+      title: '巡检',
133
+      linkText: isTestRole.value ? "" : inspectionData.value.toDoNumber ? `${inspectionData.value.toDoNumber}条待处理任务` : "",
134
+      // link: '/pages/myToDoList/index',
135
+      statTitle: '合格率',
136
+      dividerIndex: 0,
137
+      dataItems: getInspectionDataItems(),
138
+      completed: inspectionData.value.doneNumber,
139
+      pending: inspectionData.value.doingNumber,
140
+      // rankList: getRankList(),
141
+      departmentRank: inspectionData.value.brigadeRankingList && inspectionData.value.brigadeRankingList?.map(item => ({ ...item, passRate: (item.passRate * 100)?.toFixed(2) })) || [],
142
+      teamRank: inspectionData.value.teamRankingList && inspectionData.value.teamRankingList?.map(item => ({ ...item, passRate: (item.passRate * 100)?.toFixed(2) })) || [],
143
+      bottomTeamRank: [{ passRate: 0, name: '无' }],
144
+      teamSortType: sortStates.value['巡检'].teamSortType,
145
+      deptSortType: sortStates.value['巡检'].deptSortType,
146
+      bottomTeamRank: inspectionData.value.reverseTeamRankingList && inspectionData.value.reverseTeamRankingList.map(item => ({ ...item, passRate: (item.passRate * 100).toFixed(2) })),
147
+    }
148
+  ]
149
+})
150
+
151
+// 待处理数量计算
152
+const seizePendingCount = computed(() => {
153
+  if (isIndividualView.value) {
154
+    const { securityCheckerData } = seizeData.value;
155
+    const { pendingCount } = securityCheckerData || {};
156
+    return pendingCount;
157
+  } else if (userRole.includes('kezhang')) {
158
+    const { sectionMasterData } = seizeData.value;
159
+    const { pendingCount } = sectionMasterData || {};
160
+    return pendingCount;
161
+  } else if (isTeamView.value) {
162
+    const { teamLeaderData } = seizeData.value;
163
+    const { pendingCount } = teamLeaderData || {}
164
+    return pendingCount;
165
+  }
166
+  return 0
167
+})
168
+
169
+
170
+
171
+// 时间范围变化处理
172
+const handleTimeRangeChange = (range) => {
173
+  selectedTimeRange.value = range
174
+  if (range !== 'custom') {
175
+
176
+    const { startDate: newStartDate, endDate: newEndDate } = calculateDateRange(range)
177
+    startDate.value = newStartDate
178
+    endDate.value = newEndDate
179
+  }
180
+  refreshData()
181
+}
182
+
183
+// 日期变化处理
184
+const handleDateChange = (dates) => {
185
+  startDate.value = dates.startDate
186
+  endDate.value = dates.endDate
187
+  selectedTimeRange.value = 'custom'
188
+  refreshData()
189
+}
190
+
191
+// 计算日期范围(移植自home-new页面)
192
+const calculateDateRange = (timeRange) => {
193
+
194
+  const today = new Date();
195
+  const yesterday = new Date(today);
196
+  yesterday.setDate(today.getDate() - 1);
197
+
198
+  let originStartDate = new Date(yesterday);
199
+  let originEndDate = new Date(yesterday);
200
+
201
+  switch (timeRange) {
202
+    case 'week':
203
+      // 近一周:从7天前到昨天
204
+      originStartDate.setDate(yesterday.getDate() - 6);
205
+      break;
206
+    case 'month':
207
+      // 近一月:从30天前到昨天
208
+      originStartDate.setDate(yesterday.getDate() - 29);
209
+      break;
210
+    case 'quarter':
211
+      // 近三月:从90天前到昨天
212
+      originStartDate.setDate(yesterday.getDate() - 89);
213
+      break;
214
+    case 'halfYear':
215
+      // 近半年:从180天前到昨天
216
+      originStartDate.setDate(yesterday.getDate() - 179);
217
+      break;
218
+    case 'year':
219
+      // 近一年:从365天前到昨天
220
+      originStartDate.setDate(yesterday.getDate() - 364);
221
+      break;
222
+    case 'custom':
223
+      // 自定义时间:使用用户选择的日期
224
+      if (startDate.value && endDate.value) {
225
+        originStartDate = new Date(startDate.value);
226
+        originEndDate = new Date(endDate.value);
227
+      }
228
+      break;
229
+    default:
230
+      // 默认近一周
231
+      originStartDate.setDate(yesterday.getDate() - 364);
232
+  }
233
+
234
+  return {
235
+    startDate: formatDateForInput(originStartDate),
236
+    endDate: formatDateForInput(originEndDate)
237
+  };
238
+}
239
+// 格式化日期为输入框格式
240
+const formatDateForInput = (date) => {
241
+  const year = date.getFullYear();
242
+  const month = String(date.getMonth() + 1).padStart(2, '0');
243
+  const day = String(date.getDate()).padStart(2, '0');
244
+  return `${year}-${month}-${day}`;
245
+}
246
+// 数据获取方法(移植自home-new页面)
247
+const getInspectionData = async () => {
248
+  try {
249
+    let params = {}
250
+
251
+    if (isIndividualView.value) {
252
+      params = { userId: currentUser.userId }
253
+    } else {
254
+      params = { deptId: currentUser.deptId }
255
+    }
256
+
257
+    const { startDate: calcStartDate, endDate: calcEndDate } = calculateDateRange(selectedTimeRange.value)
258
+
259
+    params.startDate = calcStartDate
260
+    params.endDate = calcEndDate
261
+
262
+    const res = await getHomePage(params)
263
+
264
+    inspectionData.value = res.data || {}
265
+  } catch (error) {
266
+    console.error("获取巡检数据失败:", error)
267
+  }
268
+}
269
+
270
+const getSeizeData = async () => {
271
+  try {
272
+    let params = {}
273
+    const { startDate: calcStartDate, endDate: calcEndDate } = calculateDateRange(selectedTimeRange.value)
274
+    params.startDate = calcStartDate
275
+    params.endDate = calcEndDate
276
+
277
+    if (!isTestRole.value) {
278
+      params.dataSource = selectedRole.value
279
+    }
280
+
281
+    const res = await getSeizureReport(params)
282
+    seizeData.value = res.data || {}
283
+  } catch (error) {
284
+    console.error("获取查获数据失败:", error)
285
+  }
286
+}
287
+
288
+const fetchAttendanceStats = async () => {
289
+  try {
290
+    let params = {}
291
+    const { startDate: calcStartDate, endDate: calcEndDate } = calculateDateRange(selectedTimeRange.value)
292
+    params.startDate = calcStartDate
293
+    params.endDate = calcEndDate
294
+
295
+    const res = await getAttendanceStats(params)
296
+    attendanceStats.value = res.data || {}
297
+  } catch (error) {
298
+    console.error("获取考勤统计数据失败:", error)
299
+  }
300
+}
301
+
302
+const fetchAccuracyStatistics = async () => {
303
+  try {
304
+    let params = {}
305
+    const { startDate: calcStartDate, endDate: calcEndDate } = calculateDateRange(selectedTimeRange.value)
306
+    params.startDate = calcStartDate
307
+    params.endDate = calcEndDate
308
+    params.dataSource = selectedRole.value
309
+    const res = await getAccuracyStatistics(params)
310
+    accuracyStatistics.value = res.data || {}
311
+  } catch (error) {
312
+    console.error("获取抽问抽答数据失败:", error)
313
+  }
314
+}
315
+
316
+const getRank = async () => {
317
+  try {
318
+    let params = {}
319
+    const { startDate: calcStartDate, endDate: calcEndDate } = calculateDateRange(selectedTimeRange.value)
320
+    params.startDate = calcStartDate
321
+    params.endDate = calcEndDate
322
+    params.dataSource = selectedRole.value
323
+
324
+    let idObj = {}
325
+    if (isIndividualView.value) {
326
+      idObj = { userId: currentUser.userId }
327
+    } else {
328
+      idObj = { deptId: currentUser.deptId }
329
+    }
330
+
331
+    const apiRequests = [getSeizureRanking(params)]
332
+
333
+    if (!isTestRole.value) {
334
+      apiRequests.push(getCheckRanking({ ...params, ...idObj }))
335
+    }
336
+
337
+    const results = await Promise.allSettled(apiRequests)
338
+    const [seizureRes] = results
339
+
340
+    // 处理第一个promise的结果(查获排名)
341
+    const seizureData = seizureRes?.status === 'fulfilled' ? seizureRes.value?.data : null
342
+    rankData.value.seizeRank = seizureData && seizureData[0]?.rank || 0
343
+    rankData.value.seizeModalType = seizureData && seizureData[0]?.medalType || 0
344
+
345
+    if (results.length > 1) {
346
+      // 处理第二个promise的结果(检查排名)
347
+      const checkResult = results[1];
348
+
349
+      const checkData = checkResult?.status === 'fulfilled' ? checkResult.value?.data : {}
350
+      const { checkLargeScreenHomePageRankingDto = {}, dailyTaskAccuracyRankingDto = {} } = checkData
351
+      rankData.value.checkRank = checkLargeScreenHomePageRankingDto?.rank || 0
352
+      rankData.value.checkModalType = checkLargeScreenHomePageRankingDto?.medalType || 0
353
+      rankData.value.answerRank = dailyTaskAccuracyRankingDto?.rank || 0
354
+      rankData.value.answerModalType = dailyTaskAccuracyRankingDto?.medalType || 0
355
+    }
356
+  } catch (error) {
357
+    console.error("获取排名数据失败:", error)
358
+  }
359
+}
360
+
361
+
362
+
363
+// 数据项获取方法(完整版,移植自home-new页面)
364
+const getInspectionDataItems = () => {
365
+  const { personalPassRate, teamPassRate, departmentPassRate, stationPassRate, teamRankingList, departmentRankingList } = inspectionData.value;
366
+  if (isIndividualView.value) {
367
+    // SecurityCheck角色:本人、班组平均数、本科平均数、本站平均数
368
+    return [
369
+      { label: '本人', value: ((personalPassRate || 0) * 100).toFixed(2) + '%', isImage: false },
370
+      { label: '班组平均数', value: ((teamPassRate || 0) * 100).toFixed(2) + '%', isImage: false, color: teamPassRate < personalPassRate ? '#00AE41' : '#F96060' },
371
+      { label: '本科平均数', value: ((departmentPassRate || 0) * 100).toFixed(2) + '%', isImage: false, color: departmentPassRate < personalPassRate ? '#00AE41' : '#F96060' },
372
+      { label: '本站平均数', value: ((stationPassRate || 0) * 100).toFixed(2) + '%', isImage: false, color: stationPassRate < personalPassRate ? '#00AE41' : '#F96060' }
373
+    ];
374
+  } else if (isTeamView.value) {
375
+    // banzuzhang角色:班组、本科平均数、本站平均数
376
+    return [
377
+      { label: '班组', value: ((teamPassRate || 0) * 100).toFixed(2) + '%', isImage: false },
378
+      { label: '本科平均数', value: ((departmentPassRate || 0) * 100).toFixed(2) + '%', isImage: false, color: departmentPassRate < teamPassRate ? '#00AE41' : '#F96060' },
379
+      { label: '本站平均数', value: ((stationPassRate || 0) * 100).toFixed(2) + '%', isImage: false, color: stationPassRate < teamPassRate ? '#00AE41' : '#F96060' }
380
+    ];
381
+  } else
382
+    // if (isKezhang.value) {
383
+    //   // kezhang角色:全科、本站平均数、前三名班组(isImage: true)
384
+    //   return [
385
+    //     { label: '全科', value: ((departmentPassRate || 0) * 100).toFixed(2) + '%', isImage: false },
386
+    //     { label: '本站平均数', value: ((stationPassRate || 0) * 100).toFixed(2) + '%', isImage: false, color: stationPassRate < departmentPassRate ? '#00AE41' : '#F96060' },
387
+    //     { label: getObjByRank(teamRankingList, 1).name, value: oneIcon, isImage: true },
388
+    //     { label: getObjByRank(teamRankingList, 2).name, value: twoIcon, isImage: true },
389
+    //     { label: getObjByRank(teamRankingList, 3).name, value: threeIcon, isImage: true }
390
+    //   ];
391
+    // } else
392
+    if (isTestRole.value) {
393
+      // test/zhijianke角色:全站、前三名的大队(isImage: true)
394
+      return [
395
+        { label: '全站', value: ((stationPassRate || 0) * 100).toFixed(2) + '%', isImage: false },
396
+        { label: getObjByRank(departmentRankingList, 1).name, value: oneIcon, isImage: true },
397
+        { label: getObjByRank(departmentRankingList, 2).name, value: twoIcon, isImage: true },
398
+        { label: getObjByRank(departmentRankingList, 3).name, value: threeIcon, isImage: true }
399
+      ];
400
+    } else {
401
+      // 默认角色:检查项、完成率
402
+      return [
403
+        { label: '检查项', value: '12', isImage: false },
404
+        { label: '完成率', value: '92%', isImage: false }
405
+      ];
406
+    }
407
+}
408
+
409
+//获取抽问抽答
410
+const getQuestionDataItems = () => {
411
+  const { personalAccuracy, teamAvgAccuracy, deptAvgAccuracy, siteAvgAccuracy, topDepts, topTeamsInDept } = accuracyStatistics.value;
412
+  if (isIndividualView.value) {
413
+    // SecurityCheck角色:本人、班组平均数、本科平均数、本站平均数
414
+    return [
415
+      { label: '本人', value: (personalAccuracy || 0) + '%', isImage: false },
416
+      { label: '班组平均数', value: (teamAvgAccuracy || 0) + '%', isImage: false, color: teamAvgAccuracy < personalAccuracy ? '#00AE41' : '#F96060' },
417
+      { label: '本科平均数', value: (deptAvgAccuracy || 0) + '%', isImage: false, color: deptAvgAccuracy < personalAccuracy ? '#00AE41' : '#F96060' },
418
+      { label: '本站平均数', value: (siteAvgAccuracy || 0) + '%', isImage: false, color: siteAvgAccuracy < personalAccuracy ? '#00AE41' : '#F96060' }
419
+    ];
420
+  } else if (isTeamView.value) {
421
+    // banzuzhang角色:班组、本科平均数、本站平均数
422
+    return [
423
+      { label: '班组', value: (teamAvgAccuracy || 0) + '%', isImage: false },
424
+      { label: '本科平均数', value: (deptAvgAccuracy || 0) + '%', isImage: false, color: deptAvgAccuracy < teamAvgAccuracy ? '#00AE41' : '#F96060' },
425
+      { label: '本站平均数', value: (siteAvgAccuracy || 0) + '%', isImage: false, color: siteAvgAccuracy < teamAvgAccuracy ? '#00AE41' : '#F96060' }
426
+    ];
427
+  } else
428
+    // if (isKezhang.value) {
429
+    //   // kezhang角色:全科、本站平均数、前三名班组(isImage: true)
430
+    //   return [
431
+    //     { label: '全科', value: (deptAvgAccuracy || 0) + '%', isImage: false, },
432
+    //     { label: '本站平均数', value: (siteAvgAccuracy || 0) + '%', isImage: false, color: siteAvgAccuracy < deptAvgAccuracy ? '#00AE41' : '#F96060' },
433
+    //     { label: getObjByRank(topTeamsInDept, 1).teamName, value: oneIcon, isImage: true },
434
+    //     { label: getObjByRank(topTeamsInDept, 2).teamName, value: twoIcon, isImage: true },
435
+    //     { label: getObjByRank(topTeamsInDept, 3).teamName, value: threeIcon, isImage: true }
436
+    //   ];
437
+    // } else 
438
+    if (isTestRole.value) {
439
+      // test/zhijianke角色:全站、前三名的大队(isImage: true)
440
+      return [
441
+        { label: '全站', value: (siteAvgAccuracy || 0) + '%', isImage: false },
442
+        { label: getObjByRank(topDepts, 1).name, value: oneIcon, isImage: true },
443
+        { label: getObjByRank(topDepts, 2).name, value: twoIcon, isImage: true },
444
+        { label: getObjByRank(topDepts, 3).name, value: threeIcon, isImage: true }
445
+      ];
446
+    } else {
447
+      // 默认角色:检查项、完成率
448
+      return [
449
+
450
+      ];
451
+    }
452
+}
453
+
454
+//根据角色获取查获数据项目组
455
+const getSeizeDataItems = () => {
456
+  
457
+  if (isIndividualView.value) {
458
+    // SecurityCheck角色:从securityCheckerData解构
459
+    const { securityCheckerData } = seizeData.value;
460
+    const { selfSeizureCount, teamAverage, departmentAverage, stationAverage } = securityCheckerData || {}
461
+
462
+    // SecurityCheck角色:本人、班组平均数、本科平均数、本站平均数
463
+    return [
464
+      { label: '本人', value: selfSeizureCount || 0, isImage: false },
465
+      { label: '班组平均数', value: teamAverage || 0, isImage: false, color: teamAverage < selfSeizureCount ? '#00AE41' : '#F96060' },
466
+      { label: '本科平均数', value: departmentAverage || 0, isImage: false, color: departmentAverage < selfSeizureCount ? '#00AE41' : '#F96060' },
467
+      { label: '本站平均数', value: (stationAverage && stationAverage.toFixed(2)) || 0, isImage: false, color: stationAverage < selfSeizureCount ? '#00AE41' : '#F96060' }
468
+    ];
469
+  } else if (isTeamView.value) {
470
+    // banzuzhang角色:从teamLeaderData解构
471
+    const { teamLeaderData } = seizeData.value;
472
+    const { departmentAverage, stationAverage, teamAverage } = teamLeaderData || {};
473
+    // banzuzhang角色:班组、本科平均数、本站平均数
474
+    return [
475
+      { label: '班组', value: teamAverage || 0, isImage: false },
476
+      { label: '本科平均数', value: departmentAverage || 0, isImage: false, color: departmentAverage < teamAverage ? '#00AE41' : '#F96060' },
477
+      { label: '本站平均数', value: (stationAverage && stationAverage.toFixed(2)) || 0, isImage: false, color: stationAverage < teamAverage ? '#00AE41' : '#F96060' }
478
+    ];
479
+  } else
480
+    // if (isKezhang.value) {
481
+    //   // kezhang角色:从securityCheckerData解构
482
+    //   const { sectionMasterData } = seizeData.value;
483
+    //   const { stationAverage, topThreeTeams, departmentAverage } = sectionMasterData || {}
484
+
485
+    //   // kezhang角色:全科、本站平均数、前三名班组(isImage: true)
486
+    //   return [
487
+    //     { label: '全科', value: departmentAverage || 0, isImage: false },
488
+    //     { label: '本站平均数', value: stationAverage?.toFixed(2) || 0, isImage: false, color: stationAverage?.toFixed(2) < departmentAverage ? '#00AE41' : '#F96060' },
489
+    //     { label: getObjByRank(topThreeTeams, 1).teamName, value: oneIcon, isImage: true },
490
+    //     { label: getObjByRank(topThreeTeams, 2).teamName, value: twoIcon, isImage: true },
491
+    //     { label: getObjByRank(topThreeTeams, 3).teamName, value: threeIcon, isImage: true }
492
+    //   ];
493
+    // } else
494
+    if (isTestRole.value) {
495
+      // test/zhijianke角色:从securityCheckerData解构
496
+      const { stationMasterData } = seizeData.value;
497
+      const { totalStationSeizure, brigadeRankings } = stationMasterData || {}
498
+
499
+      // 对departmentRankings数组按照seizureCount由大到小排序
500
+      const sortedDepartmentRankings = Array.isArray(brigadeRankings)
501
+        ? [...brigadeRankings].sort((a, b) => (b.seizureCount || 0) - (a.seizureCount || 0))
502
+        : [];
503
+
504
+      if (sortedDepartmentRankings.length == 0) {
505
+        return []
506
+      }
507
+      // test/zhijianke角色:全站、前三名的大队(isImage: true)
508
+      return [
509
+        { label: '全站', value: totalStationSeizure || 0, isImage: false },
510
+        { label: sortedDepartmentRankings[0].brigadeName, value: oneIcon, isImage: true },
511
+        { label: sortedDepartmentRankings[1].brigadeName, value: twoIcon, isImage: true },
512
+        { label: sortedDepartmentRankings[2].brigadeName, value: threeIcon, isImage: true }
513
+      ];
514
+    } else {
515
+      // 默认角色:检查项、完成率
516
+      return [
517
+        { label: '检查项', value: '12', isImage: false },
518
+        { label: '完成率', value: '92%', isImage: false }
519
+      ];
520
+    }
521
+}
522
+
523
+// 根据角色获取巡检排名列表
524
+const getRankList = () => {
525
+  const { teamRanking, teamTotal, departmentRanking, departmentTotal, stationRanking, stationTotal } = inspectionData.value;
526
+  // 根据角色返回不同的排名数据
527
+  if (isIndividualView.value) {
528
+    // 科长、SecurityCheck角色:显示班组、科级和站级排名
529
+    return [
530
+      { label: '班组排名', current: teamRanking || 0, total: teamTotal || 0, percentage: teamTotal ? ((teamRanking || 0) / teamTotal) * 100 : 0 },
531
+      { label: '科级排名', current: departmentRanking || 0, total: departmentTotal || 0, percentage: departmentTotal ? ((departmentRanking || 0) / departmentTotal) * 100 : 0 },
532
+      { label: '站级排名', current: stationRanking || 0, total: stationTotal || 0, percentage: stationTotal ? ((stationRanking || 0) / stationTotal) * 100 : 0, type: 'station' }
533
+    ];
534
+  } else if (isKezhang.value) {
535
+    return [
536
+      { label: '站级排名', current: stationRanking || 0, total: stationTotal || 0, percentage: stationTotal ? ((stationRanking || 0) / stationTotal) * 100 : 0, type: 'station' }
537
+    ];
538
+  } else if (isTeamView.value) {
539
+    // 班组长角色:显示科级和站级排名
540
+    return [
541
+      { label: '科级排名', current: departmentRanking || 0, total: departmentTotal || 0, percentage: departmentTotal ? ((departmentRanking || 0) / departmentTotal) * 100 : 0 },
542
+      { label: '站级排名', current: stationRanking || 0, total: stationTotal || 0, percentage: stationTotal ? ((stationRanking || 0) / stationTotal) * 100 : 0, type: 'station' }
543
+    ];
544
+  } else {
545
+    // 默认角色:显示个人排名
546
+    return [];
547
+  }
548
+}
549
+
550
+//获取抽问抽答排名
551
+const getQuestionRankList = () => {
552
+  // 根据角色返回不同的排名数据
553
+  if (isIndividualView.value) {
554
+    const { teamRanking, deptRanking, siteRanking } = accuracyStatistics.value;
555
+    // 科长、SecurityCheck角色:显示班组、科级和站级排名
556
+    return [
557
+      { label: '班组排名', current: teamRanking?.rank || 0, total: teamRanking?.total || 0, percentage: teamRanking?.total ? ((teamRanking.rank || 0) / teamRanking.total) * 100 : 0 },
558
+      { label: '科级排名', current: deptRanking?.rank || 0, total: deptRanking?.total || 0, percentage: deptRanking?.total ? ((deptRanking.rank || 0) / deptRanking.total) * 100 : 0 },
559
+      { label: '站级排名', current: siteRanking?.rank || 0, total: siteRanking?.total || 0, percentage: siteRanking?.total ? ((siteRanking.rank || 0) / siteRanking.total) * 100 : 0, type: 'station' }
560
+    ];
561
+  } else if (isKezhang.value) {
562
+    const { deptInSiteRanking } = accuracyStatistics.value;
563
+    return [
564
+      { label: '站级排名', current: deptInSiteRanking?.rank || 0, total: deptInSiteRanking?.total || 0, percentage: deptInSiteRanking?.total ? ((deptInSiteRanking?.rank || 0) / deptInSiteRanking?.total) * 100 : 0, type: 'station' }
565
+    ];
566
+  } else if (isTeamView.value) {
567
+    const { teamInDeptRanking, teamInSiteRanking } = accuracyStatistics.value;
568
+    // 班组长角色:显示科级和站级排名
569
+    return [
570
+      { label: '科级排名', current: teamInDeptRanking?.rank || 0, total: teamInDeptRanking?.total || 0, percentage: teamInDeptRanking?.total ? ((teamInDeptRanking.rank || 0) / teamInDeptRanking.total) * 100 : 0 },
571
+      { label: '站级排名', current: teamInSiteRanking?.rank || 0, total: teamInSiteRanking?.total || 0, percentage: teamInSiteRanking?.total ? ((teamInSiteRanking.rank || 0) / teamInSiteRanking.total) * 100 : 0, type: 'station' }
572
+    ];
573
+  } else {
574
+    // 默认角色:显示个人排名
575
+    return [];
576
+  }
577
+}
578
+
579
+// 根据角色获取巡检排名列表
580
+const getSeizeRankList = () => {
581
+  // 根据角色返回不同的排名数据
582
+  if (isIndividualView.value) {
583
+    const { securityCheckerData } = seizeData.value;
584
+    const { teamRanking, departmentRanking, stationRanking } = securityCheckerData || {}
585
+    // 科长、SecurityCheck角色:显示班组、科级和站级排名
586
+    return [
587
+      { label: '班组排名', current: teamRanking?.currentRank || 0, total: teamRanking?.totalItems || 0, percentage: teamRanking?.totalItems ? ((teamRanking.currentRank || 0) / teamRanking.totalItems) * 100 : 0 },
588
+      { label: '科级排名', current: departmentRanking?.currentRank || 0, total: departmentRanking?.totalItems || 0, percentage: departmentRanking?.totalItems ? ((departmentRanking.currentRank || 0) / departmentRanking.totalItems) * 100 : 0 },
589
+      { label: '站级排名', current: stationRanking?.currentRank || 0, total: stationRanking?.totalItems || 0, percentage: stationRanking?.totalItems ? ((stationRanking.currentRank || 0) / stationRanking.totalItems) * 100 : 0, type: 'station' }
590
+    ];
591
+  } else if (isKezhang.value) {
592
+    const { sectionMasterData } = seizeData.value;
593
+    const { stationRanking } = sectionMasterData || {};
594
+    // 科长、SecurityCheck角色:显示班组、科级和站级排名
595
+    return [
596
+      { label: '站级排名', current: stationRanking?.currentRank || 0, total: stationRanking?.totalItems || 0, percentage: stationRanking?.totalItems ? ((stationRanking.currentRank || 0) / stationRanking.totalItems) * 100 : 0, type: 'station' }
597
+    ];
598
+  } else if (isTeamView.value) {
599
+    const { teamLeaderData } = seizeData.value;
600
+    const { departmentRanking, stationRanking } = teamLeaderData || {}
601
+    // 班组长角色:显示科级和站级排名
602
+    return [
603
+      { label: '科级排名', current: departmentRanking?.currentRank || 0, total: departmentRanking?.totalItems || 0, percentage: departmentRanking?.totalItems ? ((departmentRanking.currentRank || 0) / departmentRanking.totalItems) * 100 : 0 },
604
+      { label: '站级排名', current: stationRanking?.currentRank || 0, total: stationRanking?.totalItems || 0, percentage: stationRanking?.totalItems ? ((stationRanking.currentRank || 0) / stationRanking.totalItems) * 100 : 0, type: 'station' }
605
+    ];
606
+  } else {
607
+    // 默认角色:显示个人排名
608
+    return [];
609
+  }
610
+}
611
+
612
+// 辅助方法:根据排名获取对象
613
+const getObjByRank = (arr, rank) => {
614
+  if (!Array.isArray(arr)) return {}
615
+  let res = arr.find(item => item.rank == rank)
616
+  return res || {}
617
+}
618
+
619
+// 处理排序变化
620
+const handleSortChange = (title, type, sortType) => {
621
+  if (sortStates.value[title]) {
622
+    if (type === 'team') {
623
+      sortStates.value[title].teamSortType = sortType
624
+    } else if (type === 'dept') {
625
+      sortStates.value[title].deptSortType = sortType
626
+    }
627
+  }
628
+}
629
+
630
+// 刷新数据
631
+const refreshData = async () => {
632
+  await Promise.allSettled([
633
+    getInspectionData(),
634
+    getSeizeData(),
635
+    fetchAttendanceStats(),
636
+    fetchAccuracyStatistics(),
637
+    getRank()
638
+  ])
639
+}
640
+
641
+onMounted(async () => {
642
+  // 初始化时间范围
643
+  const { startDate: initStartDate, endDate: initEndDate } = calculateDateRange(selectedTimeRange.value)
644
+  startDate.value = initStartDate
645
+  endDate.value = initEndDate
646
+
647
+  // 获取初始数据
648
+  await refreshData()
649
+
650
+
651
+})
652
+</script>
653
+
654
+<style lang="scss" scoped>
655
+.content {
656
+  height: 100%;
657
+  // background: linear-gradient(135deg, #0E4B71 0%, #037599 100%);
658
+  color: #fff;
659
+  padding: 20px;
660
+  box-sizing: border-box;
661
+  overflow: hidden;
662
+
663
+  .grid-container {
664
+    height: 100%;
665
+    display: flex;
666
+    gap: 20px;
667
+
668
+    .left-section {
669
+      flex: 2;
670
+      display: flex;
671
+      flex-direction: column;
672
+      gap: 15px;
673
+      overflow: hidden; /* 添加溢出控制 */
674
+
675
+      .top-section {
676
+        flex: 0.33; /* 降低上层区域高度比例 */
677
+        display: flex;
678
+        gap: 15px;
679
+        min-height: 150px; /* 设置最小高度 */
680
+
681
+        &>* {
682
+          flex: 1;
683
+        }
684
+      }
685
+
686
+      /* 为HomePageStats组件设置明确的高度比例 */
687
+      & > :last-child {
688
+        flex: 0.67; /* 增加下层区域高度比例 */
689
+        height: 100%;
690
+        overflow: hidden; /* 防止内容溢出 */
691
+      }
692
+    }
693
+
694
+    .right-section {
695
+      flex: 0.7;
696
+      height: 100%;
697
+    }
698
+  }
699
+}
700
+</style>

+ 295 - 0
src/views/dataBigScreen/dashboard/components/pageView/OrganizationalPortrait.vue

@@ -0,0 +1,295 @@
1
+<template>
2
+  <div class="content">
3
+    <div class="content-top">
4
+      <div class="content-left">
5
+        <QualificationCapability />
6
+      </div>
7
+      <div class="content-center">
8
+        <div class="quantity-overview">
9
+          <div class="quantity-overview-item" v-for=" item in pageInfo " :key="item.label" :data-title="item.label">
10
+            <div>{{ item.desc }}</div>
11
+            <div style="font-size: 22px;">{{ item.value }}</div>
12
+          </div>
13
+        </div>
14
+        <Collaboration />
15
+      </div>
16
+      <div class="content-right">
17
+        <StandardExecution />
18
+        <SubjectiveImpression />
19
+      </div>
20
+    </div>
21
+    <div class="content-bottom">
22
+      <Attendance />
23
+      <WorkOutput />
24
+      <LearningGrowth />
25
+      <DepartmentInfo />
26
+    </div>
27
+  </div>
28
+</template>
29
+
30
+<script setup>
31
+import {
32
+  QualificationCapability,
33
+  Collaboration,
34
+  StandardExecution,
35
+  SubjectiveImpression,
36
+  Attendance,
37
+  WorkOutput,
38
+  LearningGrowth,
39
+  DepartmentInfo,
40
+  CustomStyleSelect
41
+} from '../pageItems';
42
+import { getOverview, getDeptUserTree } from '@/api/item/items'
43
+import moment from 'moment'
44
+import { useDict } from '@/utils/dict'
45
+import { useTimeOut } from '../pageItems/useTimeOut'
46
+import { provide, computed } from 'vue'
47
+
48
+const {
49
+  sys_user_working_style,
50
+  sys_user_personality_trait,
51
+  sys_user_capability_performance,
52
+  sys_user_interpersonal_interaction,
53
+  sys_user_growth_potential
54
+} = useDict('sys_user_working_style',
55
+  'sys_user_personality_trait',
56
+  'sys_user_capability_performance',
57
+  'sys_user_interpersonal_interaction',
58
+  'sys_user_growth_potential')
59
+
60
+const params = ref({
61
+  deptId: ''
62
+})
63
+const departments = ref([])
64
+const provideParams = computed(() => {
65
+  return {
66
+    deptId: params.value.deptId
67
+  }
68
+})
69
+try {
70
+  getDeptUserTree().then(res => {
71
+    departments.value = (res.data || []).map(item => {
72
+      return {
73
+        ...item,
74
+        value: item.id
75
+      }
76
+    })
77
+    params.value.deptId = departments.value[0] ? departments.value[0].value : 'ALL'
78
+  })
79
+} catch (err) {}
80
+
81
+
82
+
83
+provide('provideParams', provideParams)
84
+
85
+const infoNumber = ref({})
86
+const pageInfo = computed(() => {
87
+
88
+  return [
89
+    {
90
+      label: '资质能力',
91
+      desc: '高级资质等级人数',
92
+      value: infoNumber.value.qualificationLevel
93
+    },
94
+    {
95
+      label: '工作履历',
96
+      desc: '平均安检工作年限',
97
+      value: infoNumber.value.workYears
98
+    },
99
+    {
100
+      label: '出勤投入',
101
+      desc: '人均上岗时长',
102
+      value: (infoNumber.value.avgWorkingHours || 0) + 'h'
103
+    },
104
+    {
105
+      label: '工作产出',
106
+      desc: '有效查获数据',
107
+      value: infoNumber.value.avgSeizureCount
108
+    },
109
+    {
110
+      label: '标准执行',
111
+      desc: '巡检问题总次数',
112
+      value: infoNumber.value.checkCount
113
+    },
114
+    {
115
+      label: '学习成长',
116
+      desc: '平均分',
117
+      value: infoNumber.value.learningGrowthScore
118
+    },
119
+    {
120
+      label: '协调配合',
121
+      desc: '工作风格',
122
+      value: infoNumber.value.workingStyle
123
+    },
124
+    {
125
+      label: '主观印象',
126
+      desc: '高频词',
127
+      value: infoNumber.value.subjectiveImpression
128
+    },
129
+  ]
130
+})
131
+useTimeOut(() => {
132
+  getOverview({
133
+    ...provideParams.value
134
+  }).then((res) => {
135
+    const data = res.data || {}
136
+    let dictArr = [
137
+      ...sys_user_personality_trait.value,
138
+      ...sys_user_capability_performance.value,
139
+      ...sys_user_interpersonal_interaction.value,
140
+      ...sys_user_growth_potential.value
141
+    ]
142
+
143
+    const subjectiveImpression = dictArr.find(item => item.value === data.subjectiveImpression) || { label: data.subjectiveImpression };
144
+    const workingStyle = sys_user_working_style.value.find(item => item.value === data.workingStyle) || { label: data.workingStyle }
145
+    infoNumber.value = {
146
+      ...data,
147
+      subjectiveImpression: subjectiveImpression.label,
148
+      workingStyle: workingStyle.label
149
+    }
150
+  })
151
+}, [ provideParams ])
152
+
153
+</script>
154
+
155
+<style lang="scss" scoped>
156
+.content {
157
+  display: flex;
158
+  width: 100%;
159
+  height: 100%;
160
+  flex-direction: column;
161
+  row-gap: 15px;
162
+
163
+}
164
+
165
+.content-top {
166
+  flex: 1;
167
+  display: flex;
168
+  width: 100%;
169
+  column-gap: 30px;
170
+  overflow: hidden;
171
+
172
+  .content-left {
173
+   width: 30%;
174
+    height: 100%;
175
+    display: flex;
176
+    flex-direction: column;
177
+    overflow: hidden;
178
+    row-gap: 15px;
179
+
180
+    &>*:nth-child(2) {
181
+      width: 100%;
182
+      height: 80%;
183
+    }
184
+  }
185
+
186
+  .content-center {
187
+    flex: 1;
188
+    height: 100%;
189
+    display: flex;
190
+    flex-direction: column;
191
+    row-gap: 10px;
192
+    overflow: hidden;
193
+    padding-top: 20px;
194
+    box-sizing: border-box;
195
+
196
+    .userSelectParams {
197
+      display: flex;
198
+      column-gap: 30px;
199
+      --el-input-height: 40px;
200
+      
201
+      :deep(.el-select__wrapper) {
202
+        font-size: 16px;
203
+      }
204
+      :deep(.el-input) {
205
+        --el-input-height: 40px;
206
+        --el-input-inner-height: 40px;
207
+      }
208
+
209
+      &>*:nth-child(1) {
210
+        flex: 1;
211
+      }
212
+    }
213
+
214
+    .public-inheritance-style {
215
+      width: 100%;
216
+      display: flex;
217
+      column-gap: 10px;
218
+    }
219
+
220
+    .quantity-overview {
221
+      @extend .public-inheritance-style;
222
+      color: #fff;
223
+      font-size: 18px;
224
+      display: flex;
225
+      column-gap: 15px;
226
+      row-gap: 25px;
227
+      padding: 20px 0px 10px;
228
+      box-sizing: border-box;
229
+      flex-wrap: wrap;
230
+
231
+      .quantity-overview-item {
232
+        flex: calc(25% - 12px);
233
+        background: #051E40;
234
+        display: flex;
235
+        flex-direction: column;
236
+        align-items: center;
237
+        justify-content: center;
238
+        row-gap: 5px;
239
+        padding-top: 25px;
240
+        padding-bottom: 10px;
241
+        border-radius: 5px;
242
+        font-weight: bold;
243
+        position: relative;
244
+
245
+        div {
246
+          overflow: hidden;
247
+          width: 100%;
248
+          text-align: center;
249
+        }
250
+
251
+        &::before {
252
+          display: block;
253
+          content: attr(data-title);
254
+          position: absolute;
255
+          white-space: nowrap;
256
+          padding: 2px 10px;
257
+          top: 0px;
258
+          left: 50%;
259
+          width: fit-content;
260
+          transform: translate(-50%, -50%);
261
+          background: #062047;
262
+          border: 1px solid #064371;
263
+          color: #32CDF4;
264
+          border-radius: 6px;
265
+        }
266
+      }
267
+    }
268
+  }
269
+
270
+  .content-right {
271
+    width: 30%;
272
+    height: 100%;
273
+    display: flex;
274
+    flex-direction: column;
275
+    overflow: hidden;
276
+    row-gap: 15px;
277
+
278
+    &>*:nth-child(2) {
279
+      width: 100%;
280
+      height: 80%;
281
+    }
282
+
283
+  }
284
+}
285
+
286
+.content-bottom {
287
+  height: 30%;
288
+  display: flex;
289
+  column-gap: 15px;
290
+
291
+  &>*:nth-child(4) {
292
+    min-width: 40%;
293
+  }
294
+}
295
+</style>

+ 449 - 0
src/views/dataBigScreen/dashboard/components/pageView/OrganizationalPortraitSectionLevel.vue

@@ -0,0 +1,449 @@
1
+<template>
2
+  <div class="content">
3
+    <div class="content-top">
4
+      <div class="content-left">
5
+        <div class="personal" v-if=" pageType === 'personal' ">
6
+          <div>
7
+            <QualificationCapabilityPersonal />
8
+          </div>
9
+          <div style="height: 280px;">
10
+            <Attendance type="personal" />
11
+          </div>
12
+        </div>
13
+        <div class="department" v-else>
14
+          <QualificationCapability />
15
+        </div>
16
+
17
+      </div>
18
+      <div class="content-center">
19
+        <div class="userSelectParams">
20
+          <CustomStyleSelect type="tree" :propsConfig="{ value: 'id', showPrefix: false }" v-model="params.deptId"
21
+            :options="departments" />
22
+        </div>
23
+        <div class="quantity-overview">
24
+          <div class="quantity-overview-item" v-for=" item in pageInfo " :key="item.label" :data-title="item.label">
25
+            <div>{{ item.desc }}</div>
26
+            <div style="font-size: 22px;">{{ getLabel( item.value, item.list ) }}</div>
27
+          </div>
28
+        </div>
29
+        <template v-if=" pageType === 'personal' ">
30
+          <div style="flex: 1; overflow: hidden;">
31
+            <CollaborationPersonal />
32
+          </div>
33
+          <div class="quantity-buttom">
34
+            <WorkOutput type="personal" />
35
+            <LearningGrowth type="personal" />
36
+          </div>
37
+        </template>
38
+        <Collaboration v-else />
39
+      </div>
40
+      <div class="content-right">
41
+        <!-- if 销毁组件 重制布局 -->
42
+        <template v-if="pageType === 'personal'">
43
+          <StandardExecution :type="pageType" />
44
+          <SubjectiveImpression :type="pageType" />
45
+        </template>
46
+        <template v-else>
47
+          <StandardExecution :type="pageType" />
48
+          <SubjectiveImpression :type="pageType" />
49
+        </template>
50
+      </div>
51
+    </div>
52
+    <div class="content-bottom" v-if=" pageType !== 'personal' ">
53
+      <Attendance :type="pageType" />
54
+      <WorkOutput :type="pageType" />
55
+      <LearningGrowth :type="pageType" />
56
+      <DepartmentInfo :type="pageType" />
57
+    </div>
58
+  </div>
59
+</template>
60
+
61
+<script setup>
62
+import {
63
+  QualificationCapability,
64
+  Collaboration,
65
+  StandardExecution,
66
+  SubjectiveImpression,
67
+  Attendance,
68
+  WorkOutput,
69
+  LearningGrowth,
70
+  DepartmentInfo,
71
+  CustomStyleSelect,
72
+  CollaborationPersonal,
73
+  QualificationCapabilityPersonal
74
+} from '../pageItems';
75
+import { getOverview, getDeptUserTree } from '@/api/item/items'
76
+import moment from 'moment'
77
+import { useDict } from '@/utils/dict'
78
+import { useTimeOut } from '../pageItems/useTimeOut'
79
+import { provide, computed } from 'vue'
80
+
81
+const dictList = useDict(
82
+  'sys_user_working_style',
83
+  'sys_user_personality_trait',
84
+  'sys_user_capability_performance',
85
+  'sys_user_interpersonal_interaction',
86
+  'sys_user_growth_potential')
87
+
88
+const params = ref({
89
+  deptId: ''
90
+})
91
+const defaultIndex = ref(0)
92
+const flatDepartments = ref([])
93
+const pageType = computed(() => {
94
+  const item = flatDepartments.value.find(item => String(item.id) === String(params.value.deptId)) || { type: 'department' }
95
+  return item.type
96
+})
97
+
98
+const departments = ref([])
99
+const provideParams = computed(() => {
100
+  return {
101
+    deptId: params.value.deptId
102
+  }
103
+})
104
+
105
+try {
106
+  getDeptUserTree().then(res => {
107
+    const flatDepartmentsList = []
108
+    const appendItem = (list) => {
109
+      list.forEach(attr => {
110
+        let type = 'department'
111
+        if (attr.deptType === 'DEPARTMENT') {
112
+          type = 'department'
113
+        }
114
+        if (attr.deptType === 'TEAMS') {
115
+          type = 'team'
116
+        }
117
+        if (attr.nodeType === 'user') {
118
+          type = 'personal'
119
+        }
120
+        flatDepartmentsList.push({
121
+          id: attr.id,
122
+          type: type
123
+        })
124
+        if (attr.children && attr.children.length) {
125
+          appendItem(attr.children)
126
+        }
127
+      })
128
+    }
129
+    departments.value = res.data.reduce((cur, acc) => {
130
+      acc.children && acc.children.length && cur.push(...acc.children)
131
+      appendItem(acc.children)
132
+      return cur
133
+    }, [])
134
+    flatDepartments.value = flatDepartmentsList
135
+    params.value.deptId = departments.value[ 0 ] ? departments.value[ 0 ].id : ''
136
+  })
137
+} catch (err) { }
138
+
139
+
140
+
141
+
142
+provide('provideParams', provideParams)
143
+
144
+const infoNumber = ref({})
145
+const pageInfo = computed(() => {
146
+  return pageType.value === 'personal' ? [
147
+    {
148
+      label: '资质能力',
149
+      value: infoNumber.value.qualificationLevel
150
+    },
151
+    {
152
+      label: '工作履历',
153
+      desc: '安检工作年限',
154
+      value: infoNumber.value.workYears
155
+    },
156
+    {
157
+      label: '出勤投入',
158
+      desc: '上岗时长',
159
+      value: (infoNumber.value.avgWorkingHours || 0) + 'h'
160
+    },
161
+    {
162
+      label: '工作产出',
163
+      desc: '有效查获数据',
164
+      value: infoNumber.value.avgSeizureCount
165
+    },
166
+    {
167
+      label: '标准执行',
168
+      desc: '巡检问题总次数',
169
+      value: infoNumber.value.checkCount
170
+    },
171
+    {
172
+      label: '学习成长',
173
+      desc: '平均分',
174
+      value: infoNumber.value.learningGrowthScore
175
+    },
176
+    {
177
+      label: '协调配合',
178
+      desc: '工作风格',
179
+      value: infoNumber.value.workingStyle,
180
+      list: dictList.sys_user_working_style.value
181
+    },
182
+    {
183
+      label: '主观印象',
184
+      desc: '高频词',
185
+      value: infoNumber.value.subjectiveImpression,
186
+      list: [
187
+        ...dictList.sys_user_personality_trait.value,
188
+        ...dictList.sys_user_capability_performance.value,
189
+        ...dictList.sys_user_interpersonal_interaction.value,
190
+        ...dictList.sys_user_growth_potential.value
191
+      ]
192
+    },
193
+  ] : [
194
+    {
195
+      label: '资质能力',
196
+      desc: '高级资质等级人数',
197
+      value: infoNumber.value.qualificationLevel
198
+    },
199
+    {
200
+      label: '工作履历',
201
+      desc: '平均安检工作年限',
202
+      value: infoNumber.value.workYears
203
+    },
204
+    {
205
+      label: '出勤投入',
206
+      desc: '人均上岗时长',
207
+      value: (infoNumber.value.avgWorkingHours || 0) + 'h'
208
+    },
209
+    {
210
+      label: '工作产出',
211
+      desc: '有效查获数据',
212
+      value: infoNumber.value.avgSeizureCount
213
+    },
214
+    {
215
+      label: '标准执行',
216
+      desc: '巡检问题总次数',
217
+      value: infoNumber.value.checkCount
218
+    },
219
+    {
220
+      label: '学习成长',
221
+      desc: '平均分',
222
+      value: infoNumber.value.learningGrowthScore
223
+    },
224
+    {
225
+      label: '协调配合',
226
+      desc: '工作风格',
227
+      value: infoNumber.value.workingStyle,
228
+      list: dictList.sys_user_working_style.value
229
+    },
230
+    {
231
+      label: '主观印象',
232
+      desc: '高频词',
233
+      value: infoNumber.value.subjectiveImpression,
234
+      list: [
235
+        ...dictList.sys_user_personality_trait.value,
236
+        ...dictList.sys_user_capability_performance.value,
237
+        ...dictList.sys_user_interpersonal_interaction.value,
238
+        ...dictList.sys_user_growth_potential.value
239
+      ]
240
+    },
241
+  ]
242
+})
243
+const getLabel = (value, list = []) => {
244
+  const item = list.find(item => item.value.includes(value)) || { label: value }
245
+  return item.label
246
+}
247
+useTimeOut(() => {
248
+  getOverview(pageType.value === 'personal' ? { userId: provideParams.value.deptId } : {
249
+    ...provideParams.value
250
+  }).then((res) => {
251
+    const data = res.data || {}
252
+    infoNumber.value = {
253
+      ...data
254
+    }
255
+  })
256
+}, [ provideParams ])
257
+
258
+defineExpose({
259
+  next: () => {
260
+    defaultIndex.value++
261
+    if (defaultIndex.value === departments.value.length) {
262
+      return () => { // resetIndex
263
+        defaultIndex.value = 0
264
+      }
265
+    } else {
266
+      params.value.deptId = departments.value[ defaultIndex.value ] ? departments.value[ defaultIndex.value ].id : ''
267
+      return false
268
+    }
269
+  }
270
+})
271
+
272
+</script>
273
+
274
+<style lang="scss" scoped>
275
+.content {
276
+  display: flex;
277
+  width: 100%;
278
+  height: 100%;
279
+  flex-direction: column;
280
+  row-gap: 15px;
281
+
282
+}
283
+
284
+.content-top {
285
+  flex: 1;
286
+  display: flex;
287
+  width: 100%;
288
+  column-gap: 30px;
289
+  overflow: hidden;
290
+
291
+  .content-left {
292
+    width: 30%;
293
+    height: 100%;
294
+
295
+    .department {
296
+      width: 100%;
297
+      height: 100%;
298
+      display: flex;
299
+      flex-direction: column;
300
+      overflow: hidden;
301
+      row-gap: 15px;
302
+
303
+      &>*:nth-child(2) {
304
+        width: 100%;
305
+        height: 70%;
306
+      }
307
+    }
308
+
309
+    .personal {
310
+      height: 100%;
311
+      display: flex;
312
+      flex-direction: column;
313
+      overflow: hidden;
314
+      row-gap: 15px;
315
+
316
+      &>div:nth-child(1) {
317
+        flex: 1;
318
+        display: flex;
319
+        flex-direction: column;
320
+        overflow: hidden;
321
+        row-gap: 15px;
322
+
323
+        &>*:nth-child(2) {
324
+          width: 100%;
325
+          height: 70%;
326
+        }
327
+      }
328
+    }
329
+
330
+  }
331
+
332
+  .content-center {
333
+    flex: 1;
334
+    height: 100%;
335
+    display: flex;
336
+    flex-direction: column;
337
+    row-gap: 10px;
338
+    overflow: hidden;
339
+    padding-top: 20px;
340
+    box-sizing: border-box;
341
+
342
+    .userSelectParams {
343
+      display: flex;
344
+      column-gap: 30px;
345
+      --el-input-height: 40px;
346
+
347
+      :deep(.el-select__wrapper) {
348
+        font-size: 16px;
349
+      }
350
+
351
+      :deep(.el-input) {
352
+        font-size: 16px;
353
+        --el-input-height: 40px;
354
+        --el-input-inner-height: 40px;
355
+      }
356
+
357
+      &>*:nth-child(1) {
358
+        flex: 1;
359
+      }
360
+    }
361
+
362
+    .public-inheritance-style {
363
+      width: 100%;
364
+      display: flex;
365
+      column-gap: 10px;
366
+    }
367
+
368
+    .quantity-overview {
369
+      @extend .public-inheritance-style;
370
+      color: #fff;
371
+      font-size: 18px;
372
+      display: flex;
373
+      column-gap: 15px;
374
+      row-gap: 25px;
375
+      padding: 20px 0px 10px;
376
+      box-sizing: border-box;
377
+      flex-wrap: wrap;
378
+
379
+      .quantity-overview-item {
380
+        flex: calc(25% - 12px);
381
+        background: #051E40;
382
+        display: flex;
383
+        flex-direction: column;
384
+        align-items: center;
385
+        justify-content: center;
386
+        row-gap: 5px;
387
+        padding-top: 25px;
388
+        padding-bottom: 10px;
389
+        border-radius: 5px;
390
+        font-weight: bold;
391
+        position: relative;
392
+
393
+        div {
394
+          overflow: hidden;
395
+          width: 100%;
396
+          text-align: center;
397
+        }
398
+
399
+        &::before {
400
+          display: block;
401
+          content: attr(data-title);
402
+          position: absolute;
403
+          white-space: nowrap;
404
+          padding: 2px 10px;
405
+          top: 0px;
406
+          left: 50%;
407
+          width: fit-content;
408
+          transform: translate(-50%, -50%);
409
+          background: #062047;
410
+          border: 1px solid #064371;
411
+          color: #32CDF4;
412
+          border-radius: 6px;
413
+        }
414
+      }
415
+    }
416
+
417
+    .quantity-buttom {
418
+      height: 280px;
419
+      display: flex;
420
+      column-gap: 15px;
421
+    }
422
+  }
423
+
424
+  .content-right {
425
+    width: 30%;
426
+    height: 100%;
427
+    display: flex;
428
+    flex-direction: column;
429
+    overflow: hidden;
430
+    row-gap: 15px;
431
+
432
+    &>*:nth-child(2) {
433
+      width: 100%;
434
+      height: 80%;
435
+    }
436
+
437
+  }
438
+}
439
+
440
+.content-bottom {
441
+  height: 30%;
442
+  display: flex;
443
+  column-gap: 15px;
444
+
445
+  &>*:nth-child(4) {
446
+    min-width: 40%;
447
+  }
448
+}
449
+</style>

+ 248 - 0
src/views/dataBigScreen/dashboard/components/pageView/QualityControl.vue

@@ -0,0 +1,248 @@
1
+<template>
2
+  <div class="content">
3
+    <div class="content-top">
4
+      <div class="content-left">
5
+        <InspectionTask />
6
+      </div>
7
+      <div class="content-center">
8
+        <div class="userSelectParams">
9
+          <CustomStyleSelect v-if="type === 'brigade'" v-model="params.inspectDepartmentId" :options="departments" />
10
+          <el-date-picker
11
+            v-model="params.time"
12
+            type="daterange"
13
+            range-separator="至"
14
+            value-format="YYYY-MM-DD"
15
+            start-placeholder="开始时间"
16
+            end-placeholder="结束时间" />
17
+        </div>
18
+        <div class="quantity-overview">
19
+          <div class="quantity-overview-item">
20
+            <div>查获总数</div>
21
+            <div style="font-size: 24px;">{{ infoNumber.total }}</div>
22
+          </div>
23
+          <div class="quantity-overview-item">
24
+            <div>移交公安</div>
25
+            <div style="font-size: 24px;">{{ infoNumber.policeTotal }}</div>
26
+          </div>
27
+          <div class="quantity-overview-item">
28
+            <div>隐匿携带</div>
29
+            <div style="font-size: 24px;">{{ infoNumber.concealTotal }}</div>
30
+          </div>
31
+        </div>
32
+        <SeizeDetails />
33
+      </div>
34
+      <div class="content-right">
35
+        <ProblemDiscovery />
36
+        <IssueRectification :type="type" />
37
+      </div>
38
+    </div>
39
+    <div class="content-bottom">
40
+      <WorkPortrait />
41
+    </div>
42
+  </div>
43
+</template>
44
+
45
+<script setup>
46
+import {
47
+  InspectionTask,
48
+  SeizeDetails,
49
+  ProblemDiscovery,
50
+  IssueRectification,
51
+  WorkPortrait,
52
+  CustomStyleSelect
53
+} from '../pageItems';
54
+import { getAppTotalSome, getDeptList } from '@/api/item/items'
55
+import moment from 'moment'
56
+import { useTimeOut } from '../pageItems/useTimeOut'
57
+import { onMounted, onUnmounted, provide, computed } from 'vue'
58
+const props = defineProps({
59
+  type: {
60
+    type: String,
61
+    default: 'station'
62
+  }
63
+})
64
+
65
+const params = ref({
66
+  time: [moment().subtract(91, 'days').format('YYYY-MM-DD'), moment().format('YYYY-MM-DD')],
67
+  inspectDepartmentId: props.type === 'brigade' ? '' : 'ALL'
68
+})
69
+
70
+const provideParams = computed(() => {
71
+  return {
72
+    startDate: params.value.time && params.value.time[0] ? params.value.time[0] : undefined,
73
+    endDate:  params.value.time && params.value.time[1] ? params.value.time[1] : undefined,
74
+    inspectDepartmentId: params.value.inspectDepartmentId,
75
+    deptId: params.value.inspectDepartmentId,
76
+    checkedDepartmentId: params.value.inspectDepartmentId,
77
+  }
78
+})
79
+
80
+provide('provideParams', provideParams)
81
+
82
+const infoNumber = ref({})
83
+useTimeOut(() => {
84
+  getAppTotalSome({
85
+    startDate: provideParams.value.startDate,
86
+    endDate: provideParams.value.endDate,
87
+    inspectDepartmentId: provideParams.value.inspectDepartmentId === 'ALL' ? '' :  provideParams.value.inspectDepartmentId 
88
+  }).then(res => {
89
+    infoNumber.value = res.data
90
+  })
91
+}, [provideParams])
92
+const departments = ref(props.type === 'brigade' ? [] : [
93
+  { value: 'ALL', label: '全站' },
94
+])
95
+function buildBrigadeOptions (tree = []) {
96
+  const result = [];
97
+  function dfs (node, path = []) {
98
+    const currentPath = [ ...path, node.label ];
99
+    // 如果是 DEPARTMENT 节点
100
+    if (node.deptType === "BRIGADE") {
101
+      result.push({
102
+        text: currentPath.join(' / '),
103
+        value: node.id
104
+      });
105
+    }
106
+    // 继续递归子节点
107
+    if (node.children && Array.isArray(node.children)) {
108
+      node.children.forEach(child => dfs(child, currentPath));
109
+    }
110
+  }
111
+  tree.forEach(root => dfs(root));
112
+  return result;
113
+}
114
+const defaultIndex = ref(0)
115
+onMounted(() => {
116
+  props.type === 'brigade' && getDeptList().then(res => {
117
+    departments.value = [ ...departments.value, ...buildBrigadeOptions(res.data || []).map(item => ({
118
+      ...item,
119
+      label: item.text?.split('/')[ 1 ],
120
+      value: item.value.toString()
121
+    }))]
122
+    params.value.inspectDepartmentId = departments.value && departments.value[0] ? departments.value[0].value : ''
123
+  })
124
+})
125
+
126
+defineExpose({
127
+  next: () => {
128
+    defaultIndex.value++
129
+    if (defaultIndex.value === departments.value.length) {
130
+      return () => { // resetIndex
131
+        defaultIndex.value = 0
132
+      }
133
+    } else {
134
+      params.value.inspectDepartmentId = departments.value[ defaultIndex.value ] ? departments.value[ defaultIndex.value ].value : ''
135
+      return false
136
+    }
137
+  }
138
+})
139
+
140
+</script>
141
+
142
+<style lang="scss" scoped>
143
+.content {
144
+  display: flex;
145
+  width: 100%;
146
+  height: 100%;
147
+  flex-direction: column;
148
+  row-gap: 15px;
149
+  :deep(.el-select__wrapper) {
150
+    font-size: 16px;
151
+  }
152
+}
153
+
154
+.content-top {
155
+  flex: 1;
156
+  display: flex;
157
+  width: 100%;
158
+  column-gap: 10px;
159
+  overflow: hidden;
160
+
161
+  .content-left {
162
+    width: 30%;
163
+    height: 100%;
164
+    display: flex;
165
+    flex-direction: column;
166
+    overflow: hidden;
167
+
168
+    &>*:nth-child {
169
+      width: 100%;
170
+      height: 100%;
171
+    }
172
+  }
173
+
174
+  .content-center {
175
+    flex: 1;
176
+    // min-width: 800px;
177
+    height: 100%;
178
+    display: flex;
179
+    flex-direction: column;
180
+    row-gap: 10px;
181
+    overflow: hidden;
182
+
183
+    .userSelectParams {
184
+      display: flex;
185
+      column-gap: 30px;
186
+      padding: 20px 20px 0;
187
+      --el-input-height: 40px;
188
+      --el-font-size-base: 18px;
189
+      &>*:nth-child(1) {
190
+        flex: 1;
191
+      }
192
+
193
+      &>*:nth-child(2) {
194
+        flex: 2;
195
+      }
196
+    }
197
+
198
+    .public-inheritance-style {
199
+      width: 100%;
200
+      display: flex;
201
+      column-gap: 10px;
202
+    }
203
+
204
+    .quantity-overview {
205
+      @extend .public-inheritance-style;
206
+      height: 140px;
207
+      color: #fff;
208
+      font-size: 20px;
209
+      display: flex;
210
+      column-gap: 25px;
211
+      padding: 10px 20px 10px;
212
+      box-sizing: border-box;
213
+
214
+      .quantity-overview-item {
215
+        flex: 1;
216
+        height: 100%;
217
+        background: #051E40;
218
+        display: flex;
219
+        flex-direction: column;
220
+        align-items: center;
221
+        justify-content: center;
222
+        row-gap: 10px;
223
+        border-radius: 5px;
224
+        font-weight: bold;
225
+      }
226
+    }
227
+  }
228
+
229
+  .content-right {
230
+    width: 30%;
231
+    height: 100%;
232
+    display: flex;
233
+    flex-direction: column;
234
+    overflow: hidden;
235
+    row-gap: 15px;
236
+
237
+    &>*:nth-child {
238
+      width: 100%;
239
+      height: 50%;
240
+    }
241
+
242
+  }
243
+}
244
+
245
+.content-bottom {
246
+  height: 35%
247
+}
248
+</style>

+ 148 - 0
src/views/dataBigScreen/dashboard/components/pageView/RealTimeStatus.vue

@@ -0,0 +1,148 @@
1
+<template>
2
+  <div class="content">
3
+    <div class="content-left">
4
+      <DataViewLeft />
5
+    </div>
6
+    <div class="content-center">
7
+      <div class="quantity-overview">
8
+        <div class="quantity-overview-item">
9
+          <div>今日总查获数</div>
10
+          <div style="font-size: 26px;">{{ infoNumber.total }}</div>
11
+        </div>
12
+        <div class="quantity-overview-item">
13
+          <div>今日移交公安</div>
14
+          <div style="font-size: 26px;">{{ infoNumber.policeTotal }}</div>
15
+        </div>
16
+        <div class="quantity-overview-item">
17
+          <div>今日隐匿携带</div>
18
+          <div style="font-size: 26px;">{{ infoNumber.concealTotal }}</div>
19
+        </div>
20
+      </div>
21
+      <ChannelsAndPersonnel />
22
+    </div>
23
+    <div class="content-right">
24
+      <ClassTaskCompletionRank />
25
+      <DepartmentalTaskCompletionRank />
26
+      <PersonalTaskCompletionRank />
27
+    </div>
28
+  </div>
29
+</template>
30
+
31
+<script setup>
32
+import {
33
+  DataViewLeft,
34
+  ChannelsAndPersonnel,
35
+  ClassTaskCompletionRank,
36
+  DepartmentalTaskCompletionRank,
37
+  PersonalTaskCompletionRank
38
+} from '../pageItems';
39
+import { getTotalSome } from '@/api/item/items'
40
+import { onMounted } from 'vue';
41
+import moment from 'moment'
42
+import { useTimeOut } from '../pageItems/useTimeOut'
43
+const infoNumber = ref({})
44
+useTimeOut(() => {
45
+  getTotalSome({
46
+    startDate: moment().format('YYYY-MM-DD'),
47
+    endDate: moment().format('YYYY-MM-DD'),
48
+  }).then(res => {
49
+    infoNumber.value = res.data
50
+  })
51
+})
52
+</script>
53
+
54
+<style lang="scss" scoped>
55
+.content {
56
+  display: flex;
57
+  width: 100%;
58
+  height: 100%;
59
+  column-gap: 10px;
60
+  overflow: hidden;
61
+
62
+  .content-left {
63
+    width: 30%;
64
+    height: 100%;
65
+    display: flex;
66
+    flex-direction: column;
67
+    overflow: hidden;
68
+
69
+    &>*:nth-child(1) {
70
+      width: 100%;
71
+      height: 36%;
72
+    }
73
+
74
+    &>*:nth-child(2) {
75
+      width: 100%;
76
+      height: 32%;
77
+    }
78
+
79
+    &>*:nth-child(3) {
80
+      width: 100%;
81
+      height: 32%;
82
+    }
83
+  }
84
+
85
+  .content-center {
86
+    flex: 1;
87
+    min-width: 780px;
88
+    height: 100%;
89
+    display: flex;
90
+    flex-direction: column;
91
+    row-gap: 10px;
92
+    overflow: hidden;
93
+
94
+    .public-inheritance-style {
95
+      width: 100%;
96
+      display: flex;
97
+      column-gap: 10px;
98
+    }
99
+
100
+    .quantity-overview {
101
+      @extend .public-inheritance-style;
102
+      height: 140px;
103
+      color: #fff;
104
+      font-size: 20px;
105
+      display: flex;
106
+      column-gap: 25px;
107
+      padding: 20px 20px 10px;
108
+      box-sizing: border-box;
109
+
110
+      .quantity-overview-item {
111
+        flex: 1;
112
+        height: 100%;
113
+        background: #051E40;
114
+        display: flex;
115
+        flex-direction: column;
116
+        align-items: center;
117
+        justify-content: center;
118
+        row-gap: 10px;
119
+        border-radius: 5px;
120
+        font-weight: bold;
121
+      }
122
+    }
123
+  }
124
+
125
+  .content-right {
126
+    width: 30%;
127
+    height: 100%;
128
+    display: flex;
129
+    flex-direction: column;
130
+    overflow: hidden;
131
+
132
+    &>*:nth-child(1) {
133
+      width: 100%;
134
+      height: 36%;
135
+    }
136
+
137
+    &>*:nth-child(2) {
138
+      width: 100%;
139
+      height: 32%;
140
+    }
141
+
142
+    &>*:nth-child(3) {
143
+      width: 100%;
144
+      height: 32%;
145
+    }
146
+  }
147
+}
148
+</style>

+ 221 - 65
src/views/dataBigScreen/dashboard/index.vue

@@ -1,41 +1,49 @@
1 1
 <template>
2
-  <dashboard-container>
3
-    <top-title class="wow slideInDown" name="智慧大屏监控中心"></top-title>
4
-    <template #left>
5
-      <div class="item">
6
-        <!-- 重大违禁品情况 -->
7
-        <major-contraband-cases />
8
-      </div>
9
-      <div class="item">
10
-        <!-- 故意隐匿情况 -->
11
-        <deliberate-concealment />
12
-      </div>
13
-      <div class="item">
14
-        <div class="dashboard-title">查获类别分布</div>
15
-        <pie-chart />
16
-      </div>
17
-    </template>
2
+  <dashboard-container class="page" ref="page">
3
+    <top-title class="wow slideInDown" :name="'安检站分级质控监控大屏'" />
18 4
     <template #center>
19
-      <div class="item item-center">
20
-        <classification-of-seized-quantity />
21
-      </div>
22
-      <div class="item">
23
-        <!-- 查获排名 -->
24
-        <seized-ranking />
5
+      <div class="left">
6
+        <div :class="{ carouselItemBtn: true, carouselItemActive: activeIndex === 0 }" @click="setActiveItem(0)">首页看板
7
+        </div>
8
+        <div :class="{ carouselItemBtn: true, carouselItemActive: activeIndex === 1 }" @click="setActiveItem(1)">实时状态
9
+        </div>
10
+        <div :class="{ carouselItemBtn: true, carouselItemActive: activeIndex === 2 }" @click="setActiveItem(2)">站级质控
11
+        </div>
12
+        <div :class="{ carouselItemBtn: true, carouselItemActive: activeIndex === 3 }" @click="setActiveItem(3)">队级质控
13
+        </div>
25 14
       </div>
26
-    </template>
27
-    <template #right>
28
-      <div class="item">
29
-        <!-- 查获通道分布 -->
30
-        <channel-distribution />
15
+      <div class="right">
16
+        <div :class="{ carouselItemBtn: true, carouselItemActive: activeIndex === 4 }" @click="setActiveItem(4)">站级画像
17
+        </div>
18
+        <div :class="{ carouselItemBtn: true, carouselItemActive: activeIndex === 5 }" @click="setActiveItem(5)">队级画像
19
+        </div>
20
+        <!-- <div :class="{ carouselItemBtn: true, carouselItemActive: activeIndex === 6 || activeIndex === 7  }" @click="setActiveItem( 6 )">班组画像
21
+        </div> -->
22
+        <!-- <div :class="{ carouselItemBtn: true, carouselItemActive: activeIndex === 7 }" @click="setActiveItem( 7 )">个人画像
23
+        </div> -->
31 24
       </div>
32
-      <div class="item">
33
-        <div class="dashboard-title">查获时段分布</div>
34
-        <line-chart />
35
-      </div>
36
-      <div class="item">
37
-        <div class="dashboard-title">查获岗位分布</div>
38
-        <pie-radius-chart />
25
+      <div class="warp" ref="content">
26
+        <el-carousel style="height: 100%;" height="100%" :autoplay="false" motion-blur arrow="never"
27
+          indicator-position="none" interval="60000" ref="carousel">
28
+          <el-carousel-item>
29
+            <HomePage v-if="activeIndex === 0" />
30
+          </el-carousel-item>
31
+          <el-carousel-item>
32
+            <RealTimeStatus v-if="activeIndex === 1" />
33
+          </el-carousel-item>
34
+          <el-carousel-item>
35
+            <QualityControl v-if="activeIndex === 2" />
36
+          </el-carousel-item>
37
+          <el-carousel-item>
38
+            <QualityControl type="brigade" v-if="activeIndex === 3" ref="departmentQualityControl" />
39
+          </el-carousel-item>
40
+          <el-carousel-item>
41
+            <OrganizationalPortrait v-if="activeIndex === 4" />
42
+          </el-carousel-item>
43
+          <el-carousel-item>
44
+            <OrganizationalPortraitSectionLevel v-if="activeIndex === 5" ref="organizationalPortrait" />
45
+          </el-carousel-item>
46
+        </el-carousel>
39 47
       </div>
40 48
     </template>
41 49
   </dashboard-container>
@@ -43,44 +51,192 @@
43 51
 
44 52
 <script setup>
45 53
 import TopTitle from './TopTitle.vue';
46
-import PieChart from './PieChart.vue';
47
-import PieRadiusChart from './PieRadiusChart.vue';
48
-import LineChart from './LineChart.vue';
49
-import MajorContrabandCases from './MajorContrabandCases.vue';
50
-import SeizedRanking from './SeizedRanking.vue';
51
-import DeliberateConcealment from './DeliberateConcealment.vue';
52
-import ChannelDistribution from './ChannelDistribution.vue';
53
-import ClassificationOfSeizedQuantity from '@/views/dataBigScreen/dashboard/ClassificationOfSeizedQuantity.vue';
54 54
 import DashboardContainer from '@/views/dataBigScreen/dashboard/components/DashboardContainer.vue';
55
+import HomePage from './components/pageView/HomePage.vue';
56
+import RealTimeStatus from './components/pageView/RealTimeStatus.vue';
57
+import QualityControl from './components/pageView/QualityControl.vue';
58
+import OrganizationalPortrait from './components/pageView/OrganizationalPortrait.vue';
59
+import OrganizationalPortraitSectionLevel from './components/pageView/OrganizationalPortraitSectionLevel.vue';
60
+import { ref, computed, onMounted, onUnmounted } from 'vue';
61
+import { InactivityTimer } from './InactivityTimer.js'
62
+
63
+
64
+
65
+const page = ref(null)
66
+const carousel = ref(null)
67
+const content = ref(null)
68
+const departmentQualityControl = ref(null)
69
+const organizationalPortrait = ref(null)
70
+const setActiveItem = (index) => {
71
+  carousel.value.setActiveItem(index)
72
+}
73
+
74
+const activeIndex = computed(() => {
75
+  return carousel.value ? carousel.value.activeIndex : 7
76
+})
77
+
78
+const timer = new InactivityTimer({
79
+  timeout: 60000, // 60秒超时(1分钟)
80
+  warningTime: 5000, // 提前5秒警告
81
+  onTimeout: () => {
82
+    let switchPage = true
83
+    // 首页看板直接切换到下一个页面
84
+    if (activeIndex.value === 0) {
85
+      // 首页看板直接切换到实时状态
86
+      switchPage = true
87
+    }
88
+    else if (activeIndex.value === 3) {
89
+      switchPage = departmentQualityControl.value.next()
90
+    }
91
+    else if (activeIndex.value === 5) {
92
+      switchPage = organizationalPortrait.value.next()
93
+    }
94
+    if (typeof switchPage === 'function') {
95
+      switchPage()
96
+    }
97
+    carousel.value && switchPage && carousel.value.next()
98
+    timer.start();
99
+  },
100
+  onWarning: () => console.info('即将切换至下一张页面')
101
+});
102
+
103
+const setStyle = () => {
104
+  const root = document.querySelector(':root')
105
+  root.style.setProperty('--el-border-color-light', '#0D507A')
106
+  root.style.setProperty('--el-bg-color-overlay', '#0D507A')
107
+  root.style.setProperty('--el-box-shadow-light', 'none')
108
+  root.style.setProperty('--el-text-color-regular', '#fff')
109
+  root.style.setProperty('--el-fill-color-light', '#70CFE7')
110
+  root.style.setProperty('--el-datepicker-inrange-bg-color', '#0D507A')
111
+
112
+}
113
+
114
+const resetStyle = () => {
115
+  const root = document.querySelector(':root')
116
+  root.style.setProperty('--el-border-color-light', '#e4e7ed')
117
+  root.style.setProperty('--el-bg-color-overlay', '#ffffff')
118
+  root.style.setProperty('--el-box-shadow-light', '0px 0px 12px rgba(0, 0, 0, 0.12)')
119
+  root.style.setProperty('--el-text-color-regular', '#606266')
120
+  root.style.setProperty('--el-fill-color-light', '#f5f7fa')
121
+  root.style.setProperty('--el-datepicker-inrange-bg-color', '#fff')
122
+}
123
+
124
+
125
+onMounted(() => {
126
+  const pageRoot = page.value.getRoot()
127
+  function setCSSZoom(scale) {
128
+    const height = ((content.value.offsetHeight - (110 * scale)) / scale)
129
+    content.value.style.setProperty('height', height + 'px')
130
+    pageRoot.style.zoom = scale;
131
+  }
132
+  if (window.innerWidth < 1280) {
133
+    setCSSZoom(0.4)
134
+  }
135
+  if (window.innerWidth > 1280 && window.innerWidth <= 1440) {
136
+    setCSSZoom(0.6)
137
+  }
138
+  if (window.innerWidth > 1440 && window.innerWidth <= 1680) {
139
+    setCSSZoom(0.75)
140
+  }
141
+  if (window.innerWidth > 1680 && window.innerWidth < 1920) {
142
+    setCSSZoom(0.9)
143
+  }
144
+  if (window.innerWidth >= 1920) {
145
+    setCSSZoom(1)
146
+  }
147
+  setStyle()
148
+  // 启动定时器
149
+  timer.start();
150
+})
151
+onUnmounted(() => {
152
+  resetStyle()
153
+  // 停止定时器
154
+  timer.stop();
155
+  timer.clearTimer();
156
+})
55 157
 </script>
56 158
 
57 159
 <style lang="scss" scoped>
58
-.item {
59
-  width: calc(100% + -0px);
60
-  height: calc(33.33% - 8px);
61
-  position: relative;
62
-  background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
63
-
64
-  &.item-center {
65
-    height: calc(66.66% - 4px);
66
-  }
160
+.warp {
161
+  height: calc(100vh);
162
+  box-sizing: border-box;
67 163
 }
68 164
 
69
-.dashboard-title {
70
-  position: relative;
165
+.left {
166
+  position: absolute;
167
+  top: 0;
168
+  left: 0;
71 169
   display: flex;
72
-  justify-content: space-between;
170
+  height: 59px;
73 171
   align-items: center;
74
-  flex-wrap: nowrap;
75
-  flex-direction: row;
76
-  z-index: 1;
77
-  align-content: flex-start;
78
-  background: url(/src/assets/images/titlebg-4340cf1c.png);
79
-  background-size: 100% 100%;
80
-  font-weight: 400;
81
-  font-size: 14px;
82
-  color: #fbffff;
83
-  padding: 0 20px;
84
-  height: 42px;
172
+  justify-items: flex-start;
173
+  padding: 0 15px;
174
+  column-gap: 8px;
175
+  background: #054066;
176
+  border-bottom: 1px solid #037599;
177
+}
178
+
179
+.right {
180
+  position: absolute;
181
+  top: 0;
182
+  right: 0;
183
+  display: flex;
184
+  height: 59px;
185
+  align-items: center;
186
+  justify-items: flex-end;
187
+  padding: 0 15px;
188
+  column-gap: 8px;
189
+  background: #054066;
190
+  border-bottom: 1px solid #037599;
191
+}
192
+
193
+.carouselItemBtn {
194
+  width: 126px;
195
+  height: 40px;
196
+  color: #B4E9FF;
197
+  font-weight: bold;
198
+  border-radius: 5px;
199
+  text-align: center;
200
+  text-indent: 6px;
201
+  line-height: 38px;
202
+  font-size: 18px;
203
+  background: url('../../../assets/images/btnBg.png') no-repeat;
204
+  background-size: cover;
205
+  cursor: pointer;
206
+}
207
+
208
+.carouselItemActive {
209
+  background: linear-gradient(to right 50%, #3398BE, #0B5883);
210
+  color: #fff;
211
+  background: url('../../../assets/images/btnActive.png') no-repeat;
212
+  background-size: cover;
85 213
 }
86 214
 </style>
215
+
216
+<style lang="scss">
217
+.page {
218
+  .wow {
219
+    .quantity-overview-item {
220
+      background: url('../../../assets/images/cardBg.png') no-repeat !important;
221
+      background-size: 100% 105% !important;
222
+    }
223
+  }
224
+}
225
+
226
+.userSelectParams {
227
+  --el-border-color-extra-light: #70CFE7;
228
+  --el-text-color-primary: #fff;
229
+  --el-fill-color-blank: #0D507A;
230
+  --el-border-color: #0D507A;
231
+}
232
+
233
+.el-popper {
234
+  --el-border-color-extra-light: #70CFE7;
235
+  //--el-fill-color-blank: #0D507A;
236
+  --el-border-color: #0D507A;
237
+}
238
+
239
+.el-message-box {
240
+  --el-text-color-regular: #606060;
241
+}
242
+</style>