瀏覽代碼

feat(数据看板): 新增部门树选择功能并优化数据展示

refactor(组件): 重构部门数据传递逻辑,支持多层级展示

fix(风险隐患): 修复表格滚动和显示问题,优化描述文案

feat(质控分析): 新增移交公安数据和X光机漏检数据展示

style(使用报告): 优化文案格式和内容展示顺序

perf(导出功能): 改进Word导出逻辑,支持隐藏组件和表格优化
huoyi 1 周之前
父節點
當前提交
28d4d29992

+ 79 - 0
src/api/assistant/assistant.js

@@ -108,6 +108,8 @@ export function getCalculateByTimeList(data) {
108 108
   })
109 109
 }
110 110
 
111
+
112
+
111 113
 //使用报告
112 114
 export function getUsageReport(query) {
113 115
   return request({
@@ -115,4 +117,81 @@ export function getUsageReport(query) {
115 117
     method: 'get',
116 118
     params: query
117 119
   })
120
+}
121
+// 抽问抽答完成趋势
122
+export function getCompletionTrend(params = {}) {
123
+  return request({
124
+    url: '/v1/cs/app/daily-exam/completion-comparison',
125
+    method: 'get',
126
+    params
127
+  })
128
+}
129
+
130
+// 错题分析 - 总体问题分布
131
+export function getWrongAnalysisOverview(params = {}) {
132
+  return request({
133
+    url: '/v1/cs/app/daily-exam/wrong-analysis/pc-overview',
134
+    method: 'get',
135
+    params
136
+  })
137
+}
138
+
139
+// 错题分析 - 问题分布对比(雷达图)
140
+export function getWrongAnalysisRadar(params = {}) {
141
+  return request({
142
+    url: '/v1/cs/app/daily-exam/wrong-analysis/pc-radar',
143
+    method: 'get',
144
+    params
145
+  })
146
+}
147
+
148
+//移交公安数据
149
+export function getPoliceData(params = {}) {
150
+  return request({
151
+    url: '/quality/item-category-stats/police-data',
152
+    method: 'get',
153
+    params
154
+  })
155
+}
156
+//移交公安数据统计
157
+export function getPoliceDataStats(params = {}) {
158
+  return request({
159
+    url: '/quality/item-category-stats/police-stats',
160
+    method: 'get',
161
+    params
162
+  })
163
+}
164
+
165
+//X 光机漏检数据
166
+export function getXrayMissCheck(params = {}) {
167
+  return request({
168
+    url: '/quality/item-category-stats/xray-miss-check',
169
+    method: 'get',
170
+    params
171
+  })
172
+}
173
+//X 光机漏检人员统计 TOP3
174
+export function getXrayMissCheckStats(params = {}) {
175
+  return request({
176
+    url: '/quality/item-category-stats/xray-miss-check-top3',
177
+    method: 'get',
178
+    params
179
+  })
180
+}
181
+
182
+//异常查获数据
183
+export function getAbnormalSeizureData(params = {}) {
184
+  return request({
185
+    url: '/quality/item-category-stats/abnormal-seizure-data',
186
+    method: 'get',
187
+    params
188
+  })
189
+}
190
+//异常查获数据 TOP3
191
+export function getAbnormalSeizureStats(params = {}) {
192
+  return request({
193
+    url: '/quality/item-category-stats/abnormal-seizure-data-top3',
194
+    method: 'get',
195
+    params
196
+  })
118 197
 }

+ 8 - 0
src/api/system/user.js

@@ -143,3 +143,11 @@ export function deptTreeSelect() {
143 143
     method: 'get'
144 144
   })
145 145
 }
146
+//获取所有部门和班组下人员
147
+export function getDeptUserTree(params) {
148
+  return request({
149
+    url: '/system/user/deptUserTree',
150
+    method: 'get',
151
+    params: params
152
+  })
153
+}

+ 330 - 137
src/views/assistant/components/dataBoard.vue

@@ -23,7 +23,12 @@
23 23
     <!-- 查询表单区域 -->
24 24
     <div class="query-form">
25 25
       <div class="form-container">
26
-        <el-form :model="queryForm" label-width="80px">
26
+        <el-form :model="queryForm" label-width="80px" :inline="true">
27
+          <el-form-item label="统计范围">
28
+            <el-tree-select v-model="queryForm.deptId" :data="deptTreeOptions"
29
+              :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择统计范围"
30
+              check-strictly clearable style="width: 300px" @change="handleDeptChange" />
31
+          </el-form-item>
27 32
           <el-form-item label="查询时间">
28 33
             <div class="form-row">
29 34
               <div class="form-content">
@@ -60,13 +65,15 @@
60 65
     </div>
61 66
 
62 67
     <!-- 勤务组织组件 -->
63
-    <DutyOrganization :query-form="currentQueryParams" />
68
+    <DutyOrganization :query-form="currentQueryParams" :selected-dept-object="selectedDeptObject" />
64 69
 
65 70
     <!-- 质控活动组件 -->
66
-    <QualityControl :query-form="currentQueryParams" />
71
+    <QualityControl :query-form="currentQueryParams" :selected-dept-object="selectedDeptObject" />
67 72
 
68 73
     <!-- 风险隐患组件 -->
69
-    <RiskHazard :query-form="currentQueryParams" />
74
+    <RiskHazard :query-form="currentQueryParams" :selected-dept-object="selectedDeptObject" />
75
+    <!-- 抽问抽答组件 -->
76
+    <QaAnalysis :query-form="currentQueryParams" :selected-dept-object="selectedDeptObject" />
70 77
   </div>
71 78
 </template>
72 79
 
@@ -78,13 +85,20 @@ import html2canvas from 'html2canvas'
78 85
 import jsPDF from 'jspdf'
79 86
 import { Document, Packer, Paragraph, ImageRun, HeadingLevel, AlignmentType } from 'docx'
80 87
 import { saveAs } from 'file-saver'
88
+import { getDeptUserTree } from '@/api/system/user.js'
81 89
 import DutyOrganization from './dutyOrganization.vue'
82 90
 import QualityControl from './qualityControl.vue'
83 91
 import RiskHazard from './riskHazard.vue'
92
+import QaAnalysis from './qaAnalysis.vue'
84 93
 
85 94
 // 定义emit事件
86 95
 const emit = defineEmits(['back'])
87
-
96
+// 部门树状数据
97
+const deptTreeOptions = ref([])
98
+// 选中的部门对象(单独变量)
99
+const selectedDeptObject = ref(null)
100
+// 临时存储部门选择,点击查询后才赋值
101
+const tempSelectedDeptObject = ref(null)
88 102
 // 生成年份选项(近10年)
89 103
 const currentYear = new Date().getFullYear()
90 104
 
@@ -140,9 +154,64 @@ const handleQuery = () => {
140 154
   console.log('查询参数:', queryForm.value)
141 155
   // 将查询表单值复制到当前查询参数
142 156
   currentQueryParams.value = { ...queryForm.value }
157
+  // 将临时存储的部门选择赋值给 selectedDeptObject
158
+  selectedDeptObject.value = tempSelectedDeptObject.value
143 159
   console.log('当前查询参数已更新:', currentQueryParams.value)
160
+  console.log('部门对象已更新:', selectedDeptObject.value)
144 161
 }
162
+// 处理部门选择变化
163
+const handleDeptChange = (selectedId) => {
164
+  console.log('部门选择变化:', selectedId)
165
+
166
+  if (!selectedId) {
167
+    // 清空选择
168
+    tempSelectedDeptObject.value = null
169
+    console.log('已清空部门选择(临时)')
170
+    return
171
+  }
145 172
 
173
+  // 在部门树中查找对应的对象
174
+  const findDeptInTree = (nodes, targetId) => {
175
+    for (const node of nodes) {
176
+      if (node.id === targetId) {
177
+        return node
178
+      }
179
+      if (node.children && node.children.length > 0) {
180
+        const found = findDeptInTree(node.children, targetId)
181
+        if (found) {
182
+          return found
183
+        }
184
+      }
185
+    }
186
+    return null
187
+  }
188
+
189
+  const foundDept = findDeptInTree(deptTreeOptions.value, selectedId)
190
+  if (foundDept) {
191
+    let user = !foundDept.deptType ? { deptType: 'USER' } : {};
192
+    tempSelectedDeptObject.value = { ...foundDept, ...user }
193
+    console.log('保存的部门对象(临时):', foundDept)
194
+  } else {
195
+    tempSelectedDeptObject.value = null
196
+    console.warn('未找到对应的部门对象')
197
+  }
198
+}
199
+
200
+// 获取部门树状数据
201
+const fetchDeptTree = async () => {
202
+  try {
203
+    const response = await getDeptUserTree({ deptId: 100 })
204
+    deptTreeOptions.value = response.data || []
205
+    console.log('部门树数据:', deptTreeOptions.value)
206
+    handleDeptChange(100)
207
+    // 部门树加载完成后,初始化默认的部门对象
208
+    handleQuery()
209
+
210
+  } catch (error) {
211
+    console.error('获取部门树状数据失败:', error)
212
+    ElMessage.error('获取部门数据失败')
213
+  }
214
+}
146 215
 // 重置表单
