ModuleBrigadeTwo.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. <template>
  2. <module-container title="模块二 查堵人员分析">
  3. <div class="module-brigade-content">
  4. <div class="chart-row">
  5. <div class="chart-item">
  6. <div class="chart-title">查堵-主管排行榜</div>
  7. <rank-list :rank-data="rankData1" title="查堵-主管排行榜" header-label="排名" header-name="分管主管" header-count="计数" />
  8. </div>
  9. <div class="chart-item">
  10. <div class="chart-title">查堵-班组长排行榜</div>
  11. <rank-list :rank-data="rankData2" title="查堵-班组长排行榜" header-label="排名" header-name="分管班组长" header-count="计数" />
  12. </div>
  13. <div class="chart-item">
  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="chart-row">
  19. <div class="chart-item">
  20. <div class="chart-title">查堵男女比例</div>
  21. <div ref="chart4" class="echarts"></div>
  22. </div>
  23. <div class="chart-item">
  24. <div class="chart-title">查堵人员证书级别分布</div>
  25. <div ref="chart5" class="echarts"></div>
  26. </div>
  27. <div class="chart-item">
  28. <div class="chart-title">证书级别人员基数</div>
  29. <div ref="chart6" class="echarts"></div>
  30. </div>
  31. </div>
  32. <div class="chart-row">
  33. <div class="chart-item">
  34. <div class="chart-title">查堵物品位置</div>
  35. <div ref="chart7" class="echarts"></div>
  36. </div>
  37. <div class="chart-item">
  38. <div class="chart-title">查堵-困难图像数</div>
  39. <div class="number-display">
  40. <div class="number-value">{{ difficultValue || 0 }}</div>
  41. <div class="number-unit">幅</div>
  42. </div>
  43. </div>
  44. <div class="chart-item">
  45. <div class="chart-title">查堵-简单图像数</div>
  46. <div class="number-display">
  47. <div class="number-value">{{ simpleValue || 0 }}</div>
  48. <div class="number-unit">幅</div>
  49. </div>
  50. </div>
  51. </div>
  52. <div class="chart-row">
  53. <div class="chart-item">
  54. <div class="chart-title">查堵-主管分管次数</div>
  55. <div ref="chart10" class="echarts"></div>
  56. </div>
  57. <div class="chart-item">
  58. <div class="chart-title">查堵原因分类</div>
  59. <div ref="chart11" class="echarts"></div>
  60. </div>
  61. </div>
  62. <div class="chart-row">
  63. <div class="chart-item">
  64. <div class="chart-title">查堵人员开机年限分布</div>
  65. <div ref="chart12" class="echarts"></div>
  66. </div>
  67. <div class="chart-item">
  68. <div class="chart-title">大队开机年限人员分布</div>
  69. <div ref="chart13" class="echarts"></div>
  70. </div>
  71. </div>
  72. </div>
  73. </module-container>
  74. </template>
  75. <script setup>
  76. import { ref, onMounted, reactive, watch } from 'vue'
  77. import * as echarts from 'echarts'
  78. import ModuleContainer from './ModuleContainer.vue'
  79. import RankList from './RankList.vue'
  80. import { useEcharts } from '@/hooks/chart.js'
  81. import { formatDateHy } from '@/utils/index.js'
  82. import {
  83. brigadeSupervisorRanking,
  84. brigadeTeamLeaderRanking,
  85. brigadeReviewedUserRanking,
  86. brigadeGenderDistribution,
  87. brigadeCertificateDistributionDetail,
  88. personnelCertificateBase,
  89. brigadeItemLocationDistribution,
  90. brigadeDifficultyDistribution,
  91. brigadeSupervisorDistribution,
  92. brigadeMissCheckReasonDistribution,
  93. brigadeOperatingYearsDistribution,
  94. tenureListAll
  95. } from '@/api/blockingData/blockingDataScreen'
  96. const props = defineProps({
  97. filterParams: {
  98. type: Object,
  99. default: () => ({})
  100. }
  101. })
  102. const loading = ref(false)
  103. const rankData1 = reactive([])
  104. const rankData2 = reactive([])
  105. const rankData3 = reactive([])
  106. const difficultValue = ref(0)
  107. const simpleValue = ref(0)
  108. const processFilterParams = (params) => {
  109. const { dateRange, ...rest } = params
  110. const processed = { ...rest }
  111. if (dateRange && Array.isArray(dateRange) && dateRange.length === 2) {
  112. processed.startTime = formatDateHy(dateRange[0])
  113. processed.endTime = formatDateHy(dateRange[1])
  114. }
  115. if (processed.brigadeId == 'all') {
  116. delete processed.brigadeId
  117. }
  118. if (processed.terminalId == 'all') {
  119. delete processed.terminalId
  120. }
  121. return processed
  122. }
  123. const chart4 = ref(null)
  124. const chart5 = ref(null)
  125. const chart6 = ref(null)
  126. const chart7 = ref(null)
  127. const chart10 = ref(null)
  128. const chart11 = ref(null)
  129. const chart12 = ref(null)
  130. const chart13 = ref(null)
  131. const { setOption: setOption4 } = useEcharts(chart4)
  132. const { setOption: setOption5 } = useEcharts(chart5)
  133. const { setOption: setOption6 } = useEcharts(chart6)
  134. const { setOption: setOption7 } = useEcharts(chart7)
  135. const { setOption: setOption10 } = useEcharts(chart10)
  136. const { setOption: setOption11 } = useEcharts(chart11)
  137. const { setOption: setOption12 } = useEcharts(chart12)
  138. const { setOption: setOption13 } = useEcharts(chart13)
  139. const fetchData = async () => {
  140. loading.value = true
  141. try {
  142. const processedParams = processFilterParams(props.filterParams)
  143. const [supervisorRes, teamRes, userRes, genderRes, certDetailRes, certBaseRes, locationRes, difficultyRes, supervisorDistRes, reasonRes, yearsRes, tenureRes] = await Promise.allSettled([
  144. brigadeSupervisorRanking(processedParams),
  145. brigadeTeamLeaderRanking(processedParams),
  146. brigadeReviewedUserRanking(processedParams),
  147. brigadeGenderDistribution(processedParams),
  148. brigadeCertificateDistributionDetail(processedParams),
  149. personnelCertificateBase(processedParams),
  150. brigadeItemLocationDistribution(processedParams),
  151. brigadeDifficultyDistribution(processedParams),
  152. brigadeSupervisorDistribution(processedParams),
  153. brigadeMissCheckReasonDistribution(processedParams),
  154. brigadeOperatingYearsDistribution(processedParams),
  155. tenureListAll(processedParams)
  156. ])
  157. if (supervisorRes.value?.data) {
  158. rankData1.length = 0
  159. supervisorRes.value.data.slice(0, 10).forEach(item => {
  160. rankData1.push({
  161. name: item.supervisorName || item.name || '',
  162. value: item.count || item.value || 0
  163. })
  164. })
  165. }
  166. if (teamRes.value?.data) {
  167. rankData2.length = 0
  168. teamRes.value.data.slice(0, 10).forEach(item => {
  169. rankData2.push({
  170. name: item.teamLeaderName || item.name || '',
  171. value: item.count || item.value || 0
  172. })
  173. })
  174. }
  175. if (userRes.value?.data) {
  176. rankData3.length = 0
  177. userRes.value.data.slice(0, 10).forEach(item => {
  178. rankData3.push({
  179. name: item.userName || item.name || '',
  180. value: item.count || item.value || 0
  181. })
  182. })
  183. }
  184. if (genderRes.value?.data) {
  185. const genderData = genderRes.value.data.map(item => ({
  186. name: item.gender || item.name || '',
  187. value: item.percentage || item.value || 0
  188. }))
  189. setOption4(pieChartOption(genderData, ['#ec4899', '#3b82f6']))
  190. }
  191. if (certDetailRes.value?.data) {
  192. const certData = certDetailRes.value.data.map(item => ({
  193. name: item.certificateLevel || item.name || '',
  194. value: item.percentage || item.value || 0
  195. }))
  196. setOption5(pieChartOption(certData, ['#22c55e', '#3b82f6']))
  197. }
  198. if (certBaseRes.value?.data) {
  199. const baseData = certBaseRes.value.data.map(item => ({
  200. name: item.certificateLevel || item.name || '',
  201. value: item.percentage || item.value || 0
  202. }))
  203. setOption6(pieChartOption(baseData, ['#22c55e', '#3b82f6']))
  204. }
  205. if (locationRes.value?.data) {
  206. const colors = ['#8b5cf6', '#ec4899', '#f97316', '#22c55e', '#3b82f6', '#14b8a6', '#ef4444', '#fbbf24', '#6366f1', '#06b6d4', '#84cc16', '#e11d48']
  207. const locationData = locationRes.value.data.map(item => ({
  208. name: item.itemLocation || item.name || '',
  209. value: item.percentage || item.value || 0
  210. }))
  211. setOption7(donutChartOption(locationData, colors))
  212. }
  213. if (difficultyRes.value?.data) {
  214. let difficult = 0
  215. let simple = 0
  216. difficultyRes.value.data.forEach(item => {
  217. if (item.difficultyLevel === '难') {
  218. difficult = item.count || item.value || 0
  219. } else if (item.difficultyLevel === '简单') {
  220. simple = item.count || item.value || 0
  221. }
  222. })
  223. difficultValue.value = difficult
  224. simpleValue.value = simple
  225. }
  226. if (supervisorDistRes.value?.data) {
  227. const colors = ['#3b82f6', '#22c55e', '#f97316', '#ec4899', '#8b5cf6', '#14b8a6', '#ef4444', '#fbbf24', '#6366f1', '#06b6d4', '#84cc16', '#e11d48', '#a855f7', '#f59e0b']
  228. const supervisorData = supervisorDistRes.value.data.map(item => ({
  229. name: item.supervisorName || item.name || '',
  230. value: item.percentage || item.value || 0
  231. }))
  232. setOption10(donutChartOption(supervisorData, colors))
  233. }
  234. if (reasonRes.value?.data) {
  235. const colors = ['#3b82f6', '#22c55e', '#f97316', '#ec4899', '#8b5cf6', '#14b8a6', '#ef4444', '#fbbf24']
  236. const reasonData = reasonRes.value.data.map(item => ({
  237. name: item.missCheckReasonCategory || '',
  238. value: item.count || 0
  239. }))
  240. setOption11(pieChartOption(reasonData, colors))
  241. }
  242. if (yearsRes.value?.data) {
  243. const colors = ['#3b82f6', '#22c55e', '#f97316', '#ec4899', '#8b5cf6', '#14b8a6']
  244. const yearsData = yearsRes.value.data.map(item => ({
  245. name: item.operatingYears || '',
  246. value: item.count || 0
  247. }))
  248. setOption12(pieChartOption(yearsData, colors))
  249. }
  250. if (tenureRes.value?.data) {
  251. const xAxisData = tenureRes.value.data.map(item => item.brigadeName || '')
  252. const seriesData = [
  253. {
  254. name: '5年及以上',
  255. data: tenureRes.value.data.map(item => item.cntGe5Year || 0),
  256. color: '#3b82f6'
  257. },
  258. {
  259. name: '4年',
  260. data: tenureRes.value.data.map(item => item.cnt4Years || 0),
  261. color: '#22c55e'
  262. },
  263. {
  264. name: '3年',
  265. data: tenureRes.value.data.map(item => item.cnt3Years || 0),
  266. color: '#f97316'
  267. },
  268. {
  269. name: '2年',
  270. data: tenureRes.value.data.map(item => item.cnt2Years || 0),
  271. color: '#ec4899'
  272. },
  273. {
  274. name: '1年',
  275. data: tenureRes.value.data.map(item => item.cnt1Years || 0),
  276. color: '#8b5cf6'
  277. },
  278. {
  279. name: '不足1年',
  280. data: tenureRes.value.data.map(item => item.cntLt1Year || 0),
  281. color: '#14b8a6'
  282. }
  283. ]
  284. setOption13(multiSeriesBarChartOption(xAxisData, seriesData))
  285. }
  286. } catch (error) {
  287. console.error('获取数据失败:', error)
  288. } finally {
  289. loading.value = false
  290. }
  291. }
  292. watch(() => props.filterParams, () => {
  293. fetchData()
  294. }, { deep: true })
  295. const pieChartOption = (data, colors) => ({
  296. color: colors,
  297. tooltip: { trigger: 'item', formatter: '{b}: {c}' },
  298. legend: {
  299. data: data.map(item => item.name),
  300. top: '0%',
  301. textStyle: { fontSize: 10 }
  302. },
  303. series: [{
  304. type: 'pie',
  305. radius: ['40%', '65%'],
  306. center: ['50%', '55%'],
  307. data: data,
  308. label: {
  309. show: true,
  310. formatter: (params) => {
  311. const total = data.reduce((sum, item) => sum + item.value, 0)
  312. const percentage = ((params.value / total) * 100).toFixed(2)
  313. return `${params.name}\n${params.value}(${percentage}%)`
  314. },
  315. fontSize: 10
  316. }
  317. }]
  318. })
  319. const donutChartOption = (data, colors) => ({
  320. color: colors,
  321. tooltip: { trigger: 'item', formatter: '{b}: {c}' },
  322. legend: {
  323. data: data.map(item => item.name),
  324. top: '0%',
  325. textStyle: { fontSize: 10 }
  326. },
  327. series: [{
  328. type: 'pie',
  329. radius: ['30%', '55%'],
  330. center: ['50%', '55%'],
  331. data: data,
  332. label: {
  333. show: true,
  334. position: 'outside',
  335. formatter: (params) => {
  336. const total = data.reduce((sum, item) => sum + item.value, 0)
  337. const percentage = ((params.value / total) * 100).toFixed(2)
  338. return `${params.name} ${params.value}(${percentage}%)`
  339. },
  340. fontSize: 10
  341. }
  342. }]
  343. })
  344. const multiBarChartOption = (data, color) => ({
  345. grid: { left: '15%', top: '15%', right: '5%', bottom: '15%', containLabel: true },
  346. xAxis: { type: 'category', data: data.map(d => d.name), axisLine: { lineStyle: { color: '#999' } }, axisLabel: { fontSize: 10, color: '#666' } },
  347. yAxis: { type: 'value', axisLine: { lineStyle: { color: '#999' } }, axisLabel: { fontSize: 10, color: '#666' }, splitLine: { lineStyle: { color: '#eee' } } },
  348. series: [{ type: 'bar', data: data.map(d => d.value), itemStyle: { color: color }, barWidth: 20 }]
  349. })
  350. const multiSeriesBarChartOption = (xAxisData, series) => ({
  351. grid: { left: '10%', top: '15%', right: '5%', bottom: '15%', containLabel: true },
  352. xAxis: {
  353. type: 'category',
  354. data: xAxisData,
  355. axisLine: { lineStyle: { color: '#999' } },
  356. axisLabel: { fontSize: 10, color: '#666' }
  357. },
  358. yAxis: {
  359. type: 'value',
  360. axisLine: { lineStyle: { color: '#999' } },
  361. axisLabel: { fontSize: 10, color: '#666' },
  362. splitLine: { lineStyle: { color: '#eee' } }
  363. },
  364. legend: {
  365. data: series.map(s => s.name),
  366. top: '0%',
  367. textStyle: { fontSize: 10 }
  368. },
  369. series: series.map(s => ({
  370. name: s.name,
  371. type: 'bar',
  372. data: s.data,
  373. itemStyle: { color: s.color },
  374. barWidth: 15
  375. }))
  376. })
  377. onMounted(() => {
  378. fetchData()
  379. })
  380. </script>
  381. <style lang="less" scoped>
  382. .module-brigade-content {
  383. height: 100%;
  384. display: flex;
  385. flex-direction: column;
  386. gap: 10px;
  387. overflow-y: auto;
  388. }
  389. .chart-row {
  390. display: flex;
  391. gap: 10px;
  392. flex-shrink: 0;
  393. }
  394. .chart-item {
  395. flex: 1;
  396. background: #fff;
  397. border-radius: 6px;
  398. padding: 15px;
  399. border: 1px solid #eee;
  400. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  401. display: flex;
  402. flex-direction: column;
  403. min-height: 280px;
  404. }
  405. .chart-title {
  406. font-size: 17px;
  407. color: black;
  408. margin-bottom: 5px;
  409. text-align: left;
  410. }
  411. .echarts {
  412. flex: 1;
  413. width: 100%;
  414. min-height: 0;
  415. }
  416. .number-display {
  417. flex: 1;
  418. display: flex;
  419. flex-direction: column;
  420. align-items: center;
  421. justify-content: center;
  422. }
  423. .number-value {
  424. font-size: 48px;
  425. font-weight: bold;
  426. color: #3b82f6;
  427. }
  428. .number-unit {
  429. font-size: 14px;
  430. color: #3b82f6;
  431. margin-top: 5px;
  432. }
  433. </style>