riskHazard.vue 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206
  1. <template>
  2. <div class="risk-hazard">
  3. <!-- 风险隐患标题 -->
  4. <div class="section-title">
  5. <h2>风险隐患</h2>
  6. </div>
  7. <!-- 六个部分布局,一行三个,共两行 -->
  8. <div class="six-panel-layout">
  9. <!-- 第一行 -->
  10. <div class="panel-row">
  11. <!-- 第一个:查获违禁品类别 -->
  12. <div class="panel-item">
  13. <div class="panel-header">
  14. <h3>查获违禁品类别</h3>
  15. </div>
  16. <!-- 描述卡片 -->
  17. <div class="describe-card">
  18. <div class="describe-content">
  19. {{ categoryStatsDescription }}
  20. </div>
  21. </div>
  22. <!-- 饼图 -->
  23. <div class="chart-container">
  24. <div ref="captureRankChartRef" class="echarts-chart"></div>
  25. </div>
  26. </div>
  27. <!-- 第二个:问题发现统计 -->
  28. <div class="panel-item">
  29. <div class="panel-header">
  30. <h3>问题发现统计</h3>
  31. </div>
  32. <!-- 描述卡片 -->
  33. <div class="describe-card">
  34. <div class="describe-content">
  35. {{ seizureTimeTrendDescription }}
  36. </div>
  37. </div>
  38. <!-- 折线图 -->
  39. <div class="chart-container">
  40. <div ref="problemDiscoveryChartRef" class="echarts-chart"></div>
  41. </div>
  42. </div>
  43. <!-- 第三个:隐匿物品查获部位 -->
  44. <div class="panel-item">
  45. <div class="panel-header">
  46. <h3>隐匿物品查获部位</h3>
  47. </div>
  48. <!-- 描述卡片 -->
  49. <div class="describe-card">
  50. <div class="describe-content">
  51. {{ concealmentPositionStatsDescription }}
  52. </div>
  53. </div>
  54. <!-- 饼图 -->
  55. <div class="chart-container">
  56. <div ref="problemRectificationChartRef" class="echarts-chart"></div>
  57. </div>
  58. </div>
  59. </div>
  60. <!-- 第二行 -->
  61. <div class="panel-row">
  62. <!-- 第四个:查获排名表格 -->
  63. <div class="panel-item" v-show="!isUserType">
  64. <div class="panel-header">
  65. <h3>查获排名</h3>
  66. </div>
  67. <!-- 描述卡片 -->
  68. <div class="describe-card">
  69. <div class="describe-content">
  70. {{ departmentRankingDescription }}
  71. </div>
  72. </div>
  73. <!-- 表格 -->
  74. <div class="table-container">
  75. <el-table :data="captureRankData" style="width: 100%" size="small">
  76. <el-table-column prop="rank" label="排名" align="center" />
  77. <el-table-column prop="department" label="大队" />
  78. <el-table-column prop="percentage" label="占比" align="center">
  79. <template #default="{ row }">
  80. <span>{{ row.percentage }}%</span>
  81. </template>
  82. </el-table-column>
  83. <el-table-column prop="count" label="数量" align="center" />
  84. </el-table>
  85. </div>
  86. </div>
  87. <!-- 第五个:查获岗位分布 -->
  88. <div class="panel-item" v-show="!isUserType">
  89. <div class="panel-header">
  90. <h3>查获岗位分布</h3>
  91. </div>
  92. <!-- 描述卡片 -->
  93. <div class="describe-card">
  94. <div class="describe-content">
  95. {{ postCategoryStatsDescription }}
  96. </div>
  97. </div>
  98. <!-- 饼图 -->
  99. <div class="chart-container">
  100. <div ref="rectificationStatsChartRef" class="echarts-chart"></div>
  101. </div>
  102. </div>
  103. <!-- 第六个:查获通道TOP5 -->
  104. <div class="panel-item" v-show="!isUserType">
  105. <div class="panel-header">
  106. <h3>查获通道TOP5</h3>
  107. </div>
  108. <!-- 描述卡片 -->
  109. <div class="describe-card">
  110. <div class="describe-content">
  111. {{ channelRankingStatsDescription }}
  112. </div>
  113. </div>
  114. <!-- 表格 -->
  115. <div class="table-container">
  116. <el-table :data="captureChannelData" style="width: 100%" size="small">
  117. <el-table-column prop="channel" label="查获通道" />
  118. <el-table-column prop="count" label="查获数量" align="center" />
  119. <el-table-column prop="area" label="所在区域" />
  120. </el-table>
  121. </div>
  122. </div>
  123. <!-- 第七个:移交公安数据 -->
  124. <div class="panel-item">
  125. <div class="panel-header">
  126. <h3>移交公安数据</h3>
  127. </div>
  128. <!-- 描述卡片 -->
  129. <div class="describe-card">
  130. <div class="describe-content">
  131. {{ policeTransferStats || '移交公安的违禁品数量共X件,主要集中于XX。' }}
  132. </div>
  133. </div>
  134. <!-- 表格 -->
  135. <div class="table-container">
  136. <el-table :data="policeTransferData" size="small" height="250" :scroll="{ x: 'max-content' }">
  137. <el-table-column prop="departmentName" label="主管" min-width="120" />
  138. <el-table-column prop="teamName" label="班组" min-width="120" />
  139. <el-table-column prop="userName" label="姓名" min-width="100" />
  140. <el-table-column prop="itemName" label="查获物品" min-width="150" />
  141. <el-table-column prop="quantity" label="查获数量" align="center" width="100" />
  142. <el-table-column prop="positionName" label="查获部位" min-width="120" />
  143. <el-table-column prop="seizureTime" label="查获时间" min-width="150" />
  144. <el-table-column prop="channelName" label="查获通道" min-width="120" />
  145. </el-table>
  146. </div>
  147. </div>
  148. <!-- 第八个:X光机漏检数据 -->
  149. <div class="panel-item" v-show="!isUserType">
  150. <div class="panel-header">
  151. <h3>X光机漏检数据</h3>
  152. </div>
  153. <!-- 描述卡片 -->
  154. <div class="describe-card">
  155. <div class="describe-content">
  156. {{ xrayMissStats || 'X光机漏检事件主要集中于以下开机员:张三、李四、王五,可针对性开展判图技能强化培训。' }}
  157. </div>
  158. </div>
  159. <!-- 表格 -->
  160. <div class="table-container">
  161. <el-table :data="xrayMissData" size="small" height="250" :scroll="{ x: 'max-content' }">
  162. <el-table-column prop="departmentName" label="主管" min-width="120" />
  163. <el-table-column prop="teamName" label="班组" min-width="120" />
  164. <el-table-column prop="userName" label="姓名" min-width="100" />
  165. <el-table-column prop="missItemName" label="漏检物品" min-width="150" />
  166. <el-table-column prop="missQuantity" label="漏检数量" align="center" width="100" />
  167. <el-table-column prop="missPosition" label="漏检部位" min-width="120" />
  168. <el-table-column prop="channelName" label="漏检通道" min-width="120" />
  169. </el-table>
  170. </div>
  171. </div>
  172. <!-- 第九个:可能异常查获数据 -->
  173. <div class="panel-item" v-show="!isTeamsType && !isUserType && !isDepartmentType">
  174. <div class="panel-header">
  175. <h3>可能异常查获数据</h3>
  176. </div>
  177. <!-- 描述卡片 -->
  178. <div class="describe-card" v-if="abnormalCaptureStats">
  179. <div class="describe-content">
  180. {{ abnormalCaptureStats || '旅检一科、旅检二科、旅检三科查获违禁品数量显著高于整体水平,旅检四科、旅检五科查获违禁品数量显著低于整体水平。' }}
  181. </div>
  182. </div>
  183. <!-- 表格 -->
  184. <div class="table-container">
  185. <el-table :data="abnormalCaptureData" 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="seizureQuantity" label="查获数量" align="center" width="100" />
  190. </el-table>
  191. </div>
  192. </div>
  193. </div>
  194. </div>
  195. </div>
  196. </template>
  197. <script setup>
  198. import { ref, onMounted, onUnmounted, watch } from 'vue'
  199. import { useEcharts } from '@/hooks/chart.js'
  200. import {
  201. getCategoryStats,
  202. getSeizureTimeTrend,
  203. getConcealmentPositionStats,
  204. getDepartmentRanking,
  205. getPostCategoryStats,
  206. getChannelRankingStats,
  207. getPoliceData,
  208. getPoliceDataStats,
  209. getXrayMissCheck,
  210. getXrayMissCheckStats,
  211. getAbnormalSeizureData,
  212. getAbnormalSeizureStats
  213. } from '@/api/assistant/assistant.js'
  214. // 定义props接收queryForm参数
  215. const props = defineProps({
  216. queryForm: {
  217. type: Object,
  218. default: () => ({
  219. dateRangeQueryType: '',
  220. year: '',
  221. quarter: '',
  222. month: ''
  223. })
  224. },
  225. selectedDeptObject: {
  226. type: Object,
  227. default: null
  228. }
  229. })
  230. // 图表容器引用
  231. const captureRankChartRef = ref(null)
  232. const problemDiscoveryChartRef = ref(null)
  233. const problemRectificationChartRef = ref(null)
  234. const rectificationStatsChartRef = ref(null)
  235. // 图表实例
  236. const { setOption: setCaptureRankOption, dispose: disposeCaptureRank } = useEcharts(captureRankChartRef)
  237. const { setOption: setProblemDiscoveryOption, dispose: disposeProblemDiscovery } = useEcharts(problemDiscoveryChartRef)
  238. const { setOption: setProblemRectificationOption, dispose: disposeProblemRectification } = useEcharts(problemRectificationChartRef)
  239. const { setOption: setRectificationStatsOption, dispose: disposeRectificationStats } = useEcharts(rectificationStatsChartRef)
  240. // 六个API接口的响应式数据
  241. const categoryStatsData = ref({})
  242. const seizureTimeTrendData = ref({})
  243. const concealmentPositionStatsData = ref({})
  244. const departmentRankingData = ref({})
  245. const postCategoryStatsData = ref({})
  246. const channelRankingStatsData = ref({})
  247. // 计算属性:处理selectedDeptObject逻辑
  248. const selectedDeptType = computed(() => {
  249. return props.selectedDeptObject && props.selectedDeptObject?.deptType
  250. })
  251. // 计算属性:检查是否为STATION类型
  252. const isStationType = computed(() => {
  253. return selectedDeptType.value === 'STATION' || !selectedDeptType.value
  254. })
  255. // 计算属性:检查是否为STATION类型
  256. const isDepartmentType = computed(() => {
  257. return selectedDeptType.value === 'MANAGER'
  258. })
  259. const isBrigadeType = computed(() => {
  260. return selectedDeptType.value === 'BRIGADE'
  261. })
  262. // 计算属性:检查是否为TEAMS类型
  263. const isTeamsType = computed(() => {
  264. return selectedDeptType.value === 'TEAMS'
  265. })
  266. //
  267. const isUserType = computed(() => {
  268. return selectedDeptType.value === 'USER'
  269. })
  270. // 新增三个API接口的响应式数据
  271. const policeTransferData = ref([])
  272. const xrayMissData = ref([])
  273. const abnormalCaptureData = ref([])
  274. // 新增三个统计API接口的响应式数据
  275. const policeTransferStats = ref('')
  276. const xrayMissStats = ref('')
  277. const abnormalCaptureStats = ref('')
  278. // 计算属性:动态生成查获违禁品类别描述
  279. const categoryStatsDescription = computed(() => {
  280. if (!categoryStatsData.value || !categoryStatsData.value || !Array.isArray(categoryStatsData.value)) {
  281. return '查获物品以[占比最高物品类型]为主,占比达[X]%,其次为[第二高占比物品类型]([X]%)、[第三高占比物品类型]([X]%),需重点强化对应物品的安检识别与管控力度。'
  282. }
  283. const categoryData = categoryStatsData.value
  284. // 1. 按数量排序,获取前三名
  285. const sortedData = [...categoryData].sort((a, b) => (b.quantity || 0) - (a.quantity || 0))
  286. // 2. 计算总数量
  287. const totalCount = sortedData.reduce((sum, item) => sum + (item.quantity || 0), 0)
  288. // 3. 获取前三名数据
  289. const top1 = sortedData[0] || { categoryNameOne: '未知类型', quantity: 0 }
  290. const top2 = sortedData[1] || { categoryNameOne: '未知类型', quantity: 0 }
  291. const top3 = sortedData[2] || { categoryNameOne: '未知类型', quantity: 0 }
  292. // 4. 计算百分比
  293. const top1Percentage = totalCount > 0 ? ((top1.quantity / totalCount) * 100).toFixed(2) : '0.00'
  294. const top2Percentage = totalCount > 0 ? ((top2.quantity / totalCount) * 100).toFixed(2) : '0.00'
  295. const top3Percentage = totalCount > 0 ? ((top3.quantity / totalCount) * 100).toFixed(2) : '0.00'
  296. // 5. 生成描述文字
  297. return `查获物品以${top1.categoryNameOne || '未知类型'}为主,占比达${top1Percentage}%,其次为${top2.categoryNameOne || '未知类型'}(${top2Percentage}%)、${top3.categoryNameOne || '未知类型'}(${top3Percentage}%),需重点强化对应物品的安检识别与管控力度。`
  298. })
  299. // 计算属性:动态生成查获时间趋势描述
  300. const seizureTimeTrendDescription = computed(() => {
  301. if (!seizureTimeTrendData.value || !seizureTimeTrendData.value || !Array.isArray(seizureTimeTrendData.value)) {
  302. return '高峰时段:XX:XX、XX:XX左右(平均查获量达XX件),需强化对应时段的安检力度。低峰时段:XX:XX、XX:XX后(平均查获量XX件)'
  303. }
  304. const trendData = seizureTimeTrendData.value
  305. // 1. 按查获量排序,找出最高和最低的两个时段
  306. const sortedData = [...trendData].sort((a, b) => (b.total || 0) - (a.total || 0))
  307. // 2. 获取最高的两个时段
  308. const peakHours = sortedData.slice(0, 2).map(item => item.hourOfDay || '').filter(Boolean)
  309. // 3. 获取最低的两个时段
  310. const lowHours = sortedData.slice(-2).map(item => item.hourOfDay || '').filter(Boolean)
  311. // 4. 计算高峰时段平均查获量
  312. const peakData = trendData.filter(item => peakHours.some(hour => item.hourOfDay?.includes(hour)))
  313. const peakAvg = peakData.length > 0
  314. ? Math.round(peakData.reduce((sum, item) => sum + (item.total || 0), 0) / peakData.length)
  315. : 0
  316. // 5. 计算低峰时段平均查获量
  317. const lowData = trendData.filter(item => lowHours.some(hour => item.hourOfDay?.includes(hour)))
  318. const lowAvg = lowData.length > 0
  319. ? Math.round(lowData.reduce((sum, item) => sum + (item.total || 0), 0) / lowData.length)
  320. : 0
  321. // 6. 生成描述文字
  322. return `高峰时段:${peakHours.join('、')}左右(平均查获量达${peakAvg}件),需强化对应时段的安检力度。低峰时段:${lowHours.join('、')}后(平均查获量${lowAvg}件)`
  323. })
  324. // 计算属性:动态生成隐匿物品查获部位描述
  325. const concealmentPositionStatsDescription = computed(() => {
  326. if (!concealmentPositionStatsData.value || !Array.isArray(concealmentPositionStatsData.value)) {
  327. return '违禁品主要藏匿于"XX",是安检搜检的重点部位。'
  328. }
  329. const positionData = concealmentPositionStatsData.value
  330. // 1. 按查获量排序,找出查获量最高的部位
  331. const sortedData = [...positionData].sort((a, b) => (b.count || 0) - (a.count || 0))
  332. // 2. 获取查获量最高的部位
  333. const topPosition = sortedData[0] || { positionName: 'XX', count: 0 }
  334. // 3. 生成描述文字
  335. return `违禁品主要藏匿于"${topPosition.positionName || 'XX'}",是安检搜检的重点部位。`
  336. })
  337. // 计算属性:动态生成大队查获排名描述
  338. const departmentRankingDescription = computed(() => {
  339. if (!departmentRankingData.value || !Array.isArray(departmentRankingData.value)) {
  340. return 'XXXX是违禁品查获的主力大队,查获违禁物品数量为XX,占比XX%。'
  341. }
  342. const rankingData = departmentRankingData.value
  343. // 1. 按查获量排序,找出查获量最高的大队
  344. const sortedData = [...rankingData].sort((a, b) => (b.seizureCount || 0) - (a.seizureCount || 0))
  345. // 2. 获取查获量最高的大队
  346. const topDepartment = sortedData[0] || { departmentName: 'XXXX', seizureCount: 234 }
  347. const str = isStationType.value ? '大队' : isBrigadeType.value ? '主管' : isDepartmentType.value ? '班组' : isTeamsType.value ? '成员' : ''
  348. // 5. 生成描述文字
  349. return `${topDepartment.brigadeName || 'XXXX'}是违禁品查获的主力${str},查获违禁物品数量为${topDepartment.seizureCount || 'XX'},占比${topDepartment.currentRatio}%。`
  350. })
  351. // 计算属性:动态生成查获岗位分布描述
  352. const postCategoryStatsDescription = computed(() => {
  353. if (!postCategoryStatsData.value || !Array.isArray(postCategoryStatsData.value)) {
  354. return '"XX"的查获数量最多,为XX件,占比XX%。'
  355. }
  356. const postData = postCategoryStatsData.value
  357. // 1. 按查获量排序,找出查获量最高的岗位
  358. const sortedData = [...postData].sort((a, b) => (b.quantity || 0) - (a.quantity || 0))
  359. // 2. 获取查获量最高的岗位
  360. const topPost = sortedData[0] || { postName: 'XX', quantity: 124 }
  361. // 3. 计算总查获量
  362. const totalCount = postData.reduce((sum, item) => sum + (item.quantity || 0), 0)
  363. // 4. 计算占比
  364. const percentage = totalCount > 0 ? ((topPost.quantity / totalCount) * 100).toFixed(0) : '45'
  365. // 5. 生成描述文字
  366. return `"${topPost.postName || 'XX'}"的查获数量最多,为${topPost.quantity || 124}件,占比${percentage}%。`
  367. })
  368. // 计算属性:动态生成查获通道TOP5描述
  369. const channelRankingStatsDescription = computed(() => {
  370. if (!channelRankingStatsData.value || !Array.isArray(channelRankingStatsData.value)) {
  371. return '违禁物品查获主要集中于XXXX,查获数量为XX,占比XX%。'
  372. }
  373. const channelData = channelRankingStatsData.value
  374. // 1. 按查获量排序,找出查获量最高的通道
  375. const sortedData = [...channelData].sort((a, b) => (b.seizureQuantity || 0) - (a.seizureQuantity || 0))
  376. // 2. 获取查获量最高的通道
  377. const topChannel = sortedData[0] || { channelName: 'XXXX', seizureQuantity: 123 }
  378. // 3. 计算总查获量
  379. const totalCount = channelData.reduce((sum, item) => sum + (item.seizureQuantity || 0), 0)
  380. // 4. 计算占比
  381. const percentage = totalCount > 0 ? ((topChannel.seizureQuantity / totalCount) * 100).toFixed(0) : '23'
  382. // 5. 生成描述文字
  383. return `违禁物品查获主要集中于${topChannel.channelName || 'XXXX'},查获数量为${topChannel.seizureQuantity || 'XX'},占比${percentage}%。`
  384. })
  385. // 表格数据
  386. const captureRankData = ref([])
  387. const captureChannelData = ref([])
  388. // 处理query参数,当dateRangeQueryType为YEAR时添加yearOnYear: true
  389. const processQueryParams = (queryParams) => {
  390. const processedParams = { ...queryParams }
  391. if (processedParams.dateRangeQueryType === 'YEAR') {
  392. processedParams.yearOnYear = true
  393. } else {
  394. processedParams.chainRatio = true
  395. processedParams.yearOnYear = true
  396. }
  397. const selectedDept = props.selectedDeptObject
  398. const { deptType = "", id } = selectedDept ? selectedDept : { deptType: "", id: "" }
  399. let otherParams = {
  400. ...(deptType == 'BRIGADE' ? { brigadeId: id } : {}),
  401. ...(deptType == 'MANAGER' ? { departmentId: id } : {}),
  402. ...(deptType == 'TEAMS' ? { teamId: id } : {}),
  403. ...(deptType == 'USER' ? { userId: id } : {})
  404. }
  405. delete processedParams.deptId;
  406. return { ...processedParams, ...otherParams }
  407. }
  408. const handleAbnormalCaptureStats = (data) => {
  409. const { higList, lowList } = data;
  410. let first = !!higList && higList.map((item) => item.departmentName).length > 0 ? `${higList.map((item) => item.departmentName).join("、")}查获违禁品数量显著高于整体水平` : '';
  411. let second = !!lowList && lowList.map((item) => item.departmentName).length > 0 ? `${lowList.map((item) => item.departmentName).join("、")}查获违禁品数量显著低于整体水平` : '';
  412. return `${first}${!!second ? ',' : first && second ? '。' : ''}${second}`
  413. }
  414. const handlePoliceTransferStats = (data) => {
  415. const { totalQuantity, brigadeRankList } = data;
  416. const topDepartment = brigadeRankList.map(item => item.brigadeName).join('、')
  417. return `移交公安的违禁品数量共${totalQuantity}件,主要集中于${topDepartment}。`
  418. }
  419. // 调用API获取风险隐患数据
  420. const fetchRiskHazardData = async (queryParams) => {
  421. try {
  422. // 处理query参数
  423. const processedParams = processQueryParams(queryParams)
  424. // 按顺序调用六个API接口
  425. // 1. 查获违禁品类别统计
  426. const categoryStatsResponse = await getCategoryStats(processedParams)
  427. console.log('查获违禁品类别统计:', categoryStatsResponse)
  428. categoryStatsData.value = categoryStatsResponse.data?.categoryStats || []
  429. // 2. 查获时间趋势
  430. const seizureTimeTrendResponse = await getSeizureTimeTrend(processedParams)
  431. console.log('查获时间趋势:', seizureTimeTrendResponse)
  432. seizureTimeTrendData.value = seizureTimeTrendResponse?.data || []
  433. // 3. 隐匿物品查获部位统计
  434. const concealmentPositionStatsResponse = await getConcealmentPositionStats(processedParams)
  435. console.log('隐匿物品查获部位统计:', concealmentPositionStatsResponse)
  436. concealmentPositionStatsData.value = concealmentPositionStatsResponse?.data?.positionStats || []
  437. // 4. 大队查获排名(表格数据)
  438. const departmentRankingResponse = await getDepartmentRanking(processedParams)
  439. console.log('大队查获排名:', departmentRankingResponse)
  440. departmentRankingData.value = departmentRankingResponse.data || []
  441. // 5. 查获岗位分布统计
  442. const postCategoryStatsResponse = await getPostCategoryStats(processedParams)
  443. console.log('查获岗位分布统计:', postCategoryStatsResponse)
  444. postCategoryStatsData.value = postCategoryStatsResponse?.data?.postStats || []
  445. // 6. 查获通道TOP5(表格数据)
  446. const channelRankingStatsResponse = await getChannelRankingStats(processedParams)
  447. console.log('查获通道TOP5:', channelRankingStatsResponse)
  448. channelRankingStatsData.value = channelRankingStatsResponse?.data?.channelRankings || []
  449. // 7. 移交公安数据
  450. const policeDataResponse = await getPoliceData(processedParams)
  451. console.log('移交公安数据:', policeDataResponse)
  452. policeTransferData.value = policeDataResponse?.data || []
  453. // 8. X光机漏检数据
  454. const xrayMissCheckResponse = await getXrayMissCheck(processedParams)
  455. console.log('X光机漏检数据:', xrayMissCheckResponse)
  456. xrayMissData.value = xrayMissCheckResponse?.data || []
  457. // 9. 可能异常查获数据(只有当部门类型是STATION时才请求)
  458. if (isStationType.value || isBrigadeType.value || isDepartmentType.value) {
  459. const abnormalSeizureResponse = await getAbnormalSeizureData(processedParams)
  460. console.log('可能异常查获数据:', abnormalSeizureResponse)
  461. abnormalCaptureData.value = abnormalSeizureResponse?.data || []
  462. // 12. 可能异常查获统计数据(描述卡片文本)
  463. const abnormalSeizureStatsResponse = await getAbnormalSeizureStats(processedParams)
  464. console.log('可能异常查获统计数据:', abnormalSeizureStatsResponse, isStationType.value)
  465. // debugger
  466. abnormalCaptureStats.value = handleAbnormalCaptureStats(abnormalSeizureStatsResponse?.data)
  467. } else {
  468. abnormalCaptureData.value = []
  469. abnormalCaptureStats.value = ''
  470. }
  471. // 10. 移交公安统计数据(描述卡片文本)
  472. const policeDataStatsResponse = await getPoliceDataStats(processedParams)
  473. console.log('移交公安统计数据:', policeDataStatsResponse)
  474. policeTransferStats.value = handlePoliceTransferStats(policeDataStatsResponse?.data)
  475. // 11. X光机漏检统计数据(描述卡片文本)
  476. const xrayMissCheckStatsResponse = await getXrayMissCheckStats(processedParams)
  477. console.log('X光机漏检统计数据:', xrayMissCheckStatsResponse)
  478. let userName = xrayMissCheckStatsResponse?.data?.map(item => item.xrayOperatorName).join('、') || ''
  479. xrayMissStats.value = `X光机漏检事件主要集中于以下开机员:${userName},可针对性开展判图技能强化培训。`
  480. // 更新图表和表格数据
  481. updateChartsWithData()
  482. } catch (error) {
  483. console.error('获取风险隐患数据失败:', error)
  484. }
  485. }
  486. // 查获违禁品类别饼图配置
  487. const captureRankOptions = {
  488. tooltip: {
  489. trigger: 'item',
  490. formatter: '{a} <br/>{b}: {c}件 ({d}%)'
  491. },
  492. legend: {
  493. orient: 'vertical',
  494. left: 'left',
  495. top: 'center',
  496. textStyle: {
  497. color: '#333',
  498. fontSize: 12
  499. }
  500. },
  501. color: ['#FF9F43', '#54C6EB', '#A3D9B1', '#FF6B9D', '#9B59B6'],
  502. series: [
  503. {
  504. name: '违禁品类别',
  505. type: 'pie',
  506. radius: '60%',
  507. center: ['60%', '50%'],
  508. avoidLabelOverlap: false,
  509. itemStyle: {
  510. borderRadius: 10,
  511. borderColor: '#fff',
  512. borderWidth: 2
  513. },
  514. label: {
  515. show: true,
  516. formatter: '{b}: {c}件',
  517. fontSize: 12
  518. },
  519. emphasis: {
  520. label: {
  521. show: true,
  522. fontSize: 14,
  523. fontWeight: 'bold'
  524. },
  525. itemStyle: {
  526. shadowBlur: 10,
  527. shadowOffsetX: 0,
  528. shadowColor: 'rgba(0, 0, 0, 0.5)'
  529. }
  530. },
  531. labelLine: {
  532. show: true,
  533. length: 10,
  534. length2: 20
  535. },
  536. data: []
  537. }
  538. ]
  539. }
  540. // 问题发现折线图配置
  541. const problemDiscoveryOptions = {
  542. tooltip: {
  543. trigger: 'axis',
  544. axisPointer: {
  545. type: 'shadow'
  546. },
  547. formatter: function (params) {
  548. return `${params[0].name}<br/>问题数量: <span style="color:#FF6B6B;font-weight:bold">${params[0].data}</span>`
  549. }
  550. },
  551. grid: {
  552. left: '3%',
  553. right: '4%',
  554. bottom: '3%',
  555. containLabel: true
  556. },
  557. xAxis: {
  558. type: 'category',
  559. data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月'],
  560. axisLine: {
  561. lineStyle: {
  562. color: '#999'
  563. }
  564. },
  565. axisLabel: {
  566. fontSize: 12
  567. }
  568. },
  569. yAxis: {
  570. type: 'value',
  571. name: '问题数量',
  572. axisLine: {
  573. lineStyle: {
  574. color: '#999'
  575. }
  576. },
  577. splitLine: {
  578. lineStyle: {
  579. color: '#f0f0f0'
  580. }
  581. }
  582. },
  583. series: [
  584. {
  585. name: '问题发现',
  586. type: 'line',
  587. smooth: true,
  588. symbol: 'circle',
  589. symbolSize: 6,
  590. itemStyle: {
  591. color: '#FF6B6B'
  592. },
  593. lineStyle: {
  594. color: '#FF6B6B',
  595. width: 3
  596. },
  597. label: {
  598. show: true,
  599. position: 'top',
  600. formatter: '{c}'
  601. },
  602. data: []
  603. }
  604. ]
  605. }
  606. // 隐匿物品查获部位饼图配置
  607. const problemRectificationOptions = {
  608. tooltip: {
  609. trigger: 'item',
  610. formatter: '{a} <br/>{b}: {c}件 ({d}%)'
  611. },
  612. legend: {
  613. orient: 'vertical',
  614. left: 'left',
  615. top: 'center',
  616. textStyle: {
  617. color: '#333',
  618. fontSize: 12
  619. }
  620. },
  621. color: ['#FF9F43', '#54C6EB', '#A3D9B1', '#FF6B9D', '#9B59B6'],
  622. series: [
  623. {
  624. name: '查获部位',
  625. type: 'pie',
  626. radius: '70%',
  627. center: ['50%', '50%'],
  628. avoidLabelOverlap: false,
  629. itemStyle: {
  630. borderRadius: 10,
  631. borderColor: '#fff',
  632. borderWidth: 2
  633. },
  634. label: {
  635. show: true,
  636. formatter: '{b}: {c}件',
  637. fontSize: 12
  638. },
  639. emphasis: {
  640. label: {
  641. show: true,
  642. fontSize: 14,
  643. fontWeight: 'bold'
  644. },
  645. itemStyle: {
  646. shadowBlur: 10,
  647. shadowOffsetX: 0,
  648. shadowColor: 'rgba(0, 0, 0, 0.5)'
  649. }
  650. },
  651. labelLine: {
  652. show: true,
  653. length: 10,
  654. length2: 20
  655. },
  656. data: []
  657. }
  658. ]
  659. }
  660. // 查获岗位分布饼图配置
  661. const rectificationStatsOptions = {
  662. tooltip: {
  663. trigger: 'item',
  664. formatter: '{a} <br/>{b}: {c}件 ({d}%)'
  665. },
  666. legend: {
  667. orient: 'vertical',
  668. left: 'left',
  669. top: 'center',
  670. textStyle: {
  671. color: '#333',
  672. fontSize: 12
  673. }
  674. },
  675. color: ['#FF9F43', '#54C6EB', '#A3D9B1', '#FF6B9D', '#9B59B6'],
  676. series: [
  677. {
  678. name: '岗位分布',
  679. type: 'pie',
  680. radius: '70%',
  681. center: ['60%', '50%'],
  682. avoidLabelOverlap: false,
  683. itemStyle: {
  684. borderRadius: 10,
  685. borderColor: '#fff',
  686. borderWidth: 2
  687. },
  688. label: {
  689. show: true,
  690. formatter: '{b}: {c}件',
  691. fontSize: 12
  692. },
  693. emphasis: {
  694. label: {
  695. show: true,
  696. fontSize: 14,
  697. fontWeight: 'bold'
  698. },
  699. itemStyle: {
  700. shadowBlur: 10,
  701. shadowOffsetX: 0,
  702. shadowColor: 'rgba(0, 0, 0, 0.5)'
  703. }
  704. },
  705. labelLine: {
  706. show: true,
  707. length: 10,
  708. length2: 20
  709. },
  710. data: []
  711. }
  712. ]
  713. }
  714. // 初始化图表
  715. onMounted(() => {
  716. // 确保DOM完全渲染后再初始化图表
  717. const initCharts = () => {
  718. // 查获违禁品类别饼图
  719. if (captureRankChartRef.value && captureRankChartRef.value.offsetHeight > 0) {
  720. setCaptureRankOption(captureRankOptions)
  721. }
  722. // 问题发现折线图
  723. if (problemDiscoveryChartRef.value && problemDiscoveryChartRef.value.offsetHeight > 0) {
  724. setProblemDiscoveryOption(problemDiscoveryOptions)
  725. }
  726. // 隐匿物品查获部位饼图
  727. if (problemRectificationChartRef.value && problemRectificationChartRef.value.offsetHeight > 0) {
  728. setProblemRectificationOption(problemRectificationOptions)
  729. }
  730. // 查获岗位分布饼图
  731. if (rectificationStatsChartRef.value && rectificationStatsChartRef.value.offsetHeight > 0) {
  732. setRectificationStatsOption(rectificationStatsOptions)
  733. }
  734. }
  735. // 立即尝试初始化
  736. initCharts()
  737. // 如果容器高度为0,延迟重试
  738. setTimeout(() => {
  739. initCharts()
  740. }, 200)
  741. })
  742. // 监听queryForm参数变化,调用API获取数据
  743. watch(() => props.queryForm, (newQueryForm) => {
  744. // 只有当所有必要的查询参数都存在时才调用API
  745. if (newQueryForm.dateRangeQueryType && newQueryForm.year) {
  746. fetchRiskHazardData(newQueryForm)
  747. }
  748. }, { deep: true })
  749. // 根据API数据更新图表和表格数据
  750. const updateChartsWithData = () => {
  751. // 1. 更新查获违禁品类别饼图
  752. updateCategoryStatsChart()
  753. // 2. 更新查获时间趋势折线图
  754. updateSeizureTimeTrendChart()
  755. // 3. 更新隐匿物品查获部位饼图
  756. updateConcealmentPositionStatsChart()
  757. // 4. 更新大队查获排名表格
  758. updateDepartmentRankingTable()
  759. // 5. 更新查获岗位分布饼图
  760. updatePostCategoryStatsChart()
  761. // 6. 更新查获通道TOP5表格
  762. updateChannelRankingStatsTable()
  763. console.log('风险隐患数据已更新,图表和表格已刷新')
  764. }
  765. // 更新查获违禁品类别饼图数据
  766. const updateCategoryStatsChart = () => {
  767. if (categoryStatsData.value && categoryStatsData.value) {
  768. const pieData = categoryStatsData.value
  769. // 转换数据格式
  770. const formattedData = pieData.map(item => ({
  771. name: item.categoryNameOne || '未知类别',
  772. value: item.quantity || 0
  773. }))
  774. // 更新饼图数据
  775. captureRankOptions.series[0].data = formattedData
  776. // 重新设置图表选项
  777. setCaptureRankOption(captureRankOptions)
  778. console.log('查获违禁品类别饼图已更新:', formattedData)
  779. } else {
  780. // 无数据时清空图表
  781. captureRankOptions.series[0].data = []
  782. setCaptureRankOption(captureRankOptions)
  783. }
  784. }
  785. // 更新查获时间趋势折线图数据
  786. const updateSeizureTimeTrendChart = () => {
  787. if (seizureTimeTrendData.value && seizureTimeTrendData.value) {
  788. const trendData = seizureTimeTrendData.value
  789. // 提取横坐标和时间数据
  790. const xAxisData = trendData.map(item => item.hourOfDay || '未知时间')
  791. const seriesData = trendData.map(item => item.total || 0)
  792. // 更新图表配置
  793. problemDiscoveryOptions.xAxis.data = xAxisData
  794. problemDiscoveryOptions.series[0].data = seriesData
  795. // 重新设置图表选项
  796. setProblemDiscoveryOption(problemDiscoveryOptions)
  797. console.log('查获时间趋势折线图已更新:', { xAxis: xAxisData, series: seriesData })
  798. } else {
  799. // 无数据时清空图表
  800. problemDiscoveryOptions.xAxis.data = []
  801. problemDiscoveryOptions.series[0].data = []
  802. setProblemDiscoveryOption(problemDiscoveryOptions)
  803. }
  804. }
  805. // 更新隐匿物品查获部位饼图数据
  806. const updateConcealmentPositionStatsChart = () => {
  807. if (concealmentPositionStatsData.value && concealmentPositionStatsData.value) {
  808. const pieData = concealmentPositionStatsData.value
  809. // 转换数据格式
  810. const formattedData = pieData.map(item => ({
  811. name: item.positionName || '未知部位',
  812. value: item.quantity || 0
  813. }))
  814. // 更新饼图数据
  815. problemRectificationOptions.series[0].data = formattedData
  816. // 重新设置图表选项
  817. setProblemRectificationOption(problemRectificationOptions)
  818. console.log('隐匿物品查获部位饼图已更新:', formattedData)
  819. } else {
  820. // 无数据时清空图表
  821. problemRectificationOptions.series[0].data = []
  822. setProblemRectificationOption(problemRectificationOptions)
  823. }
  824. }
  825. // 更新大队查获排名表格数据
  826. const updateDepartmentRankingTable = () => {
  827. if (departmentRankingData.value && departmentRankingData.value) {
  828. const tableData = departmentRankingData.value
  829. // 转换数据格式
  830. const formattedData = tableData.map((item, index) => ({
  831. rank: index + 1,
  832. department: item.brigadeName || '未知大队',
  833. percentage: item.currentRatio || 0,
  834. count: item.seizureCount || 0
  835. }))
  836. // 更新表格数据
  837. captureRankData.value = formattedData
  838. console.log('大队查获排名表格已更新:', formattedData)
  839. } else {
  840. // 无数据时清空表格
  841. captureRankData.value = []
  842. }
  843. }
  844. // 更新查获岗位分布饼图数据
  845. const updatePostCategoryStatsChart = () => {
  846. if (postCategoryStatsData.value && postCategoryStatsData.value) {
  847. const pieData = postCategoryStatsData.value
  848. // 转换数据格式
  849. const formattedData = pieData.map(item => ({
  850. name: item.postName || '未知岗位',
  851. value: item.quantity || 0
  852. }))
  853. // 更新饼图数据
  854. rectificationStatsOptions.series[0].data = formattedData
  855. // 重新设置图表选项
  856. setRectificationStatsOption(rectificationStatsOptions)
  857. console.log('查获岗位分布饼图已更新:', formattedData)
  858. } else {
  859. // 无数据时清空图表
  860. rectificationStatsOptions.series[0].data = []
  861. setRectificationStatsOption(rectificationStatsOptions)
  862. }
  863. }
  864. // 更新查获通道TOP5表格数据
  865. const updateChannelRankingStatsTable = () => {
  866. if (channelRankingStatsData.value && channelRankingStatsData.value) {
  867. const tableData = channelRankingStatsData.value
  868. // 转换数据格式
  869. const formattedData = tableData.map(item => ({
  870. channel: item.channelName || '未知通道',
  871. count: item.seizureQuantity || 0,
  872. area: item.regionalName || '未知区域'
  873. }))
  874. // 更新表格数据
  875. captureChannelData.value = formattedData
  876. console.log('查获通道TOP5表格已更新:', formattedData)
  877. } else {
  878. // 无数据时清空表格
  879. captureChannelData.value = []
  880. }
  881. }
  882. // 组件卸载时销毁图表
  883. onUnmounted(() => {
  884. disposeCaptureRank()
  885. disposeProblemDiscovery()
  886. disposeProblemRectification()
  887. disposeRectificationStats()
  888. })
  889. </script>
  890. <style scoped>
  891. .risk-hazard {
  892. width: 100%;
  893. }
  894. /* 风险隐患标题 */
  895. .section-title {
  896. margin: 14px 0 14px 0;
  897. text-align: left;
  898. }
  899. .section-title h2 {
  900. margin: 0;
  901. font-size: 18px;
  902. font-weight: 600;
  903. color: #333;
  904. }
  905. /* 六个部分布局 */
  906. .six-panel-layout {
  907. display: grid;
  908. grid-template-columns: repeat(3, 1fr);
  909. gap: 16px;
  910. grid-auto-flow: dense;
  911. /* 自动填充空白区域 */
  912. }
  913. .panel-row {
  914. display: contents;
  915. /* 让子元素直接参与网格布局 */
  916. }
  917. .panel-item {
  918. background: white;
  919. border-radius: 8px;
  920. padding: 20px;
  921. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  922. display: flex;
  923. flex-direction: column;
  924. gap: 16px;
  925. min-width: 0;
  926. /* 防止内容溢出 */
  927. overflow: hidden;
  928. /* 隐藏溢出内容 */
  929. }
  930. .panel-header {
  931. padding-bottom: 0;
  932. }
  933. .panel-header h3 {
  934. font-size: 16px;
  935. font-weight: 600;
  936. color: #333;
  937. margin: 0;
  938. }
  939. /* 描述卡片样式 */
  940. .describe-card {
  941. background: #F8F8F8;
  942. border: 1px dashed #CDCCCC;
  943. border-radius: 6px;
  944. padding: 7px 10px;
  945. text-align: left;
  946. }
  947. .describe-content {
  948. font-size: 13px;
  949. color: #666;
  950. line-height: 1.5;
  951. }
  952. .stat-number {
  953. font-weight: 600;
  954. color: #557DDB;
  955. font-size: 16px;
  956. }
  957. .stat-trend {
  958. font-weight: 500;
  959. }
  960. .stat-trend.up {
  961. color: #67C23A;
  962. }
  963. .stat-trend.down {
  964. color: #F56C6C;
  965. }
  966. /* 图表容器 */
  967. .chart-container {
  968. /* background: #F8F8F8;
  969. border-radius: 6px;
  970. padding: 16px; */
  971. min-height: 200px;
  972. height: 250px;
  973. position: relative;
  974. box-sizing: border-box;
  975. overflow: hidden;
  976. display: flex;
  977. align-items: center;
  978. justify-content: center;
  979. flex-shrink: 0;
  980. /* 防止被压缩 */
  981. }
  982. /* 表格容器 */
  983. .table-container {
  984. /* background: #F8F8F8; */
  985. flex: 1;
  986. border: 1px solid #E4E7ED;
  987. border-radius: 4px;
  988. max-height: 300px;
  989. overflow: auto;
  990. position: relative;
  991. }
  992. /* Element Plus表格横向滚动配置 */
  993. .table-container :deep(.el-table) {
  994. width: 100%;
  995. min-width: 100%;
  996. }
  997. .table-container :deep(.el-table .el-table__header-wrapper),
  998. .table-container :deep(.el-table .el-table__body-wrapper) {
  999. overflow-x: auto !important;
  1000. }
  1001. .table-container :deep(.el-table__header) {
  1002. width: auto !important;
  1003. }
  1004. .table-container :deep(.el-table__body) {
  1005. width: auto !important;
  1006. }
  1007. /* 表格偶数行背景色 */
  1008. .table-container :deep(.el-table__body tr:nth-child(even)) {
  1009. background-color: #F8F8F8;
  1010. }
  1011. /* 表格样式 - 列头和列内容 */
  1012. .table-container :deep(.el-table__header th),
  1013. .table-container :deep(.el-table__body td) {
  1014. font-weight: 500;
  1015. font-size: 13px;
  1016. color: #333333;
  1017. }
  1018. .percentage {
  1019. color: #557DDB;
  1020. font-weight: 600;
  1021. }
  1022. /* ECharts图表样式 */
  1023. .echarts-chart {
  1024. width: 100% !important;
  1025. height: 100% !important;
  1026. min-height: 200px;
  1027. display: block;
  1028. }
  1029. /* 响应式设计 */
  1030. @media (max-width: 768px) {
  1031. .panel-row {
  1032. flex-direction: column;
  1033. }
  1034. }
  1035. </style>