Explorar el Código

Merge branch 'ai'

huoyi hace 1 día
padre
commit
ee7958669b

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

+ 3 - 3
src/hooks/chart.js

@@ -17,14 +17,14 @@ export function useEcharts(domRef) {
17 17
     }
18 18
   };
19 19
 
20
-  const setOption = (option) => {
20
+  const setOption = (option, notMerge = false) => {
21 21
     if (!chartInstance.value && !initialized.value) {
22 22
       initChart();
23 23
     }
24 24
     if (!chartInstance.value) return;
25
-    
25
+
26 26
     try {
27
-      chartInstance.value.setOption(option);
27
+      chartInstance.value.setOption(option, notMerge);
28 28
       // 仅在需要时调整大小,避免重复调用
29 29
       if (domRef.value && domRef.value.offsetHeight > 0) {
30 30
         chartInstance.value.resize();

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

+ 234 - 32
src/views/assistant/components/dutyOrganization.vue

@@ -16,16 +16,21 @@
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 > 0 ? attendanceData[attendanceData.length -
22
+              1]?.currentValue : 0
20 23
             }}</span>人次,同比
21 24
             <span
22
-              :class="getTrendClass(attendanceData && attendanceData.length > 3 ? attendanceData[3]?.yearOnYearValue : 0)">{{
23
-                formatRate(attendanceData && attendanceData.length > 3 ? attendanceData[3]?.yearOnYearValue : 0) }}</span>
25
+              :class="getTrendClass(attendanceData && attendanceData.length > 0 ? attendanceData[attendanceData.length - 1]?.yearOnYearValue : 0)">{{
26
+                formatRate(attendanceData && attendanceData.length > 0 ? attendanceData[attendanceData.length -
27
+                  1]?.yearOnYearValue : 0) }}</span>
24 28
             <template v-if="props.queryForm.dateRangeQueryType !== 'YEAR'">
25 29
               ,环比
26 30
               <span
27
-                :class="getTrendClass(attendanceData && attendanceData.length > 3 ? attendanceData[3]?.chainRatioValue : 0)">{{
28
-                  formatRate(attendanceData && attendanceData.length > 3 ? attendanceData[3]?.chainRatioValue : 0)
31
+                :class="getTrendClass(attendanceData && attendanceData.length > 0 ? attendanceData[attendanceData.length - 1]?.chainRatioValue : 0)">{{
32
+                  formatRate(attendanceData && attendanceData.length > 0 ? attendanceData[attendanceData.length -
33
+                    1]?.chainRatioValue : 0)
29 34
                 }}</span>
30 35
             </template>
31 36
           </div>
@@ -35,7 +40,7 @@
35 40
         <div class="dept-cards">
36 41
           <template v-if="attendanceData && attendanceData.length > 0">
37 42
             <div v-for="(item, index) in attendanceData.slice(0, 4)" :key="index" class="dept-card">
38
-              <div class="dept-name">{{ item.deptName || '未知科室' }}</div>
43
+              <div class="dept-name">{{ item.deptName || selectedDeptObject?.label }}</div>
39 44
               <div class="dept-count"><span>{{ item.currentValue || 0 }}</span>人次</div>
40 45
               <div class="dept-trend">
41 46
                 <div>
@@ -71,7 +76,7 @@
71 76
       </div>
72 77
 
73 78
       <!-- 右侧:资质等级分布 -->
74
-      <div class="right-panel panel-item">
79
+      <div class="right-panel panel-item" v-show="!isUserType">
75 80
         <div class="panel-header">
76 81
           <h3>资质等级分布</h3>
77 82
         </div>
@@ -94,7 +99,7 @@
94 99
           </div>
95 100
 
96 101
           <!-- 右侧:资质分布趋势 -->
97
-          <div class="qualification-right">
102
+          <div class="qualification-right" v-show="isStationType">
98 103
             <!-- 上方描述卡片 -->
99 104
             <div class="stat-card" v-if="qualificationPieDescriptionPart2">
100 105
               <div class="stat-content">
@@ -107,6 +112,30 @@
107 112
               <div ref="barChartRef" class="echarts-chart"></div>
108 113
             </div>
109 114
           </div>
115
+
116
+          <div class="qualification-right" style="justify-content: center;" v-show="isTeamsType">
117
+            <!-- 班组人员资质等级表格 -->
118
+            <div class="table-container">
119
+              <el-table :data="teamsQualificationData" size="small" height="250" border :scroll="{ x: 'max-content' }"
120
+                stripe>
121
+                <el-table-column prop="deptName" label="姓名" align="center" />
122
+                <el-table-column prop="qualificationLevel" label="等级" align="center" />
123
+              </el-table>
124
+            </div>
125
+          </div>
126
+        </div>
127
+      </div>
128
+      <div class="right-panel-ability panel-item" v-show="isUserType">
129
+        <div class="panel-header">
130
+          <h3>资质能力</h3>
131
+        </div>
132
+
133
+        <!-- 统计卡片 -->
134
+        <div class="stat-card">
135
+          <div class="stat-content">资质等级:{{ user?.qualificationLevel || '未知' }}</div>
136
+          <div class="stat-content">可上岗岗位:{{user?.sysPostList && user?.sysPostList.map(item => item.postName).join('、')
137
+            || '未知'
138
+          }}</div>
110 139
         </div>
111 140
       </div>
112 141
     </div>
@@ -128,9 +157,14 @@ const props = defineProps({
128 157
       quarter: '',
129 158
       month: ''
130 159
     })
160
+  },
161
+  selectedDeptObject: {
162
+    type: Object,
163
+    default: null
131 164
   }
132 165
 })
133
-
166
+const user = ref({})
167
+const teamsQualificationData = ref([])
134 168
 // 饼图容器引用
135 169
 const pieChartRef = ref(null)
136 170
 const barChartRef = ref(null)
@@ -146,7 +180,29 @@ const attendanceData = ref({})
146 180
 const trendData = ref({})
147 181
 const qualificationPieData = ref({})
148 182
 const qualificationBarData = ref({})
183
+// 计算属性:处理selectedDeptObject逻辑
184
+const selectedDeptType = computed(() => {
185
+  return props.selectedDeptObject && props.selectedDeptObject?.deptType
186
+})
187
+// 计算属性:检查是否为STATION类型
188
+const isStationType = computed(() => {
189
+  console.log('selectedDeptType.value', selectedDeptType.value)
149 190
 
191
+  return selectedDeptType.value === 'STATION' || !selectedDeptType.value
192
+})
193
+const isDepartmentType = computed(() => {
194
+  return selectedDeptType.value === 'MANAGER'
195
+})
196
+const isTeamsType = computed(() => {
197
+  return selectedDeptType.value === 'TEAMS'
198
+})
199
+const isUserType = computed(() => {
200
+  return selectedDeptType.value === 'USER'
201
+})
202
+// 计算属性:检查是否为非STATION类型
203
+const isNotUserType = computed(() => {
204
+  return !selectedDeptType.value !== 'USER'
205
+})
150 206
 // 计算属性:动态生成资质等级分布描述第一部分(句号前)
151 207
 const qualificationPieDescriptionPart1 = computed(() => {
152 208
   if (!qualificationPieData.value || !qualificationPieData.value.data || !Array.isArray(qualificationPieData.value.data)) {
@@ -158,16 +214,17 @@ const qualificationPieDescriptionPart1 = computed(() => {
158 214
   // 1. 找出占比最高的等级
159 215
   const highestLevel = pieData.reduce((max, item) => {
160 216
     return (item.count || 0) > (max.count || 0) ? item : max
161
-  }, { levelName: '级', count: 0 })
217
+  }, { levelName: '级', count: 0 })
162 218
 
163 219
   // 2. 计算总人数
164 220
   const totalCount = pieData.reduce((sum, item) => sum + (item.count || 0), 0)
165 221
 
166 222
   // 3. 计算占比
167 223
   const highestPercentage = totalCount > 0 ? ((highestLevel.count / totalCount) * 100).toFixed(2) : '0.0'
168
-
224
+  let deptName = attendanceData.value && attendanceData.value.length > 0 ? attendanceData.value[attendanceData.value.length -
225
+    1]?.deptName : ''
169 226
   // 4. 生成第一部分描述文字
170
-  return `全站资质等级以"${highestLevel.levelName || '一级'}"为主(占比为${highestPercentage}%)`
227
+  return `${deptName}资质等级以"${highestLevel.levelName || '高级'}"为主(占比为${highestPercentage}%)`
171 228
 })
172 229
 
173 230
 // 计算属性:动态生成资质等级分布描述第二部分(句号后)
@@ -211,7 +268,7 @@ const qualificationPieDescriptionPart2 = computed(() => {
211 268
   }
212 269
 
213 270
   // 生成第二部分描述文字
214
-  return `全站资质等级为"级"的人员集中在${topDeptForLevel1}(${level1Count}人)${topDeptForLevel1}的人员规模(共${totalDeptCount}人)高于${allDeptNames.filter(name => name !== topDeptForLevel1).join(', ')}`
271
+  return `全站资质等级为"级"的人员集中在${topDeptForLevel1}(${level1Count}人)${topDeptForLevel1}的人员规模(共${totalDeptCount}人)高于${allDeptNames.filter(name => name !== topDeptForLevel1).join(', ')}`
215 272
 })
216 273
 
217 274
 
@@ -259,29 +316,63 @@ const fetchDutyOrganizationData = async (queryParams) => {
259 316
   try {
260 317
     // 处理query参数
261 318
     const processedParams = processQueryParams(queryParams)
262
-
319
+    const selectedDept = props.selectedDeptObject
320
+    const { deptType = "", id } = selectedDept ? selectedDept : { deptType: "", id: "" }
321
+    delete processedParams.deptId
322
+    let calculateParams = {
323
+      ...(['TEAMS', 'DEPARTMENT', 'BRIGADE','MANAGER'].includes(deptType) ? { deptId: id } : {}),
324
+      ...(deptType == 'USER' ? { userId: id } : {})
325
+    }
263 326
     // 获取出勤人次分析数据
264
-    const attendanceResponse = await getCalculate(processedParams)
327
+    const attendanceResponse = await getCalculate({ ...processedParams, ...calculateParams })
265 328
     console.log('出勤人次分析数据:', attendanceResponse)
266 329
     attendanceData.value = attendanceResponse || []
267 330
 
268 331
     // 获取出勤人次趋势数据
269
-    const trendResponse = await getCalculateTrendData(processedParams)
332
+    const trendResponse = await getCalculateTrendData({ ...processedParams, ...calculateParams })
270 333
     console.log('出勤人次趋势数据:', trendResponse)
271 334
     trendData.value = trendResponse || []
272 335
 
273
-    // 获取资质等级分布饼图数据
274
-    const pieResponse = await getQualificationPieChart(processedParams)
275
-    console.log('资质等级分布饼图数据:', pieResponse)
276
-    qualificationPieData.value = pieResponse.data || []
336
+    // 获取资质等级分布饼图数据(如果部门类型不是STATION)
337
+    if (isNotUserType.value) {
338
+      const pieResponse = await getQualificationPieChart({ ...processedParams, ...calculateParams })
339
+    
340
+      qualificationPieData.value = pieResponse.data || []
341
+    } else {
342
+      qualificationPieData.value = []
343
+    }
344
+
345
+    if (isStationType.value) {
346
+      //获取资质等级分布柱状图数据
347
+      const barResponse = await getQualificationBarChart({ ...processedParams, ...calculateParams })
348
+
349
+      qualificationBarData.value = barResponse.data?.brigades || []
350
+
351
+    } else if (isTeamsType.value) {
352
+
353
+      // 获取资质等级分布柱状图数据
354
+      const barResponse = await getQualificationBarChart({ ...processedParams, ...calculateParams })
355
+      console.log('资质等级分布柱状图数据:', barResponse.data?.brigades)
277 356
 
278
-    // 获取资质等级分布柱状图数据
279
-    const barResponse = await getQualificationBarChart(processedParams)
280
-    console.log('资质等级分布柱状图数据:', barResponse)
281
-    qualificationBarData.value = barResponse.data?.brigades || []
357
+      // 处理班组人员资质等级数据
358
+      if (barResponse.data?.brigades && Array.isArray(barResponse.data.brigades)) {
359
+        teamsQualificationData.value = barResponse.data.brigades
360
+      } else {
361
+        teamsQualificationData.value = []
362
+      }
363
+
364
+    } else if (isUserType.value) {
365
+      // 获取资质等级分布柱状图数据
366
+      const barResponse = await getQualificationBarChart({ ...processedParams, ...calculateParams })
367
+      console.log('资质等级分布柱状图数据:', barResponse.data.brigades)
368
+      user.value = barResponse?.data?.brigades[0]
369
+
370
+    }
282 371
 
283
-    // 更新图表和统计信息
284
-    updateChartsWithData()
372
+    setTimeout(() => {
373
+      // 更新图表和统计信息
374
+      updateChartsWithData()
375
+    }, 0);
285 376
 
286 377
   } catch (error) {
287 378
     console.error('获取勤务组织数据失败:', error)
@@ -294,7 +385,7 @@ watch(() => props.queryForm, (newQueryForm) => {
294 385
   if (newQueryForm.dateRangeQueryType && newQueryForm.year) {
295 386
     fetchDutyOrganizationData(newQueryForm)
296 387
   }
297
-}, { deep: true, immediate: true })
388
+}, { deep: true })
298 389
 
299 390
 // 饼图配置
300 391
 const pieOptions = {
@@ -468,7 +559,8 @@ const attendanceBarOptions = {
468 559
   },
469 560
   legend: {
470 561
     data: ['安检一大队', '安检二大队', '安检三大队', '安检综合大队', '全站'],
471
-    top: 0
562
+    top: 0,
563
+    show: true,
472 564
   },
473 565
   grid: {
474 566
     left: '3%',
@@ -576,6 +668,76 @@ const attendanceBarOptions = {
576 668
     }
577 669
   ]
578 670
 }
671
+// 出勤人次柱状图配置
672
+const attendanceBarOtherOptions = {
673
+  tooltip: {
674
+    trigger: 'axis',
675
+    axisPointer: {
676
+      type: 'shadow'
677
+    },
678
+    formatter: function (params) {
679
+      let result = `${params[0].axisValue}<br/>`
680
+      params.forEach(param => {
681
+        result += ` <span style="color:${param.color};font-weight:bold">${param.data}</span>人<br/>`
682
+      })
683
+      return result
684
+    }
685
+  },
686
+  legend: {
687
+    top: 0,
688
+    show: true,
689
+  },
690
+  grid: {
691
+    left: '3%',
692
+    right: '4%',
693
+    bottom: '0%',
694
+    top: '80',
695
+    containLabel: true
696
+  },
697
+  xAxis: {
698
+    type: 'category',
699
+    data: [],
700
+    axisLine: {
701
+      lineStyle: {
702
+        color: '#999'
703
+      }
704
+    },
705
+    axisLabel: {
706
+      fontSize: 12
707
+    }
708
+  },
709
+  yAxis: {
710
+    type: 'value',
711
+    name: '人数',
712
+    axisLine: {
713
+      lineStyle: {
714
+        color: '#999'
715
+      }
716
+    },
717
+    splitLine: {
718
+      lineStyle: {
719
+        color: '#f0f0f0'
720
+      }
721
+    }
722
+  },
723
+  series: [
724
+    {
725
+      name: '',
726
+      type: 'bar',
727
+      barWidth: '10%',
728
+      itemStyle: {
729
+        color: '#5470C6'
730
+      },
731
+      label: {
732
+        show: true,
733
+        position: 'top',
734
+        formatter: '{c}人'
735
+      },
736
+      data: []
737
+    },
738
+  ]
739
+}
740
+
579 741
 
580 742
 // 初始化图表
581 743
 onMounted(() => {
@@ -704,11 +866,34 @@ const updateTrendBarChart = () => {
704 866
     setBarOption(trendBarOptions)
705 867
   }
706 868
 }
869
+//非站长走这个逻辑
870
+const updateAttendanceBarOtherChart = () => {
871
+  if (trendData.value && Array.isArray(trendData.value)) {
872
+    const trendList = trendData.value
873
+    
874
+    // 提取横坐标数据(timeLabel字段)
875
+    const xAxisData = trendList.map(item => item.timeLabel || '未知时间')
876
+    // 提取各科室数据
877
+    const allData = trendList.map(item => item.overall || 0)
878
+    // 更新图表配置
879
+    attendanceBarOtherOptions.xAxis.data = xAxisData
880
+    attendanceBarOtherOptions.series[0].data = allData
881
+    attendanceBarOtherOptions.legend.show = !!isStationType.value
882
+    // 重新设置图表选项
883
+    setAttendanceOption(attendanceBarOtherOptions,true)
884
+  } else {
885
+    // 无数据时清空图表
886
+    attendanceBarOtherOptions.xAxis.data = []
887
+    attendanceBarOtherOptions.series[0].data = []
888
+    attendanceBarOtherOptions.legend.show = !!isStationType.value
889
+    setAttendanceOption(attendanceBarOtherOptions,true)
890
+  }
891
+}
707 892
 
708 893
 const updateAttendanceBarChart = () => {
709 894
   if (trendData.value && Array.isArray(trendData.value)) {
710 895
     const trendList = trendData.value
711
-    
896
+
712 897
     // 提取横坐标数据(timeLabel字段)
713 898
     const xAxisData = trendList.map(item => item.timeLabel || '未知时间')
714 899
 
@@ -726,6 +911,7 @@ const updateAttendanceBarChart = () => {
726 911
     attendanceBarOptions.series[2].data = dept2Data
727 912
     attendanceBarOptions.series[3].data = dept3Data
728 913
     attendanceBarOptions.series[4].data = dept4Data
914
+    attendanceBarOptions.legend.show = !!isStationType.value
729 915
 
730 916
     // 重新设置图表选项
731 917
     setAttendanceOption(attendanceBarOptions)
@@ -738,14 +924,21 @@ const updateAttendanceBarChart = () => {
738 924
     attendanceBarOptions.series[1].data = []
739 925
     attendanceBarOptions.series[2].data = []
740 926
     attendanceBarOptions.series[3].data = []
927
+    attendanceBarOptions.legend.show = !!isStationType.value
741 928
     setAttendanceOption(attendanceBarOptions)
742 929
   }
743 930
 }
744 931
 
745 932
 // 根据API数据更新图表和统计信息
746 933
 const updateChartsWithData = () => {
934
+  
747 935
   // 更新出勤人次柱状图
748
-  updateAttendanceBarChart()
936
+  if (isStationType.value) {
937
+    updateAttendanceBarChart()
938
+  } else{
939
+    updateAttendanceBarOtherChart()
940
+  }
941
+
749 942
   // 更新资质等级分布饼图
750 943
   updateQualificationPieChart()
751 944
   // 更新资质趋势柱状图
@@ -774,10 +967,10 @@ onUnmounted(() => {
774 967
 }
775 968
 
776 969
 .section-title h2 {
777
-  font-size: 24px;
970
+  margin: 0;
971
+  font-size: 18px;
778 972
   font-weight: 600;
779 973
   color: #333;
780
-  margin: 0;
781 974
 }
782 975
 
783 976
 /* 横向布局内容区域 */
@@ -805,6 +998,15 @@ onUnmounted(() => {
805 998
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
806 999
 }
807 1000
 
1001
+.right-panel-ability {
1002
+  flex: 1 1 30%;
1003
+  min-width: 400px;
1004
+  background: white;
1005
+  border-radius: 8px;
1006
+  padding: 20px;
1007
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1008
+}
1009
+
808 1010
 .panel-header {
809 1011
   margin-bottom: 20px;
810 1012
   padding-bottom: 0;

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

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

+ 54 - 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 && !isDepartmentType">
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 === 'MANAGER'
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,23 @@ 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
+    let progressParams = { ...queryParams }
300
+    if (progressParams.dateRangeQueryType === 'YEAR') {
301
+      progressParams.yearOnYear = true
302
+      delete progressParams.chainRatio
303
+    } else {
304
+      progressParams.chainRatio = true
305
+      progressParams.yearOnYear = true
306
+    }
307
+    let rangParams = {
308
+      userId: deptType == 'USER' ? id : "",
309
+      deptId: ["STATION", "MANAGER", "TEAMS", "BRIGADE"].includes(deptType) ? id : ""
310
+    }
311
+
312
+    const response = await getAnalysisReport({ ...progressParams, ...rangParams })
270 313
     console.log('质控分析数据:', response.data)
271 314
 
272 315
     // 解构赋值API返回的数据
@@ -332,7 +375,7 @@ watch(() => props.queryForm, (newQueryForm) => {
332 375
   if (newQueryForm.dateRangeQueryType && newQueryForm.year) {
333 376
     fetchAnalysisData(newQueryForm)
334 377
   }
335
-}, { deep: true, immediate: true })
378
+}, { deep: true })
336 379
 
337 380
 // 定义响应式数据
338 381
 const checkTask = ref({})

+ 342 - 132
src/views/assistant/components/riskHazard.vue

@@ -70,7 +70,7 @@
70 70
       <!-- 第二行 -->
71 71
       <div class="panel-row">
72 72
         <!-- 第四个:查获排名表格 -->
73
-        <div class="panel-item">
73
+        <div class="panel-item" v-show="!isUserType">
74 74
           <div class="panel-header">
75 75
             <h3>查获排名</h3>
76 76
           </div>
@@ -98,7 +98,7 @@
98 98
         </div>
99 99
 
100 100
         <!-- 第五个:查获岗位分布 -->
101
-        <div class="panel-item">
101
+        <div class="panel-item" v-show="!isUserType">
102 102
           <div class="panel-header">
103 103
             <h3>查获岗位分布</h3>
104 104
           </div>
@@ -117,7 +117,7 @@
117 117
         </div>
118 118
 
119 119
         <!-- 第六个:查获通道TOP5 -->
120
-        <div class="panel-item">
120
+        <div class="panel-item" v-show="!isUserType">
121 121
           <div class="panel-header">
122 122
             <h3>查获通道TOP5</h3>
123 123
           </div>
@@ -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 && !isDepartmentType">
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>
@@ -152,7 +231,13 @@ import {
152 231
   getConcealmentPositionStats,
153 232
   getDepartmentRanking,
154 233
   getPostCategoryStats,
155
-  getChannelRankingStats
234
+  getChannelRankingStats,
235
+  getPoliceData,
236
+  getPoliceDataStats,
237
+  getXrayMissCheck,
238
+  getXrayMissCheckStats,
239
+  getAbnormalSeizureData,
240
+  getAbnormalSeizureStats
156 241
 } from '@/api/assistant/assistant.js'
157 242
 
158 243
 // 定义props接收queryForm参数
@@ -165,6 +250,10 @@ const props = defineProps({
165 250
       quarter: '',
166 251
       month: ''
167 252
     })
253
+  },
254
+  selectedDeptObject: {
255
+    type: Object,
256
+    default: null
168 257
   }
169 258
 })
170 259
 
@@ -187,154 +276,190 @@ const concealmentPositionStatsData = ref({})
187 276
 const departmentRankingData = ref({})
188 277
 const postCategoryStatsData = ref({})
189 278
 const channelRankingStatsData = ref({})
279
+// 计算属性:处理selectedDeptObject逻辑
280
+const selectedDeptType = computed(() => {
281
+  return props.selectedDeptObject && props.selectedDeptObject?.deptType
282
+})
283
+
284
+// 计算属性:检查是否为STATION类型
285
+const isStationType = computed(() => {
286
+  return selectedDeptType.value === 'STATION' || !selectedDeptType.value
287
+})
288
+// 计算属性:检查是否为STATION类型
289
+const isDepartmentType = computed(() => {
290
+  return selectedDeptType.value === 'MANAGER'
291
+})
292
+
293
+const isBrigadeType = computed(() => {
294
+  return selectedDeptType.value === 'BRIGADE'
295
+})
296
+
297
+// 计算属性:检查是否为TEAMS类型
298
+const isTeamsType = computed(() => {
299
+  return selectedDeptType.value === 'TEAMS'
300
+})
301
+
302
+//
303
+const isUserType = computed(() => {
304
+  return selectedDeptType.value === 'USER'
305
+})
306
+// 新增三个API接口的响应式数据
307
+const policeTransferData = ref([])
308
+const xrayMissData = ref([])
309
+const abnormalCaptureData = ref([])
310
+
311
+// 新增三个统计API接口的响应式数据
312
+const policeTransferStats = ref('')
313
+const xrayMissStats = ref('')
314
+const abnormalCaptureStats = ref('')
190 315
 
191 316
 // 计算属性:动态生成查获违禁品类别描述
192 317
 const categoryStatsDescription = computed(() => {
193
-  
318
+
194 319
   if (!categoryStatsData.value || !categoryStatsData.value || !Array.isArray(categoryStatsData.value)) {
195 320
     return '查获物品以[占比最高物品类型]为主,占比达[X]%,其次为[第二高占比物品类型]([X]%)、[第三高占比物品类型]([X]%),需重点强化对应物品的安检识别与管控力度。'
196 321
   }
197 322
 
198 323
   const categoryData = categoryStatsData.value
199
-  
324
+
200 325
   // 1. 按数量排序,获取前三名
201 326
   const sortedData = [...categoryData].sort((a, b) => (b.quantity || 0) - (a.quantity || 0))
202
-  
327
+
203 328
   // 2. 计算总数量
204 329
   const totalCount = sortedData.reduce((sum, item) => sum + (item.quantity || 0), 0)
205
-  
330
+
206 331
   // 3. 获取前三名数据
207 332
   const top1 = sortedData[0] || { categoryNameOne: '未知类型', quantity: 0 }
208 333
   const top2 = sortedData[1] || { categoryNameOne: '未知类型', quantity: 0 }
209 334
   const top3 = sortedData[2] || { categoryNameOne: '未知类型', quantity: 0 }
210
-  
335
+
211 336
   // 4. 计算百分比
212 337
   const top1Percentage = totalCount > 0 ? ((top1.quantity / totalCount) * 100).toFixed(2) : '0.00'
213 338
   const top2Percentage = totalCount > 0 ? ((top2.quantity / totalCount) * 100).toFixed(2) : '0.00'
214 339
   const top3Percentage = totalCount > 0 ? ((top3.quantity / totalCount) * 100).toFixed(2) : '0.00'
215
-  
340
+
216 341
   // 5. 生成描述文字
217
-  return `查获物品以${top1.categoryNameOne  || '未知类型'}为主,占比达${top1Percentage}%,其次为${top2.categoryNameOne  || '未知类型'}(${top2Percentage}%)、${top3.categoryNameOne  || '未知类型'}(${top3Percentage}%),需重点强化对应物品的安检识别与管控力度。`
342
+  return `查获物品以${top1.categoryNameOne || '未知类型'}为主,占比达${top1Percentage}%,其次为${top2.categoryNameOne || '未知类型'}(${top2Percentage}%)、${top3.categoryNameOne || '未知类型'}(${top3Percentage}%),需重点强化对应物品的安检识别与管控力度。`
218 343
 })
219 344
 
220 345
 // 计算属性:动态生成查获时间趋势描述
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
-    }
346
+const seizureTimeTrendDescription = computed(() => {
347
+  if (!seizureTimeTrendData.value || !seizureTimeTrendData.value || !Array.isArray(seizureTimeTrendData.value)) {
348
+    return '高峰时段:XX:XX、XX:XX左右(平均查获量达XX件),需强化对应时段的安检力度。低峰时段:XX:XX、XX:XX后(平均查获量XX件)'
349
+  }
225 350
 
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
-    }
351
+  const trendData = seizureTimeTrendData.value
258 352
 
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
-    }
353
+  // 1. 按查获量排序,找出最高和最低的两个时段
354
+  const sortedData = [...trendData].sort((a, b) => (b.total || 0) - (a.total || 0))
277 355
 
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
-    }
356
+  // 2. 获取最高的两个时段
357
+  const peakHours = sortedData.slice(0, 2).map(item => item.hourOfDay || '').filter(Boolean)
296 358
 
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
-    }
359
+  // 3. 获取最低的两个时段
360
+  const lowHours = sortedData.slice(-2).map(item => item.hourOfDay || '').filter(Boolean)
361
+
362
+  // 4. 计算高峰时段平均查获量
363
+  const peakData = trendData.filter(item => peakHours.some(hour => item.hourOfDay?.includes(hour)))
364
+  const peakAvg = peakData.length > 0
365
+    ? Math.round(peakData.reduce((sum, item) => sum + (item.total || 0), 0) / peakData.length)
366
+    : 0
367
+
368
+  // 5. 计算低峰时段平均查获量
369
+  const lowData = trendData.filter(item => lowHours.some(hour => item.hourOfDay?.includes(hour)))
370
+  const lowAvg = lowData.length > 0
371
+    ? Math.round(lowData.reduce((sum, item) => sum + (item.total || 0), 0) / lowData.length)
372
+    : 0
373
+
374
+  // 6. 生成描述文字
375
+  return `高峰时段:${peakHours.join('、')}左右(平均查获量达${peakAvg}件),需强化对应时段的安检力度。低峰时段:${lowHours.join('、')}后(平均查获量${lowAvg}件)`
376
+})
377
+
378
+// 计算属性:动态生成隐匿物品查获部位描述
379
+const concealmentPositionStatsDescription = computed(() => {
380
+  if (!concealmentPositionStatsData.value || !Array.isArray(concealmentPositionStatsData.value)) {
381
+    return '违禁品主要藏匿于"XX",是安检搜检的重点部位。'
382
+  }
383
+
384
+  const positionData = concealmentPositionStatsData.value
385
+
386
+  // 1. 按查获量排序,找出查获量最高的部位
387
+  const sortedData = [...positionData].sort((a, b) => (b.count || 0) - (a.count || 0))
388
+
389
+  // 2. 获取查获量最高的部位
390
+  const topPosition = sortedData[0] || { positionName: 'XX', count: 0 }
391
+
392
+  // 3. 生成描述文字
393
+  return `违禁品主要藏匿于"${topPosition.positionName || 'XX'}",是安检搜检的重点部位。`
394
+})
395
+
396
+// 计算属性:动态生成大队查获排名描述
397
+const departmentRankingDescription = computed(() => {
398
+
399
+  if (!departmentRankingData.value || !Array.isArray(departmentRankingData.value)) {
400
+    return 'XXXX是违禁品查获的主力大队,查获违禁物品数量为XX,占比XX%。'
401
+  }
320 402
 
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
-  })
403
+  const rankingData = departmentRankingData.value
404
+
405
+  // 1. 按查获量排序,找出查获量最高的大队
406
+  const sortedData = [...rankingData].sort((a, b) => (b.seizureCount || 0) - (a.seizureCount || 0))
407
+
408
+  // 2. 获取查获量最高的大队
409
+  const topDepartment = sortedData[0] || { departmentName: 'XXXX', seizureCount: 234 }
410
+
411
+  const str = isStationType.value ? '大队' : isBrigadeType.value ? '主管' : isDepartmentType.value ? '班组' : isTeamsType.value ? '成员' : ''
412
+  // 5. 生成描述文字
413
+  return `${topDepartment.brigadeName || 'XXXX'}是违禁品查获的主力${str},查获违禁物品数量为${topDepartment.seizureCount || 'XX'},占比${topDepartment.currentRatio}%。`
414
+})
415
+
416
+// 计算属性:动态生成查获岗位分布描述
417
+const postCategoryStatsDescription = computed(() => {
418
+  if (!postCategoryStatsData.value || !Array.isArray(postCategoryStatsData.value)) {
419
+    return '"XX"的查获数量最多,为XX件,占比XX%。'
420
+  }
421
+
422
+  const postData = postCategoryStatsData.value
423
+
424
+  // 1. 按查获量排序,找出查获量最高的岗位
425
+  const sortedData = [...postData].sort((a, b) => (b.quantity || 0) - (a.quantity || 0))
426
+
427
+  // 2. 获取查获量最高的岗位
428
+  const topPost = sortedData[0] || { postName: 'XX', quantity: 124 }
429
+
430
+  // 3. 计算总查获量
431
+  const totalCount = postData.reduce((sum, item) => sum + (item.quantity || 0), 0)
432
+
433
+  // 4. 计算占比
434
+  const percentage = totalCount > 0 ? ((topPost.quantity / totalCount) * 100).toFixed(0) : '45'
435
+
436
+  // 5. 生成描述文字
437
+  return `"${topPost.postName || 'XX'}"的查获数量最多,为${topPost.quantity || 124}件,占比${percentage}%。`
438
+})
439
+
440
+// 计算属性:动态生成查获通道TOP5描述
441
+const channelRankingStatsDescription = computed(() => {
442
+  if (!channelRankingStatsData.value || !Array.isArray(channelRankingStatsData.value)) {
443
+    return '违禁物品查获主要集中于XXXX,查获数量为XX,占比XX%。'
444
+  }
445
+
446
+  const channelData = channelRankingStatsData.value
447
+
448
+  // 1. 按查获量排序,找出查获量最高的通道
449
+  const sortedData = [...channelData].sort((a, b) => (b.seizureQuantity || 0) - (a.seizureQuantity || 0))
450
+
451
+  // 2. 获取查获量最高的通道
452
+  const topChannel = sortedData[0] || { channelName: 'XXXX', seizureQuantity: 123 }
453
+
454
+  // 3. 计算总查获量
455
+  const totalCount = channelData.reduce((sum, item) => sum + (item.seizureQuantity || 0), 0)
456
+
457
+  // 4. 计算占比
458
+  const percentage = totalCount > 0 ? ((topChannel.seizureQuantity / totalCount) * 100).toFixed(0) : '23'
459
+
460
+  // 5. 生成描述文字
461
+  return `违禁物品查获主要集中于${topChannel.channelName || 'XXXX'},查获数量为${topChannel.seizureQuantity || 'XX'},占比${percentage}%。`
462
+})
338 463
 
