ModuleTwo.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. <template>
  2. <module-container title="模块二 查堵人员分析">
  3. <div class="module-two-content">
  4. <div class="top-row">
  5. <div class="table-card">
  6. <div class="chart-title">查堵-主管排行榜</div>
  7. <rank-list :rank-data="rankData1" title="查堵-主管排行榜" header-label="排名" header-name="分管主管" header-count="计数" />
  8. </div>
  9. <div class="table-card">
  10. <div class="chart-title">查堵-班组排行榜</div>
  11. <rank-list :rank-data="rankData2" title="查堵-班组排行榜" header-label="排名" header-name="分管班组长" header-count="计数" />
  12. </div>
  13. <div class="table-card">
  14. <div class="chart-title">查堵-人员排行榜</div>
  15. <rank-list :rank-data="rankData3" title="查堵-人员排行榜" header-label="排名" header-name="被回查人" header-count="计数" />
  16. </div>
  17. </div>
  18. <div class="middle-row">
  19. <div class="chart-card">
  20. <div class="chart-title">查堵-物品位置分布</div>
  21. <div ref="chart1" class="echarts"></div>
  22. </div>
  23. <div class="chart-card">
  24. <div class="chart-title">查堵-原因分类</div>
  25. <div ref="chart2" class="echarts"></div>
  26. </div>
  27. </div>
  28. <div class="bottom-row">
  29. <div class="chart-card">
  30. <div class="chart-title">查堵-开机人员年限分布</div>
  31. <div ref="chart3" class="echarts"></div>
  32. </div>
  33. <div class="chart-card">
  34. <div class="chart-title">查堵-开机人员性别比例</div>
  35. <div ref="chart4" class="echarts"></div>
  36. </div>
  37. <div class="chart-card">
  38. <div class="chart-title">查堵-图像难易程度比例</div>
  39. <div ref="chart5" class="echarts"></div>
  40. </div>
  41. </div>
  42. <div class="extra-row">
  43. <div class="chart-card">
  44. <div class="chart-title">大队开机人员年限分布</div>
  45. <div ref="chart6" class="echarts"></div>
  46. </div>
  47. <div class="chart-card">
  48. <div class="chart-title">大队开机人员证书分布</div>
  49. <div ref="chart7" class="echarts"></div>
  50. </div>
  51. </div>
  52. </div>
  53. </module-container>
  54. </template>
  55. <script setup>
  56. import { ref, onMounted, reactive, watch } from 'vue'
  57. import * as echarts from 'echarts'
  58. import ModuleContainer from './ModuleContainer.vue'
  59. import RankList from './RankList.vue'
  60. import { useEcharts } from '@/hooks/chart.js'
  61. import {
  62. supervisorRanking,
  63. teamLeaderRanking,
  64. reviewedUserRanking,
  65. itemLocationDistribution,
  66. missCheckReasonDistribution,
  67. brigadeOperatingYearsDistribution,
  68. brigadeGenderDistribution,
  69. brigadeDifficultyDistribution,
  70. brigadeCertificateDistribution,
  71. tenureListAll
  72. } from '@/api/blockingData/blockingDataScreen.js'
  73. import { formatDateHy } from '@/utils/index.js'
  74. // 接收筛选参数
  75. const props = defineProps({
  76. filterParams: {
  77. type: Object,
  78. default: () => ({})
  79. }
  80. })
  81. const chart1 = ref(null)
  82. const chart2 = ref(null)
  83. const chart3 = ref(null)
  84. const chart4 = ref(null)
  85. const chart5 = ref(null)
  86. const chart6 = ref(null)
  87. const chart7 = ref(null)
  88. const { setOption: setOption1 } = useEcharts(chart1)
  89. const { setOption: setOption2 } = useEcharts(chart2)
  90. const { setOption: setOption3 } = useEcharts(chart3)
  91. const { setOption: setOption4 } = useEcharts(chart4)
  92. const { setOption: setOption5 } = useEcharts(chart5)
  93. const { setOption: setOption6 } = useEcharts(chart6)
  94. const { setOption: setOption7 } = useEcharts(chart7)
  95. // 处理filterParams,将dateRange拆分为startTime和endTime
  96. const processFilterParams = (params) => {
  97. const { dateRange, ...rest } = params
  98. const processed = { ...rest }
  99. if (dateRange && Array.isArray(dateRange) && dateRange.length === 2) {
  100. processed.startTime = formatDateHy(dateRange[0])
  101. processed.endTime = formatDateHy(dateRange[1])
  102. }
  103. if (processed.brigadeId == 'all') {
  104. delete processed.brigadeId
  105. }
  106. if (processed.terminalId == 'all') {
  107. delete processed.terminalId
  108. }
  109. return processed
  110. }
  111. // 数据加载函数
  112. const loadData = async () => {
  113. try {
  114. const queryParams = processFilterParams(props.filterParams)
  115. // 排行榜数据
  116. const [supervisorRankRes, teamLeaderRankRes, reviewedUserRankRes] = await Promise.allSettled([
  117. supervisorRanking(queryParams),
  118. teamLeaderRanking(queryParams),
  119. reviewedUserRanking(queryParams)
  120. ])
  121. // 更新排行榜数据
  122. if (supervisorRankRes?.value?.data) {
  123. rankData1.splice(0, rankData1.length, ...supervisorRankRes.value.data.map((item, index) => ({
  124. name: item.name || '未知',
  125. value: item.totalCount || 0
  126. })))
  127. }
  128. if (teamLeaderRankRes?.value?.data) {
  129. rankData2.splice(0, rankData2.length, ...teamLeaderRankRes.value.data.map((item, index) => ({
  130. name: item.name || '未知',
  131. value: item.totalCount || 0
  132. })))
  133. }
  134. if (reviewedUserRankRes?.value?.data) {
  135. rankData3.splice(0, rankData3.length, ...reviewedUserRankRes.value.data.map((item, index) => ({
  136. name: item.name || '未知',
  137. value: item.totalCount || 0
  138. })))
  139. }
  140. // 图表数据
  141. const [itemLocationRes, missCheckReasonRes, operatingYearsRes, genderRes, difficultyRes, certificateRes, tenureListRes] = await Promise.allSettled([
  142. itemLocationDistribution(queryParams),
  143. missCheckReasonDistribution(queryParams),
  144. brigadeOperatingYearsDistribution(queryParams),
  145. brigadeGenderDistribution(queryParams),
  146. brigadeDifficultyDistribution(queryParams),
  147. brigadeCertificateDistribution(queryParams),
  148. tenureListAll(queryParams)
  149. ])
  150. // 设置图表数据
  151. setOption1(pieOption(
  152. itemLocationRes?.value?.data?.map(item => ({
  153. name: item.itemLocation || '未知',
  154. value: item.count || 0
  155. })) || [],
  156. ['#3b82f6', '#22c55e', '#f97316', '#ec4899', '#8b5cf6', '#14b8a6', '#64748b']
  157. ))
  158. setOption2(pieOption(
  159. missCheckReasonRes?.value?.data?.map(item => ({
  160. name: item.missCheckReasonCategory || item.reason || '未知',
  161. value: item.value || item.count || 0
  162. })) || [],
  163. ['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6', '#64748b'],
  164. true
  165. ))
  166. setOption3(barOption(
  167. operatingYearsRes?.value?.data?.map(item => ({
  168. name: item.operatingYears || '未知',
  169. value: item.count || 0
  170. })) || [],
  171. ['#3b82f6', '#60a5fa', '#93c5fd', '#bfdbfe', '#dbeafe', '#eff6ff']
  172. ))
  173. setOption4(pieOption(
  174. genderRes?.value?.data?.map(item => ({
  175. name: item.gender || '未知',
  176. value: item.count || 0
  177. })) || [],
  178. ['#3b82f6', '#ec4899']
  179. ))
  180. setOption5(pieOption(
  181. difficultyRes?.value?.data?.map(item => ({
  182. name: item.difficultyLevel || '未知',
  183. value: item.count || 0
  184. })) || [],
  185. ['#93c5fd', '#3b82f6', '#64748b', '#e2e8f0']
  186. ))
  187. // 大队开机人员年限分布
  188. const tenureListData = tenureListRes?.value?.data || []
  189. if (tenureListData.length > 0) {
  190. const categories = [...new Set(tenureListData.map(item => item.brigadeName))]
  191. const tenureLevels = [
  192. { name: '1年', key: 'cnt1Years' },
  193. { name: '2年', key: 'cnt2Years' },
  194. { name: '3年', key: 'cnt3Years' },
  195. { name: '4年', key: 'cnt4Years' },
  196. { name: '5年', key: 'cntGe5Year' }
  197. ]
  198. const seriesData = tenureLevels.map(level => ({
  199. name: level.name,
  200. type: 'bar',
  201. data: categories.map(brigade => {
  202. const item = tenureListData.find(d => d.brigadeName === brigade)
  203. return item ? item[level.key] : 0
  204. }),
  205. itemStyle: {
  206. color: ['#3b82f6', '#22c55e', '#f97316', '#ec4899', '#8b5cf6'][tenureLevels.indexOf(level) % 5]
  207. },
  208. barWidth: 15,
  209. label: { show: true, position: 'top', fontSize: 9 }
  210. }))
  211. setOption6({
  212. grid: { left: '15%', top: '10%', right: '5%', bottom: '15%', containLabel: true },
  213. xAxis: {
  214. type: 'category',
  215. data: categories,
  216. axisLabel: { fontSize: 9 },
  217. axisTick: {
  218. alignWithLabel: true
  219. }
  220. },
  221. yAxis: { type: 'value', axisLabel: { fontSize: 9 } },
  222. legend: { top: 0, right: 10, textStyle: { fontSize: 9 } },
  223. series: seriesData
  224. })
  225. }
  226. // 大队开机人员证书分布
  227. const certificateData = certificateRes?.value?.data || []
  228. if (certificateData.length > 0) {
  229. const certificateCategories = ['高级证书', '中级证书', '初级证书']
  230. const brigadeNames = [...new Set(certificateData.map(item => item.brigadeName))]
  231. const certificateSeriesData = brigadeNames.map((brigadeName, index) => {
  232. const brigadeData = certificateData.find(d => d.brigadeName === brigadeName)
  233. return {
  234. name: brigadeName,
  235. type: 'bar',
  236. data: [
  237. brigadeData?.seniorCount || 0,
  238. brigadeData?.middleCount || 0,
  239. brigadeData?.juniorCount || 0
  240. ],
  241. itemStyle: {
  242. color: ['#3b82f6', '#22c55e', '#f97316', '#ec4899', '#8b5cf6', '#14b8a6'][index % 6]
  243. },
  244. barWidth: 20,
  245. label: { show: true, position: 'top', fontSize: 9 }
  246. }
  247. })
  248. setOption7({
  249. grid: { left: '15%', top: '10%', right: '5%', bottom: '15%', containLabel: true },
  250. xAxis: {
  251. type: 'category',
  252. data: certificateCategories,
  253. axisLabel: { fontSize: 10 },
  254. axisLine: { lineStyle: { color: '#999' } }
  255. },
  256. yAxis: {
  257. type: 'value',
  258. axisLabel: { fontSize: 10 },
  259. axisLine: { lineStyle: { color: '#999' } },
  260. splitLine: { lineStyle: { color: '#eee' } }
  261. },
  262. legend: {
  263. top: 0,
  264. right: 10,
  265. textStyle: { fontSize: 10 }
  266. },
  267. series: certificateSeriesData
  268. })
  269. }
  270. } catch (error) {
  271. console.error('加载数据失败:', error)
  272. }
  273. }
  274. // 监听筛选参数变化
  275. watch(() => props.filterParams, () => {
  276. loadData()
  277. }, { deep: true })
  278. const rankData1 = reactive([
  279. ])
  280. const rankData2 = reactive([
  281. ])
  282. const rankData3 = reactive([
  283. ])
  284. const pieOption = (data, colors, isRing = false) => ({
  285. color: colors,
  286. legend: {
  287. orient: 'horizontal',
  288. top: 'top',
  289. textStyle: { fontSize: 10 }
  290. },
  291. series: [{
  292. type: 'pie',
  293. radius: isRing ? ['40%', '50%'] : '60%',
  294. center: ['50%', '55%'],
  295. data: data,
  296. label: {
  297. show: true,
  298. formatter: '{b}\n{c} ({d}%)',
  299. fontSize: 10
  300. },
  301. }]
  302. })
  303. const barOption = (data, colors) => ({
  304. grid: { left: '15%', top: '10%', right: '5%', bottom: '10%', containLabel: true },
  305. xAxis: { type: 'value', axisLabel: { fontSize: 9 } },
  306. yAxis: { type: 'category', data: data.map(d => d.name), axisLabel: { fontSize: 9 } },
  307. series: [{
  308. type: 'bar',
  309. data: data.map(d => d.value),
  310. itemStyle: {
  311. color: (params) => colors[params.dataIndex % colors.length]
  312. },
  313. barWidth: 15,
  314. label: { show: true, position: 'right', fontSize: 10 }
  315. }]
  316. })
  317. onMounted(() => {
  318. loadData()
  319. })
  320. </script>
  321. <style lang="less" scoped>
  322. .module-two-content {
  323. height: 100%;
  324. display: flex;
  325. flex-direction: column;
  326. gap: 8px;
  327. overflow-y: auto;
  328. }
  329. .top-row,
  330. .middle-row,
  331. .bottom-row,
  332. .extra-row {
  333. display: flex;
  334. gap: 8px;
  335. flex-shrink: 0;
  336. }
  337. .table-card {
  338. flex: 1;
  339. background: #fff;
  340. border-radius: 6px;
  341. padding: 8px;
  342. border: 1px solid #eee;
  343. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  344. display: flex;
  345. flex-direction: column;
  346. min-height: 200px;
  347. }
  348. .chart-card {
  349. flex: 1;
  350. background: #fff;
  351. border-radius: 6px;
  352. padding: 15px;
  353. border: 1px solid #eee;
  354. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  355. display: flex;
  356. flex-direction: column;
  357. min-height: 400px;
  358. }
  359. .chart-title {
  360. font-size: 17px;
  361. color: black;
  362. margin-bottom: 5px;
  363. text-align: left;
  364. }
  365. .rank-table {
  366. flex: 1;
  367. overflow-y: auto;
  368. }
  369. .rank-row {
  370. display: flex;
  371. align-items: center;
  372. padding: 4px 6px;
  373. border-bottom: 1px solid #f0f0f0;
  374. font-size: 11px;
  375. }
  376. .rank-row:last-child {
  377. border-bottom: none;
  378. }
  379. .rank-number {
  380. width: 45px;
  381. font-weight: bold;
  382. margin-right: 8px;
  383. }
  384. .rank-1 {
  385. color: #eab308;
  386. }
  387. .rank-2 {
  388. color: #94a3b8;
  389. }
  390. .rank-3 {
  391. color: #f97316;
  392. }
  393. .rank-name {
  394. flex: 1;
  395. color: #333;
  396. }
  397. .rank-value {
  398. color: #666;
  399. }
  400. .echarts {
  401. flex: 1;
  402. width: 100%;
  403. min-height: 250px;
  404. }
  405. </style>