147 216
 const handleReset = () => {
148 217
   queryForm.value = {
@@ -154,6 +223,10 @@ const handleReset = () => {
154 223
   // 重置后自动查询接口
155 224
   handleQuery()
156 225
 }
226
+// 组件挂载时初始化
227
+onMounted(() => {
228
+  fetchDeptTree()
229
+})
157 230
 const handleExport = async () => {
158 231
 
159 232
   const btn = document.querySelector('.export-btn');
@@ -175,7 +248,7 @@ const handleExport = async () => {
175 248
 
176 249
     // 生成动态文档标题
177 250
     let reportTitle = '质控分析报告';
178
-    
251
+
179 252
     if (queryForm.value.year) {
180 253
       if (queryForm.value.dateRangeQueryType === 'YEAR') {
181 254
         reportTitle = `${queryForm.value.year}年质控分析报告`;
@@ -207,6 +280,28 @@ const handleExport = async () => {
207 280
       const sectionTitle = sectionTitles[i];
208 281
       const sectionName = sectionTitle.textContent;
209 282
       
283
+      // 检查组件是否显示(通过检查父组件的显示状态)
284
+      const componentContainer = sectionTitle.closest('.duty-organization, .quality-control, .risk-hazard, .qa-analysis');
285
+      const isComponentVisible = componentContainer && 
286
+        window.getComputedStyle(componentContainer).display !== 'none' &&
287
+        componentContainer.offsetHeight > 0 &&
288
+        componentContainer.offsetWidth > 0;
289
+
290
+      // 如果组件被隐藏,跳过该组件的导出
291
+      if (!isComponentVisible) {
292
+        // 跳过该组件下的所有面板项
293
+        while (panelIndex < panelItems.length) {
294
+          const panelItem = panelItems[panelIndex];
295
+          const panelSectionTitle = panelItem.closest('.quality-control, .duty-organization, .risk-hazard, .qa-analysis')?.querySelector('.section-title h2');
296
+          if (panelSectionTitle && panelSectionTitle.textContent === sectionName) {
297
+            panelIndex++;
298
+          } else {
299
+            break;
300
+          }
301
+        }
302
+        continue;
303
+      }
304
+
210 305
       // 添加组件大标题
211 306
       children.push(
212 307
         new Paragraph({
@@ -220,11 +315,18 @@ const handleExport = async () => {
220 315
       const componentPanelItems = [];
221 316
       while (panelIndex < panelItems.length) {
222 317
         const panelItem = panelItems[panelIndex];
223
-        
318
+
224 319
         // 检查这个面板项是否属于当前组件
225
-        const panelSectionTitle = panelItem.closest('.quality-control, .duty-organization, .risk-hazard')?.querySelector('.section-title h2');
320
+        const panelSectionTitle = panelItem.closest('.quality-control, .duty-organization, .risk-hazard, .qa-analysis')?.querySelector('.section-title h2');
226 321
         if (panelSectionTitle && panelSectionTitle.textContent === sectionName) {
227
-          componentPanelItems.push(panelItem);
322
+          // 检查面板项是否显示
323
+          const isPanelVisible = window.getComputedStyle(panelItem).display !== 'none' &&
324
+            panelItem.offsetHeight > 0 &&
325
+            panelItem.offsetWidth > 0;
326
+          
327
+          if (isPanelVisible) {
328
+            componentPanelItems.push(panelItem);
329
+          }
228 330
           panelIndex++;
229 331
         } else {
230 332
           break; // 遇到下一个组件的面板项,停止处理
@@ -234,140 +336,231 @@ const handleExport = async () => {
234 336
       // 处理当前组件下的面板项
235 337
       for (let j = 0; j < componentPanelItems.length; j++) {
236 338
         const panelItem = componentPanelItems[j];
237
-       
238
-       // 获取面板项标题
239
-       const titleElement = panelItem.querySelector('.panel-header h2, .panel-header h3');
240
-       const title = titleElement?.textContent || `图表${j + 1}`;
241
-       
242
-       // 添加面板项标题
243
-       children.push(
244
-         new Paragraph({
245
-           text: `${i + 1}.${j + 1} ${title}`,
246
-           heading: HeadingLevel.HEADING_2,
247
-         }),
248
-         new Paragraph({ text: '' }) // 在小标题后添加空行
249
-       );
250
-
251
-       // 获取面板项中的所有相关元素,按DOM顺序排列
252
-       const contentElements = panelItem.querySelectorAll('.stat-card, .describe-card, .chart-container');
253
-       
254
-       // 按DOM顺序处理每个元素
255
-       for (let k = 0; k < contentElements.length; k++) {
256
-         const element = contentElements[k];
257
-         
258
-         if (element.classList.contains('stat-card') || element.classList.contains('describe-card')) {
259
-           // 处理描述内容卡片
260
-           const description = element.textContent || '';
261
-           if (description) {
262
-             children.push(
263
-               new Paragraph({
264
-                 text: description,
265
-                 alignment: AlignmentType.LEFT,
266
-               }),
267
-               new Paragraph({ text: '' }) // 空行
268
-             );
269
-           }
270
-         } else if (element.classList.contains('chart-container')) {
271
-           // 处理图表容器
272
-           // 截图
273
-           const canvas = await html2canvas(element, {
274
-             backgroundColor: '#ffffff',
275
-             scale: 2, // 高清截图
276
-             logging: false
277
-           });
278
-
279
-           // 转换为 base64
280
-           const imageData = canvas.toDataURL('image/png');
281
-           const base64Data = imageData.replace(/^data:image\/png;base64,/, '');
282
-
283
-           // 计算保持宽高比的尺寸
284
-           const maxWidth = 500; // Word中最大宽度
285
-           const originalWidth = canvas.width;
286
-           const originalHeight = canvas.height;
287
-           const aspectRatio = originalWidth / originalHeight;
288
-
289
-           let finalWidth = maxWidth;
290
-           let finalHeight = maxWidth / aspectRatio;
291
-
292
-           // 如果高度超过限制,按高度缩放
293
-           const maxHeight = 400;
294
-           if (finalHeight > maxHeight) {
295
-             finalHeight = maxHeight;
296
-             finalWidth = maxHeight * aspectRatio;
297
-           }
298
-
299
-           // 添加图片(保持原始宽高比)
300
-           children.push(
301
-             new Paragraph({
302
-               children: [
303
-                 new ImageRun({
304
-                   data: Uint8Array.from(atob(base64Data), c => c.charCodeAt(0)),
305
-                   transformation: {
306
-                     width: Math.round(finalWidth),
307
-                     height: Math.round(finalHeight)
308
-                   }
309
-                 })
310
-               ],
311
-               alignment: AlignmentType.CENTER,
312
-             }),
313
-             new Paragraph({ text: '' }) // 图片后空行
314
-           );
315
-
316
-           // 小延迟,让UI更新
317
-           await new Promise(resolve => setTimeout(resolve, 100));
318
-         }
319
-       }
320
-
321
-      // 获取表格容器
322
-      const tableContainer = panelItem.querySelector('.table-container');
323
-      if (tableContainer) {
324
-        // 截图
325
-        const canvas = await html2canvas(tableContainer, {
326
-          backgroundColor: '#ffffff',
327
-          scale: 2, // 高清截图
328
-          logging: false
329
-        });
330
-
331
-        // 转换为 base64
332
-        const imageData = canvas.toDataURL('image/png');
333
-        const base64Data = imageData.replace(/^data:image\/png;base64,/, '');
334
-
335
-        // 计算保持宽高比的尺寸
336
-        const maxWidth = 500; // Word中最大宽度
337
-        const originalWidth = canvas.width;
338
-        const originalHeight = canvas.height;
339
-        const aspectRatio = originalWidth / originalHeight;
340
-
341
-        let finalWidth = maxWidth;
342
-        let finalHeight = maxWidth / aspectRatio;
343
-
344
-        // 如果高度超过限制,按高度缩放
345
-        const maxHeight = 400;
346
-        if (finalHeight > maxHeight) {
347
-          finalHeight = maxHeight;
348
-          finalWidth = maxHeight * aspectRatio;
349
-        }
350 339
 
351
-        // 添加表格图片(保持原始宽高比)
340
+        // 获取面板项标题
341
+        const titleElement = panelItem.querySelector('.panel-header h2, .panel-header h3');
342
+        const title = titleElement?.textContent || `图表${j + 1}`;
343
+
344
+        // 添加面板项标题
352 345
         children.push(
353 346
           new Paragraph({
354
-            children: [
355
-              new ImageRun({
356
-                data: Uint8Array.from(atob(base64Data), c => c.charCodeAt(0)),
357
-                transformation: {
358
-                  width: Math.round(finalWidth),
359
-                  height: Math.round(finalHeight)
360
-                }
361
-              })
362
-            ],
363
-            alignment: AlignmentType.CENTER,
347
+            text: `${i + 1}.${j + 1} ${title}`,
348
+            heading: HeadingLevel.HEADING_2,
364 349
           }),
365
-          new Paragraph({ text: '' }) // 图片后空行
350
+          new Paragraph({ text: '' }) // 在小标题后添加空行
366 351
         );
367 352
 
368
-        // 小延迟,让UI更新
369
-        await new Promise(resolve => setTimeout(resolve, 100));
370
-      }
353
+        // 获取面板项中的所有相关元素,按DOM顺序排列
354
+        const contentElements = panelItem.querySelectorAll('.stat-card, .describe-card, .chart-container');
355
+
356
+        // 按DOM顺序处理每个元素
357
+        for (let k = 0; k < contentElements.length; k++) {
358
+          const element = contentElements[k];
359
+
360
+          // 检查元素是否实际可见(包括检查父容器)
361
+          const isElementVisible = window.getComputedStyle(element).display !== 'none' &&
362
+            element.offsetHeight > 0 &&
363
+            element.offsetWidth > 0;
364
+
365
+          if (!isElementVisible) {
366
+            console.warn('元素被隐藏,跳过:', element.classList);
367
+            continue;
368
+          }
369
+
370
+          if (element.classList.contains('stat-card') || element.classList.contains('describe-card')) {
371
+            // 处理描述内容卡片
372
+            const description = element.textContent || '';
373
+            if (description && description.trim() !== '') {
374
+              children.push(
375
+                new Paragraph({
376
+                  text: description,
377
+                  alignment: AlignmentType.LEFT,
378
+                }),
379
+                new Paragraph({ text: '' }) // 空行
380
+              );
381
+            }
382
+          } else if (element.classList.contains('chart-container')) {
383
+            // 处理图表容器
384
+            // 截图
385
+            const canvas = await html2canvas(element, {
386
+              backgroundColor: '#ffffff',
387
+              scale: 2, // 高清截图
388
+              logging: false
389
+            });
390
+
391
+            // 转换为 base64
392
+            const imageData = canvas.toDataURL('image/png');
393
+            const base64Data = imageData.replace(/^data:image\/png;base64,/, '');
394
+
395
+            // 验证 base64 数据格式
396
+            if (!base64Data || base64Data.trim() === '') {
397
+              console.warn('截图数据为空,跳过该图表');
398
+              continue;
399
+            }
400
+
401
+            // 计算保持宽高比的尺寸
402
+            const maxWidth = 500; // Word中最大宽度
403
+            const originalWidth = canvas.width;
404
+            const originalHeight = canvas.height;
405
+            const aspectRatio = originalWidth / originalHeight;
406
+
407
+            let finalWidth = maxWidth;
408
+            let finalHeight = maxWidth / aspectRatio;
409
+
410
+            // 如果高度超过限制,按高度缩放
411
+            const maxHeight = 400;
412
+            if (finalHeight > maxHeight) {
413
+              finalHeight = maxHeight;
414
+              finalWidth = maxHeight * aspectRatio;
415
+            }
416
+
417
+            // 添加图片(保持原始宽高比)
418
+            try {
419
+              const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
420
+              children.push(
421
+                new Paragraph({
422
+                  children: [
423
+                    new ImageRun({
424
+                      data: imageBytes,
425
+                      transformation: {
426
+                        width: Math.round(finalWidth),
427
+                        height: Math.round(finalHeight)
428
+                      }
429
+                    })
430
+                  ],
431
+                  alignment: AlignmentType.CENTER,
432
+                }),
433
+                new Paragraph({ text: '' }) // 图片后空行
434
+              );
435
+            } catch (error) {
436
+              console.error('图片数据处理失败:', error);
437
+              // 跳过该图表,继续处理其他内容
438
+              continue;
439
+            }
440
+
441
+            // 小延迟,让UI更新
442
+            await new Promise(resolve => setTimeout(resolve, 100));
443
+          }
444
+        }
445
+
446
+        // 获取表格容器
447
+        const tableContainer = panelItem.querySelector('.table-container');
448
+        if (tableContainer) {
449
+          // 临时移除表格容器的高度和宽度限制,让表格显示所有内容
450
+          const originalMaxHeight = tableContainer.style.maxHeight || '';
451
+          const originalOverflowY = tableContainer.style.overflowY || '';
452
+          const originalOverflowX = tableContainer.style.overflowX || '';
453
+          const originalTableContainerWidth = tableContainer.style.width || '';
454
+
455
+          tableContainer.style.maxHeight = 'none';
456
+          tableContainer.style.overflowY = 'visible';
457
+          tableContainer.style.overflowX = 'visible';
458
+          tableContainer.style.width = '800px'; // 统一表格容器宽度
459
+
460
+          // 临时移除表格的高度和宽度属性,让Element Plus表格显示所有行和列
461
+          const tableElement = tableContainer.querySelector('.el-table');
462
+          let originalTableHeight = '';
463
+          let originalTableWidth = '';
464
+          if (tableElement) {
465
+            originalTableHeight = tableElement.style.height || '';
466
+            originalTableWidth = tableElement.style.width || '';
467
+            tableElement.style.height = 'auto';
468
+            tableElement.style.width = '800px'; // 统一表格宽度
469
+
470
+            // 临时移除表格内部滚动容器的限制
471
+            const bodyWrapper = tableElement.querySelector('.el-table__body-wrapper');
472
+            if (bodyWrapper) {
473
+              bodyWrapper.style.overflowX = 'visible';
474
+              bodyWrapper.style.overflowY = 'visible';
475
+            }
476
+          }
477
+
478
+          // 等待DOM更新
479
+          await new Promise(resolve => setTimeout(resolve, 200));
480
+
481
+          // 截图 - 使用固定宽度确保所有表格宽度一致
482
+          const canvas = await html2canvas(tableContainer, {
483
+            backgroundColor: '#ffffff',
484
+            scale: 1.5, // 适当降低缩放比例以避免图片过大
485
+            logging: false,
486
+            useCORS: true,
487
+            allowTaint: true,
488
+            scrollX: 0, // 确保从最左侧开始截图
489
+            scrollY: 0, // 确保从最顶部开始截图
490
+            width: 800, // 固定宽度,确保所有表格宽度一致
491
+            height: tableContainer.scrollHeight // 使用完整高度
492
+          });
493
+
494
+          // 恢复原始样式
495
+          tableContainer.style.maxHeight = originalMaxHeight;
496
+          tableContainer.style.overflowY = originalOverflowY;
497
+          tableContainer.style.overflowX = originalOverflowX;
498
+          tableContainer.style.width = originalTableContainerWidth;
499
+
500
+          if (tableElement) {
501
+            tableElement.style.height = originalTableHeight;
502
+            tableElement.style.width = originalTableWidth;
503
+
504
+            const bodyWrapper = tableElement.querySelector('.el-table__body-wrapper');
505
+            if (bodyWrapper) {
506
+              bodyWrapper.style.overflowX = '';
507
+              bodyWrapper.style.overflowY = '';
508
+            }
509
+          }
510
+
511
+          // 转换为 base64
512
+          const imageData = canvas.toDataURL('image/png');
513
+          const base64Data = imageData.replace(/^data:image\/png;base64,/, '');
514
+
515
+          // 验证 base64 数据格式
516
+          if (!base64Data || base64Data.trim() === '') {
517
+            console.warn('表格截图数据为空,跳过该表格');
518
+            continue;
519
+          }
520
+
521
+          // 计算保持宽高比的尺寸
522
+          const maxWidth = 500; // Word中最大宽度
523
+          const originalWidth = canvas.width;
524
+          const originalHeight = canvas.height;
525
+          const aspectRatio = originalWidth / originalHeight;
526
+
527
+          let finalWidth = maxWidth;
528
+          let finalHeight = maxWidth / aspectRatio;
529
+
530
+          // 如果高度超过限制,按高度缩放
531
+          const maxHeight = 400;
532
+          if (finalHeight > maxHeight) {
533
+            finalHeight = maxHeight;
534
+            finalWidth = maxHeight * aspectRatio;
535
+          }
536
+
537
+          // 添加表格图片(保持原始宽高比)
538
+          try {
539
+            const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
540
+            children.push(
541
+              new Paragraph({
542
+                children: [
543
+                  new ImageRun({
544
+                    data: imageBytes,
545
+                    transformation: {
546
+                      width: Math.round(finalWidth),
547
+                      height: Math.round(finalHeight)
548
+                    }
549
+                  })
550
+                ],
551
+                alignment: AlignmentType.CENTER,
552
+              }),
553
+              new Paragraph({ text: '' }) // 图片后空行
554
+            );
555
+          } catch (error) {
556
+            console.error('表格图片数据处理失败:', error);
557
+            // 跳过该表格,继续处理其他内容
558
+            continue;
559
+          }
560
+
561
+          // 小延迟,让UI更新
562
+          await new Promise(resolve => setTimeout(resolve, 100));
563
+        }
371 564
       }
372 565
     }
373 566
 

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

@@ -16,7 +16,9 @@
16 16
         <!-- 统计卡片 -->
17 17
         <div class="stat-card">
18 18
           <div class="stat-content">
19
-            全站出勤<span>{{ attendanceData && attendanceData.length > 3 ? attendanceData[3]?.currentValue : 0
19
+            {{ attendanceData && attendanceData.length > 0 ? attendanceData[attendanceData.length -
20
+              1]?.deptName : ''
21
+            }}出勤<span>{{ attendanceData && attendanceData.length > 3 ? attendanceData[3]?.currentValue : 0
20 22
             }}</span>人次,同比
21 23
             <span
22 24
               :class="getTrendClass(attendanceData && attendanceData.length > 3 ? attendanceData[3]?.yearOnYearValue : 0)">{{
@@ -128,6 +130,10 @@ const props = defineProps({
128 130
       quarter: '',
129 131
       month: ''
130 132
     })
133
+  },
134
+  selectedDeptObject: {
135
+    type: Object,
136
+    default: null
131 137
   }
132 138
 })
133 139
 
@@ -146,6 +152,10 @@ const attendanceData = ref({})
146 152
 const trendData = ref({})
147 153
 const qualificationPieData = ref({})
148 154
 const qualificationBarData = ref({})
155
+// 计算属性:处理selectedDeptObject逻辑
156
+const selectedDeptType = computed(() => {
157
+  return props.selectedDeptObject && props.selectedDeptObject?.deptType
158
+})
149 159
 
150 160
 // 计算属性:动态生成资质等级分布描述第一部分(句号前)
151 161
 const qualificationPieDescriptionPart1 = computed(() => {
@@ -259,24 +269,30 @@ const fetchDutyOrganizationData = async (queryParams) => {
259 269
   try {
260 270
     // 处理query参数
261 271
     const processedParams = processQueryParams(queryParams)
272
+    const selectedDept = props.selectedDeptObject
273
+    const { deptType = "", id } = selectedDept ? selectedDept : { deptType: "", id: "" }
262 274
 
275
+    let calculateParams = {
276
+      ...(['TEAMS', 'DEPARTMENT','BRIGADE'].includes(deptType) ? { deptId: id } : {}),
277
+      ...(deptType == 'USER' ? { userId: id } : {})
278
+    }
263 279
     // 获取出勤人次分析数据
264
-    const attendanceResponse = await getCalculate(processedParams)
280
+    const attendanceResponse = await getCalculate({ ...processedParams, ...calculateParams })
265 281
     console.log('出勤人次分析数据:', attendanceResponse)
266 282
     attendanceData.value = attendanceResponse || []
267 283
 
268 284
     // 获取出勤人次趋势数据
269
-    const trendResponse = await getCalculateTrendData(processedParams)
285
+    const trendResponse = await getCalculateTrendData({ ...processedParams, ...calculateParams })
270 286
     console.log('出勤人次趋势数据:', trendResponse)
271 287
     trendData.value = trendResponse || []
272 288
 
273 289
     // 获取资质等级分布饼图数据
274
-    const pieResponse = await getQualificationPieChart(processedParams)
290
+    const pieResponse = await getQualificationPieChart({ ...processedParams, ...calculateParams })
275 291
     console.log('资质等级分布饼图数据:', pieResponse)
276 292
     qualificationPieData.value = pieResponse.data || []
277 293
 
278 294
     // 获取资质等级分布柱状图数据
279
-    const barResponse = await getQualificationBarChart(processedParams)
295
+    const barResponse = await getQualificationBarChart({ ...processedParams, ...calculateParams })
280 296
     console.log('资质等级分布柱状图数据:', barResponse)
281 297
     qualificationBarData.value = barResponse.data?.brigades || []
282 298
 
@@ -294,7 +310,7 @@ watch(() => props.queryForm, (newQueryForm) => {
294 310
   if (newQueryForm.dateRangeQueryType && newQueryForm.year) {
295 311
     fetchDutyOrganizationData(newQueryForm)
296 312
   }
297
-}, { deep: true, immediate: true })
313
+}, { deep: true })
298 314
 
299 315
 // 饼图配置
300 316
 const pieOptions = {
@@ -708,7 +724,7 @@ const updateTrendBarChart = () => {
708 724
 const updateAttendanceBarChart = () => {
709 725
   if (trendData.value && Array.isArray(trendData.value)) {
710 726
     const trendList = trendData.value
711
-    
727
+
712 728
     // 提取横坐标数据(timeLabel字段)
713 729
     const xAxisData = trendList.map(item => item.timeLabel || '未知时间')
714 730
 

+ 761 - 0
src/views/assistant/components/qaAnalysis.vue

@@ -0,0 +1,761 @@
1
+<template>
2
+  <div class="qa-analysis">
3
+    <!-- 抽问抽答标题 -->
4
+    <div class="section-title">
5
+      <h2>抽问抽答</h2>
6
+    </div>
7
+
8
+    <!-- 三个横向均分的内容区域 -->
9
+    <div class="three-panel-layout">
10
+      <!-- 第一个:答题完成趋势 -->
11
+      <div class="panel-item">
12
+        <div class="panel-header">
13
+          <h3>答题完成趋势</h3>
14
+        </div>
15
+
16
+        <!-- 描述卡片 -->
17
+        <div class="describe-card">
18
+          <div class="describe-content">
19
+            {{ completionTrendDescription }}
20
+          </div>
21
+        </div>
22
+
23
+        <!-- 折线图 -->
24
+        <div class="chart-container">
25
+          <div ref="completionTrendChartRef" class="echarts-chart"></div>
26
+        </div>
27
+      </div>
28
+
29
+      <!-- 第二个:错题分布 -->
30
+      <div class="panel-item">
31
+        <div class="panel-header">
32
+          <h3>错题分布</h3>
33
+        </div>
34
+
35
+        <!-- 描述卡片 -->
36
+        <div class="describe-card">
37
+          <div class="describe-content">
38
+            {{ errorDistributionDescription }}
39
+          </div>
40
+        </div>
41
+
42
+        <!-- 饼图 -->
43
+        <div class="chart-container">
44
+          <div ref="errorDistributionChartRef" class="echarts-chart"></div>
45
+        </div>
46
+      </div>
47
+
48
+      <!-- 第三个:各科错题分布对比 -->
49
+      <div class="panel-item" v-show="isStationType">
50
+        <div class="panel-header">
51
+          <h3>各科错题分布对比</h3>
52
+        </div>
53
+
54
+        <!-- 描述卡片 -->
55
+        <div class="describe-card">
56
+          <div class="describe-content">
57
+            {{ departmentComparisonDescription }}
58
+          </div>
59
+        </div>
60
+
61
+        <!-- 雷达图 -->
62
+        <div class="chart-container">
63
+          <div ref="departmentComparisonChartRef" class="echarts-chart"></div>
64
+        </div>
65
+      </div>
66
+    </div>
67
+  </div>
68
+</template>
69
+
70
+<script setup>
71
+import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
72
+import { useEcharts } from '@/hooks/chart.js'
73
+import useUserStore from '@/store/modules/user'
74
+import { getCompletionTrend, getWrongAnalysisOverview, getWrongAnalysisRadar } from '@/api/assistant/assistant.js'
75
+
76
+const userStore = useUserStore()
77
+
78
+// 计算属性:用户角色
79
+const role = computed(() => {
80
+  return userStore.roles || []
81
+})
82
+
83
+// 定义props接收queryForm参数
84
+const props = defineProps({
85
+  queryForm: {
86
+    type: Object,
87
+    default: () => ({
88
+      dateRangeQueryType: '',
89
+      year: '',
90
+      quarter: '',
91
+      month: ''
92
+    })
93
+  },
94
+  selectedDeptObject: {
95
+    type: Object,
96
+    default: null
97
+  }
98
+})
99
+
100
+// 计算属性:处理selectedDeptObject逻辑
101
+const selectedDeptType = computed(() => {
102
+  return props.selectedDeptObject && props.selectedDeptObject?.deptType
103
+})
104
+
105
+const selectedDeptName = computed(() => {
106
+
107
+  return props.selectedDeptObject && props.selectedDeptObject?.label
108
+})
109
+
110
+// 计算属性:检查是否为STATION类型
111
+const isStationType = computed(() => {
112
+  return selectedDeptType.value === 'STATION' || !selectedDeptType.value
113
+})
114
+
115
+// 统一的参数处理函数
116
+const processQueryParams = (queryParams) => {
117
+  const processedParams = { ...queryParams }
118
+
119
+  // 根据部门类型添加相应的参数
120
+  const selectedDept = props.selectedDeptObject
121
+  const { deptType = "", id } = selectedDept ? selectedDept : { deptType: "", id: "" }
122
+  processedParams.scopeType = deptType === 'STATION' ? 'STATION' : deptType == 'DEPARTMENT' ? 'DEPT' : deptType == 'TEAMS' ? 'TEAM' : deptType == 'USER' ? 'USER' : ''
123
+  processedParams.scopeId = id
124
+  delete processedParams.deptId;
125
+  delete processedParams.teamId;
126
+  delete processedParams.userId;
127
+
128
+  return processedParams
129
+}
130
+
131
+// 图表容器引用
132
+const completionTrendChartRef = ref(null)
133
+const errorDistributionChartRef = ref(null)
134
+const departmentComparisonChartRef = ref(null)
135
+
136
+// 图表实例
137
+const { setOption: setCompletionTrendOption, dispose: disposeCompletionTrend } = useEcharts(completionTrendChartRef)
138
+const { setOption: setErrorDistributionOption, dispose: disposeErrorDistribution } = useEcharts(errorDistributionChartRef)
139
+const { setOption: setDepartmentComparisonOption, dispose: disposeDepartmentComparison } = useEcharts(departmentComparisonChartRef)
140
+
141
+
142
+
143
+
144
+
145
+// 描述卡片文本变量
146
+const completionTrendDescription = ref('')
147
+const errorDistributionDescription = ref('')
148
+const departmentComparisonDescription = ref('')
149
+
150
+// API数据处理函数
151
+const processApiData = (apiData) => {
152
+  console.log('processApiData', apiData);
153
+
154
+  // 根据API返回的数据结构进行处理
155
+  if (!apiData || !apiData.model) {
156
+    return { processedData: [], xAxisData: [] };
157
+  }
158
+
159
+  const { xAxis, series } = apiData.model;
160
+
161
+  // 处理x轴数据
162
+  const xAxisData = xAxis || [];
163
+
164
+  // 处理系列数据
165
+  const processedData = [];
166
+
167
+  if (series && series.length > 0) {
168
+    // 如果是站长角色,显示多条线;如果是科长角色,显示单条线
169
+    // 这里简化处理,只显示第一条线
170
+    for (let i = 0; i < series.length; i++) {
171
+      // 确保数据是数值类型
172
+      const numericData = series[i].data.map(item => {
173
+        if (typeof item === 'string') {
174
+          return parseFloat(item) || 0;
175
+        }
176
+        return Number(item) || 0;
177
+      });
178
+
179
+      processedData.push({
180
+        name: series[i].name,
181
+        type: 'line',
182
+        data: numericData,
183
+
184
+      });
185
+    }
186
+  }
187
+
188
+  return { processedData, xAxisData };
189
+}
190
+
191
+// 处理总体问题分布数据(饼图)
192
+const processOverviewData = (apiData) => {
193
+  const { categories } = apiData.model;
194
+
195
+  if (!categories.length) return [];
196
+
197
+  return categories.map(category => ({
198
+    id: category.id,
199
+    value: category.count,
200
+    name: category.name,
201
+
202
+  }));
203
+}
204
+
205
+// 处理雷达图数据
206
+const processRadarData = (apiData) => {
207
+  const { indicators, series } = apiData.model;
208
+
209
+  if (!apiData || !indicators || !series) return [];
210
+
211
+  return { indicators, series }
212
+}
213
+
214
+// 获取答题完成趋势数据
215
+const fetchCompletionTrendData = async () => {
216
+  try {
217
+    // 使用统一的参数处理函数
218
+    const processedParams = processQueryParams(props.queryForm)
219
+    const response = await getCompletionTrend(processedParams);
220
+    const { processedData, xAxisData } = processApiData(response);
221
+
222
+    let stationData = processedData.find(item => item.name === '全站')?.data || 0;
223
+
224
+
225
+    // 找到除了全站之外的其他科室数据
226
+    const otherDepartments = processedData.filter(item => item.name !== '全站');
227
+
228
+    let maxData = null;
229
+    let maxDataPercent = '0.00';
230
+
231
+    if (otherDepartments.length > 0) {
232
+      // 如果有其他科室数据,找出完成人数最多的科室
233
+      maxData = otherDepartments.reduce((max, item) => {
234
+        if (item.data[2] > max.data[2]) {
235
+          return item;
236
+        }
237
+        return max;
238
+      }, otherDepartments[0]);
239
+
240
+      // 防止除数为0或NaN
241
+      const stationLastValue = stationData[stationData.length - 1] || 0;
242
+      if (stationLastValue > 0) {
243
+        maxDataPercent = ((maxData.data[maxData.data.length - 1] / stationLastValue) * 100).toFixed(2);
244
+      } else {
245
+        maxDataPercent = '0.00';
246
+      }
247
+    }
248
+
249
+    //计算同比
250
+    console.log('maxData', maxData)
251
+
252
+    // 安全获取数组元素,防止NaN
253
+    const lastValue = stationData[stationData.length - 1] || 0;
254
+    const firstValue = stationData[0] || 0;
255
+    const secondValue = stationData[1] || 0;
256
+
257
+    // 计算同比
258
+    let tongValue = 0;
259
+    if (firstValue > 0) {
260
+      tongValue = ((lastValue - firstValue) / firstValue) * 100;
261
+    }
262
+    let tong = firstValue === 0 ? '--' : (tongValue > 0 ? '+' : '') + tongValue.toFixed(2);
263
+
264
+    // 计算环比
265
+    let huanValue = 0;
266
+    if (secondValue > 0) {
267
+      huanValue = ((lastValue - secondValue) / secondValue) * 100;
268
+    }
269
+    let huan = secondValue === 0 ? '--' : (huanValue > 0 ? '+' : '') + huanValue.toFixed(2);
270
+    // 安全获取maxData数组元素,防止NaN
271
+    const maxDataLastValue = maxData.data[maxData.data.length - 1] || 0;
272
+    const maxDataFirstValue = maxData.data[0] || 0;
273
+    const maxDataSecondValue = maxData.data[1] || 0;
274
+    // 为maxData也计算同比和环比
275
+    let maxDataTong = '--';
276
+    let maxDataHuan = '--';
277
+    if (maxData && maxData.data) {
278
+
279
+
280
+      // 计算maxData同比
281
+      let maxDataTongValue = 0;
282
+      if (maxDataFirstValue > 0) {
283
+        maxDataTongValue = ((maxDataLastValue - maxDataFirstValue) / maxDataFirstValue) * 100;
284
+      }
285
+      maxDataTong = maxDataFirstValue === 0 ? '--' : (maxDataTongValue > 0 ? '+' : (maxDataTongValue < 0 ? '' : '')) + maxDataTongValue.toFixed(2);
286
+
287
+      // 计算maxData环比
288
+      let maxDataHuanValue = 0;
289
+      if (maxDataSecondValue > 0) {
290
+        maxDataHuanValue = ((maxDataLastValue - maxDataSecondValue) / maxDataSecondValue) * 100;
291
+      }
292
+      maxDataHuan = maxDataSecondValue === 0 ? '--' : (maxDataHuanValue > 0 ? '+' : (maxDataHuanValue < 0 ? '' : '')) + maxDataHuanValue.toFixed(2);
293
+    }
294
+
295
+    // 根据查询条件决定显示内容
296
+    let descriptionText = '';
297
+    let totalPerson = stationData[stationData.length - 1] || 0;
298
+    if (maxData) {
299
+      // 有其他科室数据时显示科室信息
300
+      if (props.queryForm.dateRangeQueryType === 'YEAR') {
301
+        const firstYearStr = isStationType.value ? `${selectedDeptName.value}共答题${totalPerson}人次,同比${tong}%,` : '';
302
+        const secondYearStr = isStationType.value ? `${maxData.name}共答题${maxData.data[maxData.data.length - 1]}人次,占比最高为${maxDataPercent}%。` : `${selectedDeptName.value}完成${maxDataLastValue}人次,同比${maxDataTong}%。`
303
+        // 年查询:只显示同比,隐藏环比
304
+        descriptionText = `${firstYearStr}${secondYearStr}`;
305
+      } else {
306
+        const firstNotYearStr = isStationType.value ? `${selectedDeptName.value}共答题${totalPerson}人次,同比${tong}%,环比${huan}%,` : '';
307
+        const secondNotYearStr = isStationType.value ? `${maxData.name}共答题${maxData.data[maxData.data.length - 1]}人次,占比最高为${maxDataPercent}%。` : `${selectedDeptName.value}完成${maxDataLastValue}人次,同比${maxDataTong}%,环比${maxDataHuan}%。`
308
+        // 季度或月查询:显示同比和环比
309
+        descriptionText = `${firstNotYearStr}${secondNotYearStr}`;
310
+      }
311
+    } else {
312
+      // 只有全站数据时显示简化的描述
313
+      if (props.queryForm.dateRangeQueryType === 'YEAR') {
314
+        descriptionText = `${selectedDeptName.value}共答题${totalPerson}人次,同比${tong}%。`;
315
+      } else {
316
+        descriptionText = `${selectedDeptName.value}共答题${totalPerson}人次,同比${tong}%,环比${huan}%。`;
317
+      }
318
+    }
319
+
320
+    // 设置描述卡片文本
321
+    completionTrendDescription.value = descriptionText;
322
+    let data = processedData.map(series => ({
323
+      ...series,
324
+      symbol: 'circle', // 添加数据点标记
325
+      symbolSize: 6,
326
+      smooth: true,
327
+      lineStyle: {
328
+        width: 3
329
+      },
330
+    }))
331
+
332
+    // 更新答题完成趋势图表
333
+    setCompletionTrendOption({
334
+      tooltip: {
335
+        trigger: 'axis'
336
+      },
337
+      legend: {
338
+        data: processedData.map(item => item.name)
339
+      },
340
+      grid: {
341
+        left: '3%',
342
+        right: '4%',
343
+        bottom: '3%',
344
+        containLabel: true
345
+      },
346
+      xAxis: {
347
+        type: 'category',
348
+        boundaryGap: false,
349
+        data: xAxisData
350
+      },
351
+      yAxis: {
352
+        type: 'value'
353
+        // 移除min/max限制,让ECharts自动计算范围
354
+      },
355
+      series: data
356
+    }, true);
357
+  } catch (error) {
358
+    console.error('获取答题完成趋势数据失败:', error);
359
+    // 使用默认数据作为后备
360
+    setCompletionTrendOption({
361
+      tooltip: {
362
+        trigger: 'axis'
363
+      },
364
+      legend: {
365
+        data: ['完成率']
366
+      },
367
+      grid: {
368
+        left: '3%',
369
+        right: '4%',
370
+        bottom: '3%',
371
+        containLabel: true
372
+      },
373
+      xAxis: {
374
+        type: 'category',
375
+        boundaryGap: false,
376
+        data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
377
+      },
378
+      yAxis: {
379
+        type: 'value',
380
+        min: 70,
381
+        max: 100
382
+      },
383
+      series: [
384
+        {
385
+          name: '完成率',
386
+          type: 'line',
387
+          smooth: true,
388
+          data: [85, 88, 92, 90, 87, 89, 91, 93, 95, 94, 92, 90],
389
+          itemStyle: {
390
+            color: '#5470c6'
391
+          }
392
+        }
393
+      ]
394
+    });
395
+  }
396
+}
397
+
398
+// 获取错题分布数据(饼图)
399
+const fetchWrongAnalysisOverviewData = async () => {
400
+  try {
401
+    // 使用统一的参数处理函数
402
+    const processedParams = processQueryParams(props.queryForm)
403
+    const response = await getWrongAnalysisOverview(processedParams);
404
+    const pieData = processOverviewData(response);
405
+    console.log('fetchWrongAnalysisOverviewData', pieData);
406
+    let sortData = pieData.sort((a, b) => b.value - a.value);
407
+    let total = pieData.reduce((acc, cur) => acc + cur.value, 0);
408
+    console.log('total', (sortData[0]?.value / total) * 100)
409
+    // 设置描述卡片文本
410
+    const firstPercentage = ((sortData[0]?.value / total) * 100);
411
+    const secondPercentage = ((sortData[1]?.value / total) * 100);
412
+
413
+    errorDistributionDescription.value = `错题主要集中于:${sortData[0]?.name || '--'},占比:${isNaN(firstPercentage) ? '--' : firstPercentage.toFixed(2)}%;其次为:${sortData[1]?.name || '--'},占比:${isNaN(secondPercentage) ? '--' : secondPercentage.toFixed(2)}%,为靶向培训、题库优化提供核心依据。`
414
+
415
+    // 更新错题分布饼图
416
+    setErrorDistributionOption({
417
+      tooltip: {
418
+        trigger: 'item'
419
+      },
420
+      legend: {
421
+        orient: 'vertical',
422
+        left: 10,
423
+        top: 'center'
424
+      },
425
+
426
+      tooltip: {
427
+        trigger: 'item',
428
+        formatter: '{a} <br/>{b}: {c} ({d}%)'
429
+      },
430
+      series: [
431
+        {
432
+          name: '错题分布',
433
+          type: 'pie',
434
+          radius: ['40%', '70%'],
435
+          avoidLabelOverlap: false,
436
+          itemStyle: {
437
+            borderRadius: 10,
438
+            borderColor: '#fff',
439
+            borderWidth: 2
440
+          },
441
+
442
+          emphasis: {
443
+            label: {
444
+              show: true,
445
+              fontSize: 12,
446
+              fontWeight: 'bold'
447
+            }
448
+          },
449
+          label: {
450
+            show: true,
451
+            formatter: '{b}\n{c} ({d}%)',
452
+            fontSize: 12,
453
+            fontWeight: 'normal'
454
+          },
455
+          labelLine: {
456
+            show: true,
457
+            length: 10,
458
+            length2: 20
459
+          },
460
+          data: pieData
461
+        }
462
+      ]
463
+    });
464
+  } catch (error) {
465
+    console.error('获取错题分布数据失败:', error);
466
+    // 使用默认数据作为后备
467
+    setErrorDistributionOption({
468
+      tooltip: {
469
+        trigger: 'item'
470
+      },
471
+      legend: {
472
+        orient: 'vertical',
473
+        right: 10,
474
+        top: 'center'
475
+      },
476
+      series: [
477
+        {
478
+          name: '错题分布',
479
+          type: 'pie',
480
+          radius: ['40%', '70%'],
481
+          avoidLabelOverlap: false,
482
+          itemStyle: {
483
+            borderRadius: 10,
484
+            borderColor: '#fff',
485
+            borderWidth: 2
486
+          },
487
+          label: {
488
+            show: false,
489
+            position: 'center'
490
+          },
491
+          emphasis: {
492
+            label: {
493
+              show: true,
494
+              fontSize: 12,
495
+              fontWeight: 'bold'
496
+            }
497
+          },
498
+          labelLine: {
499
+            show: false
500
+          },
501
+          data: [
502
+            { value: 45.3, name: '操作执行类' },
503
+            { value: 28.7, name: '设施设备类' },
504
+            { value: 15.2, name: '勤务组织类' },
505
+            { value: 8.4, name: '安全知识类' },
506
+            { value: 2.4, name: '其他' }
507
+          ]
508
+        }
509
+      ]
510
+    });
511
+  }
512
+}
513
+// 计算雷达图数据的最大值
514
+const calculateRadarMaxValue = (radarData) => {
515
+  if (!radarData || radarData === 0) return 100;
516
+
517
+  let maxValue = 0;
518
+
519
+  // 遍历所有雷达图数据系列,找出最大值
520
+  radarData.forEach(series => {
521
+    if (series.data && Array.isArray(series.data)) {
522
+      const seriesMax = Math.max(...series.data);
523
+      if (seriesMax > maxValue) {
524
+        maxValue = seriesMax;
525
+      }
526
+    }
527
+  });
528
+
529
+
530
+  return maxValue
531
+}
532
+// 获取各科错题分布对比数据(雷达图)
533
+const fetchWrongAnalysisRadarData = async () => {
534
+  try {
535
+    // 使用统一的参数处理函数
536
+    const processedParams = processQueryParams(props.queryForm)
537
+    const response = await getWrongAnalysisRadar(processedParams);
538
+    const radarData = processRadarData(response);
539
+    let max = calculateRadarMaxValue(radarData.series)
540
+    // console.log('fetchWrongAnalysisRadarData', radarData);
541
+    // debugger
542
+    const series = radarData.series.map(ele => {
543
+      if (ele.data.every(item => item === 0)) {
544
+        return ''
545
+      }
546
+      let maxIndex = ele.data.indexOf(Math.max(...ele.data))
547
+      return `${ele.name}:问题主要集中于${radarData.indicators[maxIndex]}`
548
+
549
+    }).filter(element => !!element).join(';')
550
+
551
+
552
+    // 设置描述卡片文本
553
+    departmentComparisonDescription.value = `${series}${series.length > 0 ? ',' : '暂无问题'},可精准识别科室管理短板,分配质控资源。`
554
+
555
+    // 更新各科错题分布对比雷达图
556
+    setDepartmentComparisonOption({
557
+      tooltip: {
558
+        trigger: 'item'
559
+      },
560
+      legend: {
561
+        show: true,
562
+        orient: 'vertical',
563
+        left: 10,
564
+        top: 'center'
565
+      },
566
+      radar: {
567
+        indicator: radarData.indicators.map(indicator => ({
568
+          name: indicator,
569
+          max: max || 100
570
+        }))
571
+      },
572
+      series: [
573
+        {
574
+          type: 'radar',
575
+          data: radarData.series.map(seriesItem => ({
576
+            value: seriesItem.data,
577
+            name: seriesItem.name,
578
+            // areaStyle: {
579
+            //   color: seriesItem.color || 'rgba(84, 112, 198, 0.3)'
580
+            // },
581
+            // lineStyle: {
582
+            //   color: seriesItem.lineColor || '#5470c6'
583
+            // }
584
+          })),
585
+          areaStyle: {
586
+
587
+          },
588
+
589
+        }
590
+      ]
591
+    });
592
+  } catch (error) {
593
+    console.error('获取各科错题分布对比数据失败:', error);
594
+    // 使用默认数据作为后备
595
+    setDepartmentComparisonOption({
596
+      tooltip: {
597
+        trigger: 'item'
598
+      },
599
+      radar: {
600
+        indicator: [
601
+          { name: '操作执行类', max: 100 },
602
+          { name: '设施设备类', max: 100 },
603
+          { name: '勤务组织类', max: 100 },
604
+          { name: '安全知识类', max: 100 },
605
+          { name: '其他', max: 100 }
606
+        ]
607
+      },
608
+      series: [
609
+        {
610
+          type: 'radar',
611
+          data: [
612
+            {
613
+              value: [85, 45, 30, 60, 10],
614
+              name: '旅检一科',
615
+              areaStyle: {
616
+                color: 'rgba(84, 112, 198, 0.3)'
617
+              },
618
+              lineStyle: {
619
+                color: '#5470c6'
620
+              }
621
+            },
622
+            {
623
+              value: [40, 80, 55, 70, 15],
624
+              name: '旅检二科',
625
+              areaStyle: {
626
+                color: 'rgba(91, 192, 222, 0.3)'
627
+              },
628
+              lineStyle: {
629
+                color: '#5bc0de'
630
+              }
631
+            },
632
+            {
633
+              value: [35, 50, 90, 45, 20],
634
+              name: '旅检三科',
635
+              areaStyle: {
636
+                color: 'rgba(240, 173, 78, 0.3)'
637
+              },
638
+              lineStyle: {
639
+                color: '#f0ad4e'
640
+              }
641
+            }
642
+          ]
643
+        }
644
+      ]
645
+    });
646
+  }
647
+}
648
+
649
+// 初始化图表
650
+const initCharts = () => {
651
+  // 获取答题完成趋势数据
652
+  fetchCompletionTrendData();
653
+
654
+  // 获取错题分布数据(饼图)
655
+  fetchWrongAnalysisOverviewData();
656
+
657
+  // 获取各科错题分布对比数据(雷达图)
658
+  if (selectedDeptType.value === 'STATION' || !selectedDeptType.value) {
659
+    fetchWrongAnalysisRadarData()
660
+  }
661
+}
662
+
663
+// 监听查询参数变化
664
+watch(() => props.queryForm, (newVal) => {
665
+  console.log('Q&A分析组件接收到新的查询参数:', newVal)
666
+
667
+  // 重新获取所有图表数据
668
+  fetchCompletionTrendData()
669
+  fetchWrongAnalysisOverviewData()
670
+  if (selectedDeptType.value === 'STATION' || !selectedDeptType.value) {
671
+    fetchWrongAnalysisRadarData()
672
+  }
673
+
674
+}, { deep: true })
675
+
676
+// 组件挂载时初始化
677
+onMounted(() => {
678
+  initCharts()
679
+})
680
+
681
+// 组件卸载时清理
682
+onUnmounted(() => {
683
+  disposeCompletionTrend()
684
+  disposeErrorDistribution()
685
+  disposeDepartmentComparison()
686
+})
687
+</script>
688
+
689
+<style scoped>
690
+.qa-analysis {
691
+  margin-bottom: 20px;
692
+}
693
+
694
+.section-title {
695
+  margin: 14px 0 14px 0;
696
+
697
+  /* border-bottom: 2px solid #e8e8e8; */
698
+}
699
+
700
+.section-title h2 {
701
+  margin: 0;
702
+  font-size: 18px;
703
+  font-weight: 600;
704
+  color: #333;
705
+}
706
+
707
+.three-panel-layout {
708
+  display: flex;
709
+  gap: 20px;
710
+}
711
+
712
+.panel-item {
713
+  flex: 1;
714
+  background: #fff;
715
+  border-radius: 8px;
716
+  padding: 16px;
717
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
718
+}
719
+
720
+.panel-header h3 {
721
+  margin: 0 0 12px 0;
722
+  font-size: 16px;
723
+  font-weight: 600;
724
+  color: #333;
725
+}
726
+
727
+.describe-card {
728
+  background: #F8F8F8;
729
+  border: 1px dashed #CDCCCC;
730
+  border-radius: 6px;
731
+  padding: 7px 10px;
732
+  text-align: left;
733
+  margin-bottom: 10px;
734
+}
735
+
736
+.describe-content {
737
+  font-size: 13px;
738
+  color: #666;
739
+  line-height: 1.5;
740
+}
741
+
742
+.chart-container {
743
+  height: 300px;
744
+}
745
+
746
+.echarts-chart {
747
+  width: 100%;
748
+  height: 100%;
749
+}
750
+
751
+/* 响应式设计 */
752
+@media (max-width: 1200px) {
753
+  .three-panel-layout {
754
+    flex-direction: column;
755
+  }
756
+
757
+  .panel-item {
758
+    margin-bottom: 20px;
759
+  }
760
+}
761
+</style>

+ 47 - 11
src/views/assistant/components/qualityControl.vue

@@ -8,7 +8,7 @@
8 8
     <!-- 四个横向均分的内容区域 -->
9 9
     <div class="four-panel-layout">
10 10
       <!-- 第一个:任务计划安排统计 -->
11
-      <div class="panel-item">
11
+      <div class="panel-item" v-show="!isTeamType && !isUserType">
12 12
         <div class="panel-header">
13 13
           <h3>任务计划安排统计</h3>
14 14
         </div>
@@ -118,6 +118,10 @@ const props = defineProps({
118 118
       quarter: '',
119 119
       month: ''
120 120
     })
121
+  },
122
+  selectedDeptObject: {
123
+    type: Object,
124
+    default: null
121 125
   }
122 126
 })
123 127
 
@@ -133,7 +137,30 @@ const { setOption: setPlanScheduleOption, dispose: disposePlanSchedule } = useEc
133 137
 const { setOption: setProblemDiscoveryOption, dispose: disposeProblemDiscovery } = useEcharts(problemDiscoveryChartRef)
134 138
 const { setOption: setProblemDistributionOption, dispose: disposeProblemDistribution } = useEcharts(problemDistributionChartRef)
135 139
 const { setOption: setProblemRectificationOption, dispose: disposeProblemRectification } = useEcharts(problemRectificationChartRef)
140
+// 计算属性:处理selectedDeptObject逻辑
141
+const selectedDeptType = computed(() => {
142
+  return props.selectedDeptObject && props.selectedDeptObject?.deptType
143
+})
144
+const isUserType = computed(() => {
145
+  return selectedDeptType.value === 'USER'
146
+})
136 147
 
148
+const isTeamType = computed(() => {
149
+  return selectedDeptType.value === 'TEAMS'
150
+})
151
+
152
+// 计算属性:检查是否为STATION类型
153
+const isStationType = computed(() => {
154
+  return selectedDeptType.value === 'STATION' || !selectedDeptType.value
155
+})
156
+
157
+// 计算属性:检查是否为DEPARTMENT类型
158
+const isDepartmentType = computed(() => {
159
+  return selectedDeptType.value === 'DEPARTMENT'
160
+})
161
+const isBrigadeType = computed(() => {
162
+  return selectedDeptType.value === 'BRIGADE'
163
+})
137 164
 
138 165
 // 解析desc数据并更新专项任务和日常任务数据
139 166
 const parseTaskData = (desc) => {
@@ -156,7 +183,7 @@ const parseTaskData = (desc) => {
156 183
         const trendValue = yearOnYearMatch[2]
157 184
         let trendType = 'neutral'
158 185
         let displayValue = trendValue
159
-        
186
+
160 187
         // 处理趋势值为"--"的情况
161 188
         if (trendValue === '--') {
162 189
           trendType = 'neutral'
@@ -172,7 +199,7 @@ const parseTaskData = (desc) => {
172 199
             trendType = 'neutral'
173 200
           }
174 201
         }
175
-        
202
+
176 203
         specialTask.value.yearOnYearTrend = trendType
177 204
         specialTask.value.yearOnYear = displayValue + '%'
178 205
       }
@@ -181,7 +208,7 @@ const parseTaskData = (desc) => {
181 208
         const trendValue = chainRatioMatch[2]
182 209
         let trendType = 'neutral'
183 210
         let displayValue = trendValue
184
-        
211
+
185 212
         // 处理趋势值为"--"的情况
186 213
         if (trendValue === '--') {
187 214
           trendType = 'neutral'
@@ -197,7 +224,7 @@ const parseTaskData = (desc) => {
197 224
             trendType = 'neutral'
198 225
           }
199 226
         }
200
-        
227
+
201 228
         specialTask.value.chainRatioTrend = trendType
202 229
         specialTask.value.chainRatio = displayValue + '%'
203 230
       }
@@ -214,7 +241,7 @@ const parseTaskData = (desc) => {
214 241
         const trendValue = yearOnYearMatch[2]
215 242
         let trendType = 'neutral'
216 243
         let displayValue = trendValue
217
-        
244
+
218 245
         // 处理趋势值为"--"的情况
219 246
         if (trendValue === '--') {
220 247
           trendType = 'neutral'
@@ -230,7 +257,7 @@ const parseTaskData = (desc) => {
230 257
             trendType = 'neutral'
231 258
           }
232 259
         }
233
-        
260
+
234 261
         dailyTask.value.yearOnYearTrend = trendType
235 262
         dailyTask.value.yearOnYear = displayValue + '%'
236 263
       }
@@ -239,7 +266,7 @@ const parseTaskData = (desc) => {
239 266
         const trendValue = chainRatioMatch[2]
240 267
         let trendType = 'neutral'
241 268
         let displayValue = trendValue
242
-        
269
+
243 270
         // 处理趋势值为"--"的情况
244 271
         if (trendValue === '--') {
245 272
           trendType = 'neutral'
@@ -255,7 +282,7 @@ const parseTaskData = (desc) => {
255 282
             trendType = 'neutral'
256 283
           }
257 284
         }
258
-        
285
+
259 286
         dailyTask.value.chainRatioTrend = trendType
260 287
         dailyTask.value.chainRatio = displayValue + '%'
261 288
       }
@@ -266,7 +293,16 @@ const parseTaskData = (desc) => {
266 293
 // 调用API获取质控分析数据
267 294
 const fetchAnalysisData = async (queryParams) => {
268 295
   try {
269
-    const response = await getAnalysisReport(queryParams)
296
+    // 添加null检查,防止props.selectedDeptObject为null
297
+    const selectedDept = props.selectedDeptObject
298
+    const { deptType = "", id } = selectedDept ? selectedDept : { deptType: "", id: "" }
299
+
300
+    let rangParams = {
301
+      userId: deptType == 'USER' ? id : "",
302
+      deptId: ["STATION", "DEPARTMENT", "TEAMS", "BRIGADE"].includes(deptType) ? id : ""
303
+    }
304
+
305
+    const response = await getAnalysisReport({ ...queryParams, ...rangParams })
270 306
     console.log('质控分析数据:', response.data)
271 307
 
272 308
     // 解构赋值API返回的数据
@@ -332,7 +368,7 @@ watch(() => props.queryForm, (newQueryForm) => {
332 368
   if (newQueryForm.dateRangeQueryType && newQueryForm.year) {
333 369
     fetchAnalysisData(newQueryForm)
334 370
   }
335
-}, { deep: true, immediate: true })
371
+}, { deep: true })
336 372
 
337 373
 // 定义响应式数据
338 374
 const checkTask = ref({})

+ 286 - 134
src/views/assistant/components/riskHazard.vue

@@ -138,6 +138,85 @@
138 138
             </el-table>
139 139
           </div>
140 140
         </div>
141
+
142
+        <!-- 第七个:移交公安数据 -->
143
+        <div class="panel-item">
144
+          <div class="panel-header">
145
+            <h3>移交公安数据</h3>
146
+          </div>
147
+
148
+          <!-- 描述卡片 -->
149
+          <div class="describe-card">
150
+            <div class="describe-content">
151
+              {{ policeTransferStats || '移交公安的违禁品数量共X件,主要集中于XX。' }}
152
+            </div>
153
+          </div>
154
+
155
+          <!-- 表格 -->
156
+          <div class="table-container">
157
+            <el-table :data="policeTransferData" size="small" height="250" :scroll="{ x: 'max-content' }">
158
+              <el-table-column prop="departmentName" label="科室" min-width="120" />
159
+              <el-table-column prop="teamName" label="班组" min-width="120" />
160
+              <el-table-column prop="userName" label="姓名" min-width="100" />
161
+              <el-table-column prop="itemName" label="查获物品" min-width="150" />
162
+              <el-table-column prop="quantity" label="查获数量" align="center" width="100" />
163
+              <el-table-column prop="positionName" label="查获部位" min-width="120" />
164
+              <el-table-column prop="seizureTime" label="查获时间" min-width="150" />
165
+              <el-table-column prop="channelName" label="查获通道" min-width="120" />
166
+            </el-table>
167
+          </div>
168
+        </div>
169
+
170
+        <!-- 第八个:X光机漏检数据 -->
171
+        <div class="panel-item" v-show="!isUserType">
172
+          <div class="panel-header">
173
+            <h3>X光机漏检数据</h3>
174
+          </div>
175
+
176
+          <!-- 描述卡片 -->
177
+          <div class="describe-card">
178
+            <div class="describe-content">
179
+              {{ xrayMissStats || 'X光机漏检事件主要集中于以下开机员:张三、李四、王五,可针对性开展判图技能强化培训。' }}
180
+            </div>
181
+          </div>
182
+
183
+          <!-- 表格 -->
184
+          <div class="table-container">
185
+            <el-table :data="xrayMissData" size="small" height="250" :scroll="{ x: 'max-content' }">
186
+              <el-table-column prop="departmentName" label="科室" min-width="120" />
187
+              <el-table-column prop="teamName" label="班组" min-width="120" />
188
+              <el-table-column prop="userName" label="姓名" min-width="100" />
189
+              <el-table-column prop="missItemName" label="漏检物品" min-width="150" />
190
+              <el-table-column prop="missQuantity" label="漏检数量" align="center" width="100" />
191
+              <el-table-column prop="missPosition" label="漏检部位" min-width="120" />
192
+              <el-table-column prop="channelName" label="漏检通道" min-width="120" />
193
+            </el-table>
194
+          </div>
195
+        </div>
196
+
197
+        <!-- 第九个:可能异常查获数据 -->
198
+        <div class="panel-item" v-show="!isTeamsType && !isUserType">
199
+          <div class="panel-header">
200
+            <h3>可能异常查获数据</h3>
201
+          </div>
202
+
203
+          <!-- 描述卡片 -->
204
+          <div class="describe-card" v-if="abnormalCaptureStats">
205
+            <div class="describe-content">
206
+              {{ abnormalCaptureStats || '旅检一科、旅检二科、旅检三科查获违禁品数量显著高于整体水平,旅检四科、旅检五科查获违禁品数量显著低于整体水平。' }}
207
+            </div>
208
+          </div>
209
+
210
+          <!-- 表格 -->
211
+          <div class="table-container">
212
+            <el-table :data="abnormalCaptureData" size="small" height="250" :scroll="{ x: 'max-content' }">
213
+              <el-table-column prop="departmentName" label="科室" min-width="120" />
214
+              <el-table-column prop="teamName" label="班组" min-width="120" />
215
+              <el-table-column prop="userName" label="姓名" min-width="100" />
216
+              <el-table-column prop="seizureQuantity" label="查获数量" align="center" width="100" />
217
+            </el-table>
218
+          </div>
219
+        </div>
141 220
       </div>
142 221
     </div>
143 222
   </div>
@@ -165,6 +244,10 @@ const props = defineProps({
165 244
       quarter: '',
166 245
       month: ''
167 246
     })
247
+  },
248
+  selectedDeptObject: {
249
+    type: Object,
250
+    default: null
168 251
   }
169 252
 })
170 253
 
@@ -187,154 +270,190 @@ const concealmentPositionStatsData = ref({})
187 270
 const departmentRankingData = ref({})
188 271
 const postCategoryStatsData = ref({})
189 272
 const channelRankingStatsData = ref({})
273
+// 计算属性:处理selectedDeptObject逻辑
274
+const selectedDeptType = computed(() => {
275
+  return props.selectedDeptObject && props.selectedDeptObject?.deptType
276
+})
277
+
278
+// 计算属性:检查是否为STATION类型
279
+const isStationType = computed(() => {
280
+  return selectedDeptType.value === 'STATION' || !selectedDeptType.value
281
+})
282
+// 计算属性:检查是否为STATION类型
283
+const isDepartmentType = computed(() => {
284
+  return selectedDeptType.value === 'DEPARTMENT'
285
+})
286
+
287
+const isBrigadeType = computed(() => {
288
+  return selectedDeptType.value === 'BRIGADE'
289
+})
290
+
291
+// 计算属性:检查是否为TEAMS类型
292
+const isTeamsType = computed(() => {
293
+  return selectedDeptType.value === 'TEAMS'
294
+})
295
+
296
+//
297
+const isUserType = computed(() => {
298
+  return selectedDeptType.value === 'USER'
299
+})
300
+// 新增三个API接口的响应式数据
301
+const policeTransferData = ref([])
302
+const xrayMissData = ref([])
303
+const abnormalCaptureData = ref([])
304
+
305
+// 新增三个统计API接口的响应式数据
306
+const policeTransferStats = ref('')
307
+const xrayMissStats = ref('')
308
+const abnormalCaptureStats = ref('')
190 309
 
191 310
 // 计算属性:动态生成查获违禁品类别描述
192 311
 const categoryStatsDescription = computed(() => {
193
-  
312
+
194 313
   if (!categoryStatsData.value || !categoryStatsData.value || !Array.isArray(categoryStatsData.value)) {
195 314
     return '查获物品以[占比最高物品类型]为主,占比达[X]%,其次为[第二高占比物品类型]([X]%)、[第三高占比物品类型]([X]%),需重点强化对应物品的安检识别与管控力度。'
196 315
   }
197 316
 
198 317
   const categoryData = categoryStatsData.value
199
-  
318
+
200 319
   // 1. 按数量排序,获取前三名
201 320
   const sortedData = [...categoryData].sort((a, b) => (b.quantity || 0) - (a.quantity || 0))
202
-  
321
+
203 322
   // 2. 计算总数量
204 323
   const totalCount = sortedData.reduce((sum, item) => sum + (item.quantity || 0), 0)
205
-  
324
+
206 325
   // 3. 获取前三名数据
207 326
   const top1 = sortedData[0] || { categoryNameOne: '未知类型', quantity: 0 }
208 327
   const top2 = sortedData[1] || { categoryNameOne: '未知类型', quantity: 0 }
209 328
   const top3 = sortedData[2] || { categoryNameOne: '未知类型', quantity: 0 }
210
-  
329
+
211 330
   // 4. 计算百分比
212 331
   const top1Percentage = totalCount > 0 ? ((top1.quantity / totalCount) * 100).toFixed(2) : '0.00'
213 332
   const top2Percentage = totalCount > 0 ? ((top2.quantity / totalCount) * 100).toFixed(2) : '0.00'
214 333
   const top3Percentage = totalCount > 0 ? ((top3.quantity / totalCount) * 100).toFixed(2) : '0.00'
215
-  
334
+
216 335
   // 5. 生成描述文字
217
-  return `查获物品以${top1.categoryNameOne  || '未知类型'}为主,占比达${top1Percentage}%,其次为${top2.categoryNameOne  || '未知类型'}(${top2Percentage}%)、${top3.categoryNameOne  || '未知类型'}(${top3Percentage}%),需重点强化对应物品的安检识别与管控力度。`
336
+  return `查获物品以${top1.categoryNameOne || '未知类型'}为主,占比达${top1Percentage}%,其次为${top2.categoryNameOne || '未知类型'}(${top2Percentage}%)、${top3.categoryNameOne || '未知类型'}(${top3Percentage}%),需重点强化对应物品的安检识别与管控力度。`
218 337
 })
219 338
 
220 339
 // 计算属性:动态生成查获时间趋势描述
221
-  const seizureTimeTrendDescription = computed(() => {
222
-    if (!seizureTimeTrendData.value || !seizureTimeTrendData.value || !Array.isArray(seizureTimeTrendData.value)) {
223
-      return '高峰时段:XX:XX、XX:XX左右(平均查获量达XX件),需强化对应时段的安检力度。低峰时段:XX:XX、XX:XX后(平均查获量XX件)'
224
-    }
340
+const seizureTimeTrendDescription = computed(() => {
341
+  if (!seizureTimeTrendData.value || !seizureTimeTrendData.value || !Array.isArray(seizureTimeTrendData.value)) {
342
+    return '高峰时段:XX:XX、XX:XX左右(平均查获量达XX件),需强化对应时段的安检力度。低峰时段:XX:XX、XX:XX后(平均查获量XX件)'
343
+  }
225 344
 
226
-    const trendData = seizureTimeTrendData.value
227
-    
228
-    // 1. 按查获量排序,找出最高和最低的两个时段
229
-    const sortedData = [...trendData].sort((a, b) => (b.total || 0) - (a.total || 0))
230
-    
231
-    // 2. 获取最高的两个时段
232
-    const peakHours = sortedData.slice(0, 2).map(item => item.hourOfDay || '').filter(Boolean)
233
-    
234
-    // 3. 获取最低的两个时段
235
-    const lowHours = sortedData.slice(-2).map(item => item.hourOfDay || '').filter(Boolean)
236
-    
237
-    // 4. 计算高峰时段平均查获量
238
-    const peakData = trendData.filter(item => peakHours.some(hour => item.hourOfDay?.includes(hour)))
239
-    const peakAvg = peakData.length > 0 
240
-      ? Math.round(peakData.reduce((sum, item) => sum + (item.total || 0), 0) / peakData.length)
241
-      : 0
242
-    
243
-    // 5. 计算低峰时段平均查获量
244
-    const lowData = trendData.filter(item => lowHours.some(hour => item.hourOfDay?.includes(hour)))
245
-    const lowAvg = lowData.length > 0 
246
-      ? Math.round(lowData.reduce((sum, item) => sum + (item.total || 0), 0) / lowData.length)
247
-      : 0
248
-    
249
-    // 6. 生成描述文字
250
-    return `高峰时段:${peakHours.join('、')}左右(平均查获量达${peakAvg}件),需强化对应时段的安检力度。低峰时段:${lowHours.join('、')}后(平均查获量${lowAvg}件)`
251
-  })
252
-
253
-  // 计算属性:动态生成隐匿物品查获部位描述
254
-  const concealmentPositionStatsDescription = computed(() => {
255
-    if (!concealmentPositionStatsData.value || !Array.isArray(concealmentPositionStatsData.value)) {
256
-      return '违禁品主要藏匿于"XX",是安检搜检的重点部位。'
257
-    }
345
+  const trendData = seizureTimeTrendData.value
258 346
 
259
-    const positionData = concealmentPositionStatsData.value
260
-    
261
-    // 1. 按查获量排序,找出查获量最高的部位
262
-    const sortedData = [...positionData].sort((a, b) => (b.count || 0) - (a.count || 0))
263
-    
264
-    // 2. 获取查获量最高的部位
265
-    const topPosition = sortedData[0] || { positionName: 'XX', count: 0 }
266
-    
267
-    // 3. 生成描述文字
268
-    return `违禁品主要藏匿于"${topPosition.positionName || 'XX'}",是安检搜检的重点部位。`
269
-  })
270
-
271
-  // 计算属性:动态生成大队查获排名描述
272
-  const departmentRankingDescription = computed(() => {
273
-    
274
-    if (!departmentRankingData.value || !Array.isArray(departmentRankingData.value)) {
275
-      return 'XXXX是违禁品查获的主力大队,查获违禁物品数量为XX,占比XX%。'
276
-    }
347
+  // 1. 按查获量排序,找出最高和最低的两个时段
348
+  const sortedData = [...trendData].sort((a, b) => (b.total || 0) - (a.total || 0))
277 349
 
278
-    const rankingData = departmentRankingData.value
279
-    
280
-    // 1. 按查获量排序,找出查获量最高的大队
281
-    const sortedData = [...rankingData].sort((a, b) => (b.seizureCount || 0) - (a.seizureCount || 0))
282
-    
283
-    // 2. 获取查获量最高的大队
284
-    const topDepartment = sortedData[0] || { departmentName: 'XXXX', seizureCount: 234 }
285
-    
286
-  
287
-    // 5. 生成描述文字
288
-    return `${topDepartment.brigadeName || 'XXXX'}是违禁品查获的主力大队,查获违禁物品数量为${topDepartment.seizureCount || 'XX'},占比${topDepartment.currentRatio}%。`
289
-  })
290
-
291
-  // 计算属性:动态生成查获岗位分布描述
292
-  const postCategoryStatsDescription = computed(() => {
293
-    if (!postCategoryStatsData.value || !Array.isArray(postCategoryStatsData.value)) {
294
-      return '"XX"的查获数量最多,为XX件,占比XX%。'
295
-    }
350
+  // 2. 获取最高的两个时段
351
+  const peakHours = sortedData.slice(0, 2).map(item => item.hourOfDay || '').filter(Boolean)
296 352
 
297
-    const postData = postCategoryStatsData.value
298
-    
299
-    // 1. 按查获量排序,找出查获量最高的岗位
300
-    const sortedData = [...postData].sort((a, b) => (b.quantity || 0) - (a.quantity || 0))
301
-    
302
-    // 2. 获取查获量最高的岗位
303
-    const topPost = sortedData[0] || { postName: 'XX', quantity: 124 }
304
-    
305
-    // 3. 计算总查获量
306
-    const totalCount = postData.reduce((sum, item) => sum + (item.quantity || 0), 0)
307
-    
308
-    // 4. 计算占比
309
-    const percentage = totalCount > 0 ? ((topPost.quantity / totalCount) * 100).toFixed(0) : '45'
310
-    
311
-    // 5. 生成描述文字
312
-    return `"${topPost.postName || 'XX'}"的查获数量最多,为${topPost.quantity || 124}件,占比${percentage}%。`
313
-  })
314
-
315
-  // 计算属性:动态生成查获通道TOP5描述
316
-  const channelRankingStatsDescription = computed(() => {
317
-    if (!channelRankingStatsData.value || !Array.isArray(channelRankingStatsData.value)) {
318
-      return '违禁物品查获主要集中于XXXX,查获数量为XX,占比XX%。'
319
-    }
353
+  // 3. 获取最低的两个时段
354
+  const lowHours = sortedData.slice(-2).map(item => item.hourOfDay || '').filter(Boolean)
355
+
356
+  // 4. 计算高峰时段平均查获量
357
+  const peakData = trendData.filter(item => peakHours.some(hour => item.hourOfDay?.includes(hour)))
358
+  const peakAvg = peakData.length > 0
359
+    ? Math.round(peakData.reduce((sum, item) => sum + (item.total || 0), 0) / peakData.length)
360
+    : 0
361
+
362
+  // 5. 计算低峰时段平均查获量
363
+  const lowData = trendData.filter(item => lowHours.some(hour => item.hourOfDay?.includes(hour)))
364
+  const lowAvg = lowData.length > 0
365
+    ? Math.round(lowData.reduce((sum, item) => sum + (item.total || 0), 0) / lowData.length)
366
+    : 0
367
+
368
+  // 6. 生成描述文字
369
+  return `高峰时段:${peakHours.join('、')}左右(平均查获量达${peakAvg}件),需强化对应时段的安检力度。低峰时段:${lowHours.join('、')}后(平均查获量${lowAvg}件)`
370
+})
371
+
372
+// 计算属性:动态生成隐匿物品查获部位描述
373
+const concealmentPositionStatsDescription = computed(() => {
374
+  if (!concealmentPositionStatsData.value || !Array.isArray(concealmentPositionStatsData.value)) {
375
+    return '违禁品主要藏匿于"XX",是安检搜检的重点部位。'
376
+  }
377
+
378
+  const positionData = concealmentPositionStatsData.value
379
+
380
+  // 1. 按查获量排序,找出查获量最高的部位
381
+  const sortedData = [...positionData].sort((a, b) => (b.count || 0) - (a.count || 0))
382
+
383
+  // 2. 获取查获量最高的部位
384
+  const topPosition = sortedData[0] || { positionName: 'XX', count: 0 }
385
+
386
+  // 3. 生成描述文字
387
+  return `违禁品主要藏匿于"${topPosition.positionName || 'XX'}",是安检搜检的重点部位。`
388
+})
389
+
390
+// 计算属性:动态生成大队查获排名描述
391
+const departmentRankingDescription = computed(() => {
392
+
393
+  if (!departmentRankingData.value || !Array.isArray(departmentRankingData.value)) {
394
+    return 'XXXX是违禁品查获的主力大队,查获违禁物品数量为XX,占比XX%。'
395
+  }
396
+
397
+  const rankingData = departmentRankingData.value
398
+
399
+  // 1. 按查获量排序,找出查获量最高的大队
400
+  const sortedData = [...rankingData].sort((a, b) => (b.seizureCount || 0) - (a.seizureCount || 0))
401
+
402
+  // 2. 获取查获量最高的大队
403
+  const topDepartment = sortedData[0] || { departmentName: 'XXXX', seizureCount: 234 }
404
+
405
+  const str = isStationType.value ? '大队' : isBrigadeType.value ? '主管' : isDepartmentType.value ? '班组' : isTeamsType.value ? '成员' : ''
406
+  // 5. 生成描述文字
407
+  return `${topDepartment.brigadeName || 'XXXX'}是违禁品查获的主力${str},查获违禁物品数量为${topDepartment.seizureCount || 'XX'},占比${topDepartment.currentRatio}%。`
408
+})
409
+
410
+// 计算属性:动态生成查获岗位分布描述
411
+const postCategoryStatsDescription = computed(() => {
412
+  if (!postCategoryStatsData.value || !Array.isArray(postCategoryStatsData.value)) {
413
+    return '"XX"的查获数量最多,为XX件,占比XX%。'
414
+  }
415
+
416
+  const postData = postCategoryStatsData.value
417
+
418
+  // 1. 按查获量排序,找出查获量最高的岗位
419
+  const sortedData = [...postData].sort((a, b) => (b.quantity || 0) - (a.quantity || 0))
420
+
421
+  // 2. 获取查获量最高的岗位
422
+  const topPost = sortedData[0] || { postName: 'XX', quantity: 124 }
423
+
424
+  // 3. 计算总查获量
425
+  const totalCount = postData.reduce((sum, item) => sum + (item.quantity || 0), 0)
320 426
 
321
-    const channelData = channelRankingStatsData.value
322
-    
323
-    // 1. 按查获量排序,找出查获量最高的通道
324
-    const sortedData = [...channelData].sort((a, b) => (b.seizureQuantity || 0) - (a.seizureQuantity || 0))
325
-    
326
-    // 2. 获取查获量最高的通道
327
-    const topChannel = sortedData[0] || { channelName: 'XXXX', seizureQuantity: 123 }
328
-    
329
-    // 3. 计算总查获量
330
-    const totalCount = channelData.reduce((sum, item) => sum + (item.seizureQuantity || 0), 0)
331
-    
332
-    // 4. 计算占比
333
-    const percentage = totalCount > 0 ? ((topChannel.seizureQuantity / totalCount) * 100).toFixed(0) : '23'
334
-    
335
-    // 5. 生成描述文字
336
-    return `违禁物品查获主要集中于${topChannel.channelName || 'XXXX'},查获数量为${topChannel.seizureQuantity || 'XX'},占比${percentage}%。`
337
-  })
427
+  // 4. 计算占比
428
+  const percentage = totalCount > 0 ? ((topPost.quantity / totalCount) * 100).toFixed(0) : '45'
429
+
430
+  // 5. 生成描述文字
431
+  return `"${topPost.postName || 'XX'}"的查获数量最多,为${topPost.quantity || 124}件,占比${percentage}%。`
432
+})
433
+
434
+// 计算属性:动态生成查获通道TOP5描述
435
+const channelRankingStatsDescription = computed(() => {
436
+  if (!channelRankingStatsData.value || !Array.isArray(channelRankingStatsData.value)) {
437
+    return '违禁物品查获主要集中于XXXX,查获数量为XX,占比XX%。'
438
+  }
439
+
440
+  const channelData = channelRankingStatsData.value
441
+
442
+  // 1. 按查获量排序,找出查获量最高的通道
443
+  const sortedData = [...channelData].sort((a, b) => (b.seizureQuantity || 0) - (a.seizureQuantity || 0))
444
+
445
+  // 2. 获取查获量最高的通道
446
+  const topChannel = sortedData[0] || { channelName: 'XXXX', seizureQuantity: 123 }
447
+
448
+  // 3. 计算总查获量
449
+  const totalCount = channelData.reduce((sum, item) => sum + (item.seizureQuantity || 0), 0)
450
+
451
+  // 4. 计算占比
452
+  const percentage = totalCount > 0 ? ((topChannel.seizureQuantity / totalCount) * 100).toFixed(0) : '23'
453
+
454
+  // 5. 生成描述文字
455
+  return `违禁物品查获主要集中于${topChannel.channelName || 'XXXX'},查获数量为${topChannel.seizureQuantity || 'XX'},占比${percentage}%。`
456
+})
338 457
 
339 458
 // 表格数据
340 459
 const captureRankData = ref([])
@@ -357,36 +476,43 @@ const fetchRiskHazardData = async (queryParams) => {
357 476
   try {
358 477
     // 处理query参数
359 478
     const processedParams = processQueryParams(queryParams)
360
-
479
+    const selectedDept = props.selectedDeptObject
480
+    const { deptType = "", id } = selectedDept ? selectedDept : { deptType: "", id: "" }
481
+    let otherParams = {
482
+      ...(deptType == 'BRIGADE' ? { brigadeId: id } : {}),
483
+      ...(deptType == 'DEPARTMENT' ? { departmentId: id } : {}),
484
+      ...(deptType == 'TEAMS' ? { teamId: id } : {}),
485
+      ...(deptType == 'USER' ? { userId: id } : {})
486
+    }
361 487
     // 按顺序调用六个API接口
362 488
 
363 489
     // 1. 查获违禁品类别统计
364
-    const categoryStatsResponse = await getCategoryStats(processedParams)
490
+    const categoryStatsResponse = await getCategoryStats({ ...processedParams, ...otherParams })
365 491
     console.log('查获违禁品类别统计:', categoryStatsResponse)
366 492
     categoryStatsData.value = categoryStatsResponse.data?.categoryStats || []
367 493
 
368 494
     // 2. 查获时间趋势
369
-    const seizureTimeTrendResponse = await getSeizureTimeTrend(processedParams)
495
+    const seizureTimeTrendResponse = await getSeizureTimeTrend({ ...processedParams, ...otherParams })
370 496
     console.log('查获时间趋势:', seizureTimeTrendResponse)
371 497
     seizureTimeTrendData.value = seizureTimeTrendResponse?.data || []
372 498
 
373 499
     // 3. 隐匿物品查获部位统计
374
-    const concealmentPositionStatsResponse = await getConcealmentPositionStats(processedParams)
500
+    const concealmentPositionStatsResponse = await getConcealmentPositionStats({ ...processedParams, ...otherParams })
375 501
     console.log('隐匿物品查获部位统计:', concealmentPositionStatsResponse)
376 502
     concealmentPositionStatsData.value = concealmentPositionStatsResponse?.data?.positionStats || []
377 503
 
378 504
     // 4. 大队查获排名(表格数据)
379
-    const departmentRankingResponse = await getDepartmentRanking(processedParams)
505
+    const departmentRankingResponse = await getDepartmentRanking({ ...processedParams, ...otherParams })
380 506
     console.log('大队查获排名:', departmentRankingResponse)
381 507
     departmentRankingData.value = departmentRankingResponse.data || []
382 508
 
383 509
     // 5. 查获岗位分布统计
384
-    const postCategoryStatsResponse = await getPostCategoryStats(processedParams)
510
+    const postCategoryStatsResponse = await getPostCategoryStats({ ...processedParams, ...otherParams })
385 511
     console.log('查获岗位分布统计:', postCategoryStatsResponse)
386 512
     postCategoryStatsData.value = postCategoryStatsResponse?.data?.postStats || []
387 513
 
388 514
     // 6. 查获通道TOP5(表格数据)
389
-    const channelRankingStatsResponse = await getChannelRankingStats(processedParams)
515
+    const channelRankingStatsResponse = await getChannelRankingStats({ ...processedParams, ...otherParams })
390 516
     console.log('查获通道TOP5:', channelRankingStatsResponse)
391 517
     channelRankingStatsData.value = channelRankingStatsResponse?.data?.channelRankings || []
392 518
 
@@ -670,7 +796,7 @@ watch(() => props.queryForm, (newQueryForm) => {
670 796
   if (newQueryForm.dateRangeQueryType && newQueryForm.year) {
671 797
     fetchRiskHazardData(newQueryForm)
672 798
   }
673
-}, { deep: true, immediate: true })
799
+}, { deep: true })
674 800
 
675 801
 // 根据API数据更新图表和表格数据
676 802
 const updateChartsWithData = () => {
@@ -861,26 +987,27 @@ onUnmounted(() => {
861 987
 }
862 988
 
863 989
 .section-title h2 {
864
-  font-size: 24px;
990
+  margin: 0;
991
+  font-size: 18px;
865 992
   font-weight: 600;
866 993
   color: #333;
867
-  margin: 0;
868 994
 }
869 995
 
870 996
 /* 六个部分布局 */
871 997
 .six-panel-layout {
872
-  display: flex;
873
-  flex-direction: column;
998
+  display: grid;
999
+  grid-template-columns: repeat(3, 1fr);
874 1000
   gap: 16px;
1001
+  grid-auto-flow: dense;
1002
+  /* 自动填充空白区域 */
875 1003
 }
876 1004
 
877 1005
 .panel-row {
878
-  display: flex;
879
-  gap: 16px;
1006
+  display: contents;
1007
+  /* 让子元素直接参与网格布局 */
880 1008
 }
881 1009
 
882 1010
 .panel-item {
883
-  flex: 1;
884 1011
   background: white;
885 1012
   border-radius: 8px;
886 1013
   padding: 20px;
@@ -888,6 +1015,10 @@ onUnmounted(() => {
888 1015
   display: flex;
889 1016
   flex-direction: column;
890 1017
   gap: 16px;
1018
+  min-width: 0;
1019
+  /* 防止内容溢出 */
1020
+  overflow: hidden;
1021
+  /* 隐藏溢出内容 */
891 1022
 }
892 1023
 
893 1024
 .panel-header {
@@ -958,7 +1089,28 @@ onUnmounted(() => {
958 1089
   flex: 1;
959 1090
   border: 1px solid #E4E7ED;
960 1091
   border-radius: 4px;
1092
+  max-height: 300px;
1093
+  overflow: auto;
1094
+  position: relative;
1095
+}
1096
+
1097
+/* Element Plus表格横向滚动配置 */
1098
+.table-container :deep(.el-table) {
1099
+  width: 100%;
1100
+  min-width: 100%;
1101
+}
1102
+
1103
+.table-container :deep(.el-table .el-table__header-wrapper),
1104
+.table-container :deep(.el-table .el-table__body-wrapper) {
1105
+  overflow-x: auto !important;
1106
+}
1107
+
1108
+.table-container :deep(.el-table__header) {
1109
+  width: auto !important;
1110
+}
961 1111
 
1112
+.table-container :deep(.el-table__body) {
1113
+  width: auto !important;
962 1114
 }
963 1115
 
964 1116
 /* 表格偶数行背景色 */

文件差異過大導致無法顯示
+ 31 - 32
src/views/assistant/components/useReports.vue