339 464
 // 表格数据
340 465
 const captureRankData = ref([])
@@ -349,7 +474,29 @@ const processQueryParams = (queryParams) => {
349 474
     processedParams.chainRatio = true
350 475
     processedParams.yearOnYear = true
351 476
   }
352
-  return processedParams
477
+  const selectedDept = props.selectedDeptObject
478
+  const { deptType = "", id } = selectedDept ? selectedDept : { deptType: "", id: "" }
479
+
480
+
481
+  let otherParams = {
482
+    ...(deptType == 'BRIGADE' ? { brigadeId: id } : {}),
483
+    ...(deptType == 'MANAGER' ? { departmentId: id } : {}),
484
+    ...(deptType == 'TEAMS' ? { teamId: id } : {}),
485
+    ...(deptType == 'USER' ? { userId: id } : {})
486
+  }
487
+  delete processedParams.deptId;
488
+  return { ...processedParams, ...otherParams }
489
+}
490
+const handleAbnormalCaptureStats = (data) => {
491
+  const { higList, lowList } = data;
492
+  let first = !!higList && higList.map((item) => item.departmentName).length > 0 ? `${higList.map((item) => item.departmentName).join("、")}查获违禁品数量显著高于整体水平` : '';
493
+  let second = !!lowList && lowList.map((item) => item.departmentName).length > 0 ? `${lowList.map((item) => item.departmentName).join("、")}查获违禁品数量显著低于整体水平` : '';
494
+  return `${first}${!!second ? ',' : first && second ? '。' : ''}${second}`
495
+}
496
+const handlePoliceTransferStats = (data) => {
497
+  const { totalQuantity, brigadeRankList } = data;
498
+  const topDepartment = brigadeRankList.map(item => item.brigadeName).join('、')
499
+  return `移交公安的违禁品数量共${totalQuantity}件,主要集中于${topDepartment}。`
353 500
 }
354 501
 
355 502
 // 调用API获取风险隐患数据
@@ -358,6 +505,7 @@ const fetchRiskHazardData = async (queryParams) => {
358 505
     // 处理query参数
359 506
     const processedParams = processQueryParams(queryParams)
360 507
 
508
+
361 509
     // 按顺序调用六个API接口
362 510
 
363 511
     // 1. 查获违禁品类别统计
@@ -390,6 +538,42 @@ const fetchRiskHazardData = async (queryParams) => {
390 538
     console.log('查获通道TOP5:', channelRankingStatsResponse)
391 539
     channelRankingStatsData.value = channelRankingStatsResponse?.data?.channelRankings || []
392 540
 
541
+    // 7. 移交公安数据
542
+    const policeDataResponse = await getPoliceData(processedParams)
543
+    console.log('移交公安数据:', policeDataResponse)
544
+    policeTransferData.value = policeDataResponse?.data || []
545
+
546
+    // 8. X光机漏检数据
547
+    const xrayMissCheckResponse = await getXrayMissCheck(processedParams)
548
+    console.log('X光机漏检数据:', xrayMissCheckResponse)
549
+    xrayMissData.value = xrayMissCheckResponse?.data || []
550
+
551
+    // 9. 可能异常查获数据(只有当部门类型是STATION时才请求)
552
+   
553
+    if (isStationType.value || isBrigadeType.value || isDepartmentType.value) {
554
+      const abnormalSeizureResponse = await getAbnormalSeizureData(processedParams)
555
+      console.log('可能异常查获数据:', abnormalSeizureResponse)
556
+      abnormalCaptureData.value = abnormalSeizureResponse?.data || []
557
+      // 12. 可能异常查获统计数据(描述卡片文本)
558
+      const abnormalSeizureStatsResponse = await getAbnormalSeizureStats(processedParams)
559
+      console.log('可能异常查获统计数据:', abnormalSeizureStatsResponse, isStationType.value)
560
+      // debugger
561
+      abnormalCaptureStats.value = handleAbnormalCaptureStats(abnormalSeizureStatsResponse?.data)
562
+    } else {
563
+      abnormalCaptureData.value = []
564
+      abnormalCaptureStats.value = ''
565
+    }
566
+
567
+    // 10. 移交公安统计数据(描述卡片文本)
568
+    const policeDataStatsResponse = await getPoliceDataStats(processedParams)
569
+    console.log('移交公安统计数据:', policeDataStatsResponse)
570
+    policeTransferStats.value = handlePoliceTransferStats(policeDataStatsResponse?.data)
571
+
572
+    // 11. X光机漏检统计数据(描述卡片文本)
573
+    const xrayMissCheckStatsResponse = await getXrayMissCheckStats(processedParams)
574
+    console.log('X光机漏检统计数据:', xrayMissCheckStatsResponse)
575
+    let userName = xrayMissCheckStatsResponse?.data?.map(item => item.xrayOperatorName).join('、') || ''
576
+    xrayMissStats.value = `X光机漏检事件主要集中于以下开机员:${userName},可针对性开展判图技能强化培训。`
393 577
     // 更新图表和表格数据
394 578
     updateChartsWithData()
395 579
 
@@ -670,7 +854,7 @@ watch(() => props.queryForm, (newQueryForm) => {
670 854
   if (newQueryForm.dateRangeQueryType && newQueryForm.year) {
671 855
     fetchRiskHazardData(newQueryForm)
672 856
   }
673
-}, { deep: true, immediate: true })
857
+}, { deep: true })
674 858
 
675 859
 // 根据API数据更新图表和表格数据
676 860
 const updateChartsWithData = () => {
@@ -861,26 +1045,27 @@ onUnmounted(() => {
861 1045
 }
862 1046
 
863 1047
 .section-title h2 {
864
-  font-size: 24px;
1048
+  margin: 0;
1049
+  font-size: 18px;
865 1050
   font-weight: 600;
866 1051
   color: #333;
867
-  margin: 0;
868 1052
 }
869 1053
 
870 1054
 /* 六个部分布局 */
871 1055
 .six-panel-layout {
872
-  display: flex;
873
-  flex-direction: column;
1056
+  display: grid;
1057
+  grid-template-columns: repeat(3, 1fr);
874 1058
   gap: 16px;
1059
+  grid-auto-flow: dense;
1060
+  /* 自动填充空白区域 */
875 1061
 }
876 1062
 
877 1063
 .panel-row {
878
-  display: flex;
879
-  gap: 16px;
1064
+  display: contents;
1065
+  /* 让子元素直接参与网格布局 */
880 1066
 }
881 1067
 
882 1068
 .panel-item {
883
-  flex: 1;
884 1069
   background: white;
885 1070
   border-radius: 8px;
886 1071
   padding: 20px;
@@ -888,6 +1073,10 @@ onUnmounted(() => {
888 1073
   display: flex;
889 1074
   flex-direction: column;
890 1075
   gap: 16px;
1076
+  min-width: 0;
1077
+  /* 防止内容溢出 */
1078
+  overflow: hidden;
1079
+  /* 隐藏溢出内容 */
891 1080
 }
892 1081
 
893 1082
 .panel-header {
@@ -958,7 +1147,28 @@ onUnmounted(() => {
958 1147
   flex: 1;
959 1148
   border: 1px solid #E4E7ED;
960 1149
   border-radius: 4px;
1150
+  max-height: 300px;
1151
+  overflow: auto;
1152
+  position: relative;
1153
+}
1154
+
1155
+/* Element Plus表格横向滚动配置 */
1156
+.table-container :deep(.el-table) {
1157
+  width: 100%;
1158
+  min-width: 100%;
1159
+}
1160
+
1161
+.table-container :deep(.el-table .el-table__header-wrapper),
1162
+.table-container :deep(.el-table .el-table__body-wrapper) {
1163
+  overflow-x: auto !important;
1164
+}
1165
+
1166
+.table-container :deep(.el-table__header) {
1167
+  width: auto !important;
1168
+}
961 1169
 
1170
+.table-container :deep(.el-table__body) {
1171
+  width: auto !important;
962 1172
 }
963 1173
 
964 1174
 /* 表格偶数行背景色 */

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 31 - 32
src/views/assistant/components/useReports.vue


+ 35 - 2
src/views/item/record/index.vue

@@ -23,6 +23,19 @@
23 23
         <el-input v-model="queryParams.inspectTeamName" placeholder="请输入查获班组" clearable @keyup.enter="handleQuery" />
24 24
       </el-form-item>
25 25
 
26
+      <el-form-item label="流程状态" prop="processStatus">
27
+        <el-select v-model="queryParams.processStatus" placeholder="请选择流程状态" clearable style="width: 200px">
28
+          <el-option v-for="dict in process_status" :key="dict.value" :label="dict.label" :value="dict.value" />
29
+        </el-select>
30
+      </el-form-item>
31
+
32
+      <el-form-item label="开机指令" prop="powerOnInstruction">
33
+        <el-select v-model="queryParams.powerOnInstruction" placeholder="请选择开机指令" clearable style="width: 200px">
34
+          <el-option label="指令" :value="0" />
35
+          <el-option label="非指令" :value="1" />
36
+        </el-select>
37
+      </el-form-item>
38
+
26 39
       <!-- <el-form-item label="违禁类型" prop="forbiddenTypeText">
27 40
         <el-input v-model="queryParams.forbiddenTypeText" placeholder="请输入违禁品类型" clearable @keyup.enter="handleQuery" />
28 41
       </el-form-item>
@@ -76,7 +89,7 @@
76 89
           <span>{{ parseTime(scope.row.seizureTime, '{y}-{m}-{d}') }}</span>
77 90
         </template>
78 91
       </el-table-column>
79
-      <el-table-column label="安检位置" align="center" prop="regionalName" >
92
+      <el-table-column label="安检位置" align="center" prop="regionalName">
80 93
         <template #default="scope">
81 94
           <span>{{ `${scope.row.terminlName}/${scope.row.regionalName}/${scope.row.channelName}` }}</span>
82 95
         </template>
@@ -84,7 +97,7 @@
84 97
       <el-table-column label="安检岗位" align="center" prop="checkMethodDesc" />
85 98
       <el-table-column label="查获班组" align="center" prop="inspectTeamName" />
86 99
       <el-table-column label="上报班组" align="center" prop="attendanceTeamName" />
87
-      <el-table-column label="流程状态" align="center" prop="processStatus" >
100
+      <el-table-column label="流程状态" align="center" prop="processStatus">
88 101
         <template #default="scope">
89 102
           <dict-tag :options="process_status" :value="scope.row.processStatus" />
90 103
         </template>
@@ -146,6 +159,24 @@
146 159
             <el-option label="是" :value="1" />
147 160
           </el-select>
148 161
         </el-form-item>
162
+        <el-form-item label="是否常见违禁品" prop="commonContraband">
163
+          <el-select v-model="form.itemSeizureItemsList[0].commonContraband" placeholder="-">
164
+            <el-option label="否" :value="0" />
165
+            <el-option label="是" :value="1" />
166
+          </el-select>
167
+        </el-form-item>
168
+        <el-form-item label="违禁品描述" prop="contrabandDesc">
169
+          <el-input v-model="form.itemSeizureItemsList[0].contrabandDesc" placeholder="-" />
170
+        </el-form-item>
171
+        <el-form-item label="开机指令" prop="powerOnInstruction">
172
+          <el-select v-model="form.powerOnInstruction" placeholder="-">
173
+            <el-option label="指令" :value="0" />
174
+            <el-option label="非指令" :value="1" />
175
+          </el-select>
176
+        </el-form-item>
177
+        <el-form-item label="X光开机员" prop="xrayOperatorName">
178
+          <el-input v-model="form.xrayOperatorName" placeholder="-" />
179
+        </el-form-item>
149 180
         <!-- <el-form-item label="旅客姓名" prop="passengerName">
150 181
           <el-input v-model="form.passengerName" placeholder="请输入旅客姓名" />
151 182
         </el-form-item>
@@ -205,6 +236,8 @@ const data = reactive({
205 236
     passengerFlight: null,
206 237
     inspectTeamId: null,
207 238
     inspectTeamName: null,
239
+    powerOnInstruction: null,
240
+    processStatus: null,
208 241
     inspectDepartmentId: null,
209 242
     inspectDepartmentName: null,
210 243
     inspectStationId: null,