dutyOrganization.vue 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132
  1. <template>
  2. <view>
  3. <!-- 勤务组织标题 -->
  4. <view class="section-title">
  5. <text class="title-text">勤务组织</text>
  6. </view>
  7. <view class="duty-organization mobile-section">
  8. <!-- 出勤人次分析 -->
  9. <view class="attendance-analysis">
  10. <view class="analysis-header">
  11. <text class="header-text">出勤人次分析</text>
  12. </view>
  13. <!-- 统计卡片 -->
  14. <view class="stat-card">
  15. <view class="stat-content">
  16. <text class="stat-text">
  17. {{ attendanceData && attendanceData.length > 0 && attendanceData[attendanceData.length - 1] ?
  18. attendanceData[attendanceData.length - 1].deptName : '' }}出勤
  19. <text class="highlight">{{ attendanceData && attendanceData.length > 0 &&
  20. attendanceData[attendanceData.length - 1] ? attendanceData[attendanceData.length - 1].currentValue : 0
  21. }}</text>
  22. 人次,同比
  23. <text
  24. :class="getTrendClass(attendanceData && attendanceData.length > 0 && attendanceData[attendanceData.length - 1] ? attendanceData[attendanceData.length - 1].yearOnYearValue : 0)">
  25. {{ formatRate(attendanceData && attendanceData.length > 0 && attendanceData[attendanceData.length - 1] ?
  26. attendanceData[attendanceData.length - 1].yearOnYearValue : 0) }}
  27. </text>
  28. <template v-if="queryForm.dateRangeQueryType !== 'YEAR'">
  29. ,环比
  30. <text
  31. :class="getTrendClass(attendanceData && attendanceData.length > 0 && attendanceData[attendanceData.length - 1] ? attendanceData[attendanceData.length - 1].chainRatioValue : 0)">
  32. {{ formatRate(attendanceData && attendanceData.length > 0 && attendanceData[attendanceData.length - 1]
  33. ?
  34. attendanceData[attendanceData.length - 1].chainRatioValue : 0) }}
  35. </text>
  36. </template>
  37. </text>
  38. </view>
  39. </view>
  40. <!-- 主管数据卡片 -->
  41. <view class="dept-cards">
  42. <template v-if="attendanceData && attendanceData.length > 0">
  43. <view v-for="(item, index) in attendanceData.slice(0, 3)" :key="index" class="dept-card">
  44. <view class="dept-name">{{ item.deptName || loginUserName }}</view>
  45. <view class="dept-count">
  46. <text class="count-number">{{ item.currentValue || 0 }}</text>
  47. <text class="count-unit">人次</text>
  48. </view>
  49. <view class="dept-trend">
  50. <view class="trend-item">
  51. <text>同比</text>
  52. <text :class="getTrendClass(item.yearOnYearValue)">{{ formatRate(item.yearOnYearValue) }}</text>
  53. </view>
  54. <template v-if="queryForm.dateRangeQueryType !== 'YEAR'">
  55. <view class="trend-item">
  56. <text>环比</text>
  57. <text :class="getTrendClass(item.chainRatioValue)">{{ formatRate(item.chainRatioValue) }}</text>
  58. </view>
  59. </template>
  60. </view>
  61. </view>
  62. </template>
  63. <template v-else>
  64. <!-- 空数据时显示占位卡片 -->
  65. <view v-for="i in 3" :key="i" class="dept-card">
  66. <view class="dept-name">暂无数据</view>
  67. <view class="dept-count">
  68. <text class="count-number">0</text>
  69. <text class="count-unit">人次</text>
  70. </view>
  71. <view class="dept-trend">
  72. <view class="trend-item">
  73. <text>同比</text>
  74. <text class="trend-neutral">0%</text>
  75. </view>
  76. <template v-if="queryForm.dateRangeQueryType !== 'YEAR'">
  77. <view class="trend-item">
  78. <text>环比</text>
  79. <text class="trend-neutral">0%</text>
  80. </view>
  81. </template>
  82. </view>
  83. </view>
  84. </template>
  85. </view>
  86. <!-- 图表区域 -->
  87. <view class="chart-container">
  88. <view ref="attendanceBarChartRef" class="echarts-chart"></view>
  89. </view>
  90. </view>
  91. <!-- 资质等级分布 -->
  92. <view class="qualification-distribution" v-show="!isUserType">
  93. <view class="distribution-header">
  94. <text class="header-text">资质等级分布</text>
  95. </view>
  96. <!-- 资质等级分布内容区域 -->
  97. <view class="qualification-content">
  98. <!-- 上方:资质等级统计 -->
  99. <view class="qualification-section">
  100. <!-- 描述卡片 -->
  101. <view class="stat-card" v-if="qualificationPieDescriptionPart1">
  102. <view class="stat-content">
  103. {{ qualificationPieDescriptionPart1 || '资质等级分布数据加载中...' }}
  104. </view>
  105. </view>
  106. <!-- 饼图 -->
  107. <view class="chart-container">
  108. <view ref="pieChartRef" class="echarts-chart"></view>
  109. </view>
  110. </view>
  111. <!-- 中间:资质分布趋势 -->
  112. <view class="qualification-section" v-show="isStationType">
  113. <!-- 描述卡片 -->
  114. <view class="stat-card" v-if="qualificationPieDescriptionPart2">
  115. <view class="stat-content">
  116. {{ qualificationPieDescriptionPart2 || '资质分布趋势数据加载中...' }}
  117. </view>
  118. </view>
  119. <!-- 柱状图 -->
  120. <view class="chart-container">
  121. <view ref="barChartRef" class="echarts-chart"></view>
  122. </view>
  123. </view>
  124. <!-- 下方:班组人员资质等级表格 -->
  125. <view class="qualification-section" v-show="isTeamsType">
  126. <statistic-table :columns="teamsTableColumns" :data="teamsQualificationData" blankData="未知" />
  127. </view>
  128. </view>
  129. </view>
  130. <!-- 资质能力 -->
  131. <view class="qualification-ability" v-show="isUserType">
  132. <view class="ability-header">
  133. <text class="header-text">资质能力</text>
  134. </view>
  135. <!-- 统计卡片 -->
  136. <view class="stat-card">
  137. <view class="stat-content">资质等级:{{ user && user.qualificationLevel ? user.qualificationLevel : '未知' }}</view>
  138. <view class="stat-content">可上岗岗位:{{user && user.sysPostList ? user.sysPostList.map(item =>
  139. item.postName).join('、')
  140. : '未知'}}</view>
  141. </view>
  142. </view>
  143. </view>
  144. </view>
  145. </template>
  146. <script>
  147. import * as echarts from 'echarts'
  148. import { getCalculate, getCalculateTrendData, getQualificationPieChart, getQualificationBarChart } from '@/api/qualityControlAnalysisReport/qualityControlAnalysisReport'
  149. import StatisticTable from '@/components/statistic-table/statistic-table.vue'
  150. export default {
  151. name: 'DutyOrganization',
  152. components: {
  153. StatisticTable
  154. },
  155. props: {
  156. queryForm: {
  157. type: Object,
  158. default: () => ({})
  159. }
  160. },
  161. data() {
  162. return {
  163. attendanceData: [],
  164. trendData: [],
  165. qualificationPieData: [],
  166. qualificationBarData: [],
  167. teamsQualificationData: [],
  168. teamsTableColumns: [
  169. { props: 'deptName', title: '姓名' },
  170. { props: 'qualificationLevel', title: '等级' }
  171. ],
  172. qualificationPieDescriptionPart1: '',
  173. qualificationPieDescriptionPart2: '',
  174. user: {},
  175. loading: false,
  176. pieChart: null,
  177. barChart: null,
  178. attendanceBarChart: null
  179. }
  180. },
  181. computed: {
  182. loginUserName() {
  183. return this.$store.state?.user?.userInfo?.nickName
  184. },
  185. // 计算属性:检查是否为USER类型
  186. isUserType() {
  187. const roles = this.$store.state?.user?.roles || []
  188. // 如果是班组长且选择了个人视图,则视为用户类型
  189. if (roles.includes('banzuzhang') && this.queryForm.scopedType === 'USER') {
  190. return true
  191. }
  192. return roles.includes('SecurityCheck')
  193. },
  194. // 计算属性:检查是否为STATION类型
  195. isStationType() {
  196. const roles = this.$store.state?.user?.roles || []
  197. return roles.includes('test') || roles.includes('zhijianke')
  198. },
  199. // 计算属性:检查是否为TEAMS类型
  200. isTeamsType() {
  201. const roles = this.$store.state?.user?.roles || []
  202. return roles.includes('banzuzhang') && this.queryForm.scopedType === 'TEAMS'
  203. }
  204. },
  205. mounted() {
  206. this.loadData()
  207. this.$nextTick(() => {
  208. this.initCharts()
  209. })
  210. },
  211. beforeDestroy() {
  212. this.disposeCharts()
  213. },
  214. watch: {
  215. queryForm: {
  216. handler() {
  217. this.loadData()
  218. },
  219. deep: true
  220. },
  221. },
  222. methods: {
  223. // 初始化图表
  224. initCharts() {
  225. // 资质等级饼图
  226. if (this.$refs.pieChartRef) {
  227. this.initPieChart()
  228. }
  229. // 资质等级柱状图
  230. if (this.$refs.barChartRef) {
  231. this.initBarChart()
  232. }
  233. // 出勤人次柱状图
  234. if (this.$refs.attendanceBarChartRef) {
  235. this.initAttendanceBarChart()
  236. }
  237. },
  238. // 初始化饼图
  239. initPieChart() {
  240. const pieOptions = {
  241. tooltip: {
  242. trigger: 'item',
  243. formatter: '{a} <br/>{b}: {c}人 ({d}%)'
  244. },
  245. legend: {
  246. orient: 'vertical',
  247. left: 'left',
  248. top: 'center',
  249. textStyle: {
  250. color: '#333',
  251. fontSize: 12
  252. },
  253. show: false
  254. },
  255. color: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57'],
  256. series: [
  257. {
  258. name: '资质等级分布',
  259. type: 'pie',
  260. radius: '50%',
  261. center: ['50%', '50%'],
  262. avoidLabelOverlap: false,
  263. itemStyle: {
  264. borderRadius: 10,
  265. borderColor: '#fff',
  266. borderWidth: 2
  267. },
  268. label: {
  269. show: true,
  270. formatter: '{b}: {c}人 ({d}%)',
  271. fontSize: 12
  272. },
  273. emphasis: {
  274. label: {
  275. show: true,
  276. fontSize: 14,
  277. fontWeight: 'bold'
  278. },
  279. itemStyle: {
  280. shadowBlur: 10,
  281. shadowOffsetX: 0,
  282. shadowColor: 'rgba(0, 0, 0, 0.5)'
  283. }
  284. },
  285. labelLine: {
  286. show: true,
  287. length: 10,
  288. length2: 20
  289. },
  290. data: []
  291. }
  292. ]
  293. }
  294. // 在uni-app中使用uni.createCanvasContext或第三方图表库
  295. this.pieChart = this.createEChartsInstance(this.$refs.pieChartRef, pieOptions)
  296. },
  297. // 初始化出勤人次柱状图
  298. initAttendanceBarChart() {
  299. const attendanceBarOptions = {
  300. tooltip: {
  301. trigger: 'axis',
  302. axisPointer: {
  303. type: 'shadow'
  304. },
  305. formatter: function (params) {
  306. let result = `${params[0].axisValue}<br/>`
  307. params.forEach(param => {
  308. result += `${param.seriesName}: <span style="color:${param.color};font-weight:bold">${param.data}</span>人<br/>`
  309. })
  310. return result
  311. }
  312. },
  313. legend: {
  314. data: ['安检一大队', '安检二大队', '安检三大队', '全站'],
  315. top: 0,
  316. show: true,
  317. },
  318. grid: {
  319. left: '3%',
  320. right: '4%',
  321. bottom: '0%',
  322. top: '80',
  323. containLabel: true
  324. },
  325. xAxis: {
  326. type: 'category',
  327. data: [],
  328. axisLine: {
  329. lineStyle: {
  330. color: '#999'
  331. }
  332. },
  333. axisLabel: {
  334. fontSize: 12,
  335. interval: 0
  336. }
  337. },
  338. yAxis: {
  339. type: 'value',
  340. name: '人数',
  341. nameTextStyle: {
  342. fontSize: 12,
  343. color: '#666'
  344. },
  345. axisLine: {
  346. lineStyle: {
  347. color: '#999'
  348. }
  349. },
  350. axisLabel: {
  351. fontSize: 11,
  352. margin: 8,
  353. color: '#666'
  354. },
  355. splitLine: {
  356. lineStyle: {
  357. color: '#f5f5f5',
  358. type: 'dashed'
  359. }
  360. },
  361. minInterval: 1,
  362. splitNumber: 6
  363. },
  364. series: [
  365. {
  366. name: '全站',
  367. type: 'bar',
  368. barWidth: '10%',
  369. itemStyle: {
  370. color: '#5470C6'
  371. },
  372. data: []
  373. },
  374. {
  375. name: '安检一大队',
  376. type: 'bar',
  377. barWidth: '10%',
  378. itemStyle: {
  379. color: '#FF6B6B'
  380. },
  381. data: []
  382. },
  383. {
  384. name: '安检二大队',
  385. type: 'bar',
  386. barWidth: '10%',
  387. itemStyle: {
  388. color: '#4ECDC4'
  389. },
  390. data: []
  391. },
  392. {
  393. name: '安检三大队',
  394. type: 'bar',
  395. barWidth: '10%',
  396. itemStyle: {
  397. color: '#FFD166'
  398. },
  399. data: []
  400. }
  401. ]
  402. }
  403. this.attendanceBarChart = this.createEChartsInstance(this.$refs.attendanceBarChartRef, attendanceBarOptions)
  404. },
  405. // 初始化资质等级柱状图
  406. initBarChart() {
  407. const barOptions = {
  408. tooltip: {
  409. trigger: 'axis',
  410. axisPointer: {
  411. type: 'shadow'
  412. },
  413. formatter: function (params) {
  414. let result = `${params[0].axisValue}<br/>`
  415. params.forEach(param => {
  416. result += `${param.seriesName}: <span style="color:${param.color};font-weight:bold">${param.data}</span>人<br/>`
  417. })
  418. return result
  419. }
  420. },
  421. legend: {
  422. data: ['安检一大队', '安检二大队', '安检三大队'],
  423. top: 0
  424. },
  425. grid: {
  426. left: '3%',
  427. right: '4%',
  428. bottom: '3%',
  429. top: '40%',
  430. containLabel: true
  431. },
  432. xAxis: {
  433. type: 'category',
  434. data: [],
  435. axisLine: {
  436. lineStyle: {
  437. color: '#999'
  438. }
  439. },
  440. axisLabel: {
  441. fontSize: 12
  442. }
  443. },
  444. yAxis: {
  445. type: 'value',
  446. name: '人数',
  447. axisLine: {
  448. lineStyle: {
  449. color: '#999'
  450. }
  451. },
  452. splitLine: {
  453. lineStyle: {
  454. color: '#f0f0f0'
  455. }
  456. }
  457. },
  458. series: [
  459. ]
  460. }
  461. this.barChart = this.createEChartsInstance(this.$refs.barChartRef, barOptions)
  462. },
  463. // 创建ECharts实例(适配uni-app环境)
  464. createEChartsInstance(chartRef, options) {
  465. if (!chartRef) return null
  466. // 获取DOM元素
  467. const chartDom = chartRef.$el || chartRef
  468. if (!chartDom) return null
  469. // 创建ECharts实例
  470. const chartInstance = echarts.init(chartDom)
  471. // 设置初始选项
  472. if (options) {
  473. chartInstance.setOption(options)
  474. }
  475. return chartInstance
  476. },
  477. // 销毁图表
  478. disposeCharts() {
  479. if (this.pieChart) {
  480. this.pieChart.dispose()
  481. this.pieChart = null
  482. }
  483. if (this.attendanceBarChart) {
  484. this.attendanceBarChart.dispose()
  485. this.attendanceBarChart = null
  486. }
  487. },
  488. processQueryParams(queryParams) {
  489. const processedParams = { ...queryParams }
  490. if (processedParams.dateRangeQueryType === 'YEAR') {
  491. processedParams.yearOnYear = true
  492. } else {
  493. processedParams.chainRatio = true
  494. processedParams.yearOnYear = true
  495. }
  496. processedParams.deptId = ['TEAMS', 'DEPARTMENT', 'BRIGADE', 'MANAGER'].includes(processedParams.scopedType) ? processedParams.scopedId : '';
  497. if (processedParams.scopedType == 'TEAMS') {
  498. delete processedParams.userId;
  499. }
  500. processedParams.userId = ['USER'].includes(processedParams.scopedType) ? processedParams.scopedId : '';
  501. if (processedParams.scopedType == 'USER') {
  502. delete processedParams.deptId;
  503. }
  504. return {
  505. ...processedParams,
  506. }
  507. },
  508. // 加载数据
  509. async loadData() {
  510. if (this.loading) return
  511. this.loading = true
  512. const processedParams = this.processQueryParams(this.queryForm)
  513. try {
  514. // 加载资质等级柱状图数据(如果是STATION类型)
  515. if (this.isStationType) {
  516. const barResponse = await getQualificationBarChart(processedParams)
  517. if (barResponse.code === 200) {
  518. this.qualificationBarData = barResponse.data?.brigades || []
  519. }
  520. }
  521. // 加载班组人员资质等级数据(如果是TEAMS类型)
  522. if (this.isTeamsType) {
  523. const barResponse = await getQualificationBarChart(processedParams)
  524. if (barResponse.code === 200) {
  525. this.teamsQualificationData = barResponse.data?.brigades || []
  526. }
  527. }
  528. if (this.isUserType) {
  529. // 获取资质等级分布柱状图数据
  530. const barResponse = await getQualificationBarChart(processedParams)
  531. console.log('资质等级分布柱状图数据:', barResponse.data.brigades)
  532. this.user = barResponse?.data?.brigades[0]
  533. }
  534. // 加载出勤人次分析数据
  535. const attendanceResponse = await getCalculate(processedParams)
  536. this.attendanceData = attendanceResponse || []
  537. // 加载出勤人次趋势数据
  538. const trendResponse = await getCalculateTrendData(processedParams)
  539. this.trendData = trendResponse || []
  540. // 加载资质等级分布数据
  541. if (!this.isUserType) {
  542. const qualificationResponse = await getQualificationPieChart(processedParams)
  543. if (qualificationResponse.code === 200) {
  544. this.qualificationPieData = qualificationResponse.data?.data || []
  545. this.qualificationPieDescriptionPart1 = this.formatQualificationDescriptionPart1(this.qualificationPieData)
  546. this.qualificationPieDescriptionPart2 = this.formatQualificationDescriptionPart2(this.qualificationPieData)
  547. }
  548. } else {
  549. this.qualificationPieData = []
  550. this.qualificationPieDescriptionPart1 = ''
  551. this.qualificationPieDescriptionPart2 = ''
  552. }
  553. // 检查是否为用户类型
  554. this.checkUserType()
  555. setTimeout(() => {
  556. // 更新图表数据
  557. this.updateChartsWithData()
  558. }, 0);
  559. } catch (error) {
  560. console.error('加载勤务组织数据失败:', error)
  561. // 模拟数据作为备选
  562. } finally {
  563. this.loading = false
  564. }
  565. },
  566. // 其他部门类型的出勤人次柱状图更新函数
  567. updateAttendanceBarChartForOtherTypes() {
  568. if (this.trendData && Array.isArray(this.trendData) && this.attendanceBarChart) {
  569. const trendList = this.trendData
  570. // 提取横坐标数据(timeLabel字段)
  571. const xAxisData = trendList.map(item => item.timeLabel || '未知时间')
  572. // 提取总体数据
  573. const allData = trendList.map(item => item.overall || 0)
  574. // 更新图表配置
  575. const newOptions = {
  576. legend: {
  577. show: false
  578. },
  579. grid: {
  580. left: '3%',
  581. right: '4%',
  582. bottom: '0%',
  583. top: '20%',
  584. containLabel: true
  585. },
  586. xAxis: {
  587. data: xAxisData
  588. },
  589. series: [
  590. {
  591. name: '总体',
  592. type: 'bar',
  593. barWidth: '10%',
  594. itemStyle: {
  595. color: '#5470C6'
  596. },
  597. data: allData
  598. }
  599. ]
  600. }
  601. this.attendanceBarChart.setOption(newOptions)
  602. } else {
  603. // 无数据时清空图表
  604. const newOptions = {
  605. legend: {
  606. show: false
  607. },
  608. xAxis: {
  609. data: []
  610. },
  611. series: []
  612. }
  613. this.attendanceBarChart.setOption(newOptions)
  614. }
  615. },
  616. // 更新图表数据
  617. updateChartsWithData() {
  618. // 更新出勤人次柱状图 - 根据部门类型选择不同的更新函数
  619. if (this.isStationType) {
  620. // 站级别:使用原来的逻辑,显示多个主管数据
  621. this.updateAttendanceBarChart()
  622. } else {
  623. // 其他部门类型:使用新的函数,只显示总体数据
  624. this.updateAttendanceBarChartForOtherTypes()
  625. }
  626. // 更新资质等级分布饼图
  627. this.updateQualificationPieChart()
  628. // 更新资质等级柱状图(如果是STATION类型)
  629. if (this.isStationType) {
  630. this.updateQualificationBarChart()
  631. }
  632. },
  633. // 更新出勤人次柱状图
  634. updateAttendanceBarChart() {
  635. if (this.trendData && Array.isArray(this.trendData) && this.attendanceBarChart) {
  636. const trendList = this.trendData
  637. // 提取横坐标数据(timeLabel字段)
  638. const xAxisData = trendList.map(item => item.timeLabel || '未知时间')
  639. // 提取各主管数据
  640. const allData = trendList.map(item => item.overall || 0)
  641. const dept1Data = trendList.map(item => item.data1 || 0)
  642. const dept2Data = trendList.map(item => item.data2 || 0)
  643. const dept3Data = trendList.map(item => item.data3 || 0)
  644. // 更新图表配置
  645. const newOptions = {
  646. legend: { show: !!this.isStationType },
  647. xAxis: {
  648. data: xAxisData
  649. },
  650. series: [
  651. {
  652. name: '全站',
  653. type: 'bar',
  654. barWidth: '10%',
  655. itemStyle: {
  656. color: '#5470C6'
  657. },
  658. data: allData
  659. },
  660. {
  661. name: '安检一大队',
  662. type: 'bar',
  663. barWidth: '10%',
  664. itemStyle: {
  665. color: '#FF6B6B'
  666. },
  667. data: dept1Data
  668. },
  669. {
  670. name: '安检二大队',
  671. type: 'bar',
  672. barWidth: '10%',
  673. itemStyle: {
  674. color: '#4ECDC4'
  675. },
  676. data: dept2Data
  677. },
  678. {
  679. name: '安检三大队',
  680. type: 'bar',
  681. barWidth: '10%',
  682. itemStyle: {
  683. color: '#FFD166'
  684. },
  685. data: dept3Data
  686. }
  687. ]
  688. }
  689. this.attendanceBarChart.setOption(newOptions)
  690. }
  691. },
  692. // 更新资质等级分布饼图
  693. updateQualificationPieChart() {
  694. if (this.qualificationPieData && Array.isArray(this.qualificationPieData) && this.pieChart) {
  695. // 转换数据格式
  696. const formattedData = this.qualificationPieData.map(item => ({
  697. name: item.levelName || '未知等级',
  698. value: item.count || 0
  699. })).filter(item => item.value > 0)
  700. // 更新图表配置
  701. const newOptions = {
  702. series: [
  703. {
  704. data: formattedData
  705. }
  706. ]
  707. }
  708. this.pieChart.setOption(newOptions)
  709. }
  710. },
  711. // 更新资质等级柱状图
  712. updateQualificationBarChart() {
  713. if (this.qualificationBarData && Array.isArray(this.qualificationBarData) && this.barChart) {
  714. const barData = this.qualificationBarData
  715. // 提取所有唯一的等级名称(横坐标)
  716. const allLevelNames = []
  717. barData.forEach(dept => {
  718. if (dept.levelCounts && Array.isArray(dept.levelCounts)) {
  719. dept.levelCounts.forEach(level => {
  720. if (level.levelName && !allLevelNames.includes(level.levelName)) {
  721. allLevelNames.push(level.levelName)
  722. }
  723. })
  724. }
  725. })
  726. // 按等级顺序排序(一级、二级、三级、四级、五级)
  727. const levelOrder = ['一级', '二级', '三级', '四级', '五级']
  728. const sortedLevelNames = allLevelNames.sort((a, b) => {
  729. return levelOrder.indexOf(a) - levelOrder.indexOf(b)
  730. })
  731. // 为每个主管创建数据系列
  732. const seriesData = []
  733. const colors = ['#FF6B6B', '#4ECDC4', '#FFD166', '#9B59B6', '#3498DB']
  734. barData.forEach((dept, index) => {
  735. if (dept.deptName && dept.levelCounts) {
  736. // 为每个等级查找对应的数量
  737. const levelCounts = sortedLevelNames.map(levelName => {
  738. const levelData = dept.levelCounts.find(level => level.levelName === levelName)
  739. return levelData ? levelData.count || 0 : 0
  740. })
  741. seriesData.push({
  742. name: dept.deptName,
  743. type: 'bar',
  744. barWidth: '15%',
  745. itemStyle: {
  746. color: colors[index % colors.length]
  747. },
  748. label: {
  749. show: false,
  750. position: 'top',
  751. formatter: '{c}人'
  752. },
  753. data: levelCounts
  754. })
  755. }
  756. })
  757. // 更新图表配置
  758. const newOptions = {
  759. xAxis: {
  760. data: sortedLevelNames
  761. },
  762. legend: {
  763. data: barData.map(dept => dept.deptName).filter(name => name)
  764. },
  765. series: seriesData
  766. }
  767. this.barChart.setOption(newOptions)
  768. } else {
  769. // 无数据时清空图表
  770. const newOptions = {
  771. xAxis: {
  772. data: []
  773. },
  774. legend: {
  775. data: []
  776. },
  777. series: []
  778. }
  779. this.barChart.setOption(newOptions)
  780. }
  781. },
  782. // 格式化资质等级描述第一部分
  783. formatQualificationDescriptionPart1(data) {
  784. if (!Array.isArray(data)) {
  785. return '当前部门资质等级分布均衡,高级资质占比35%,中级资质占比45%,初级资质占比20%'
  786. }
  787. // 找出占比最高的等级
  788. const highestLevel = data.reduce((max, item) => {
  789. return (item.count || 0) > (max.count || 0) ? item : max
  790. }, { levelName: '高级', count: 0 })
  791. // 计算总人数
  792. const totalCount = data.reduce((sum, item) => sum + (item.count || 0), 0)
  793. // 计算占比
  794. const highestPercentage = totalCount > 0 ? ((highestLevel.count / totalCount) * 100).toFixed(2) : '0.0'
  795. const deptName = this.attendanceData && this.attendanceData.length > 0 && this.attendanceData[this.attendanceData.length - 1] ?
  796. this.attendanceData[this.attendanceData.length - 1].deptName : ''
  797. return `${deptName}资质等级以"${highestLevel.levelName || '高级'}"为主(占比为${highestPercentage}%)`
  798. },
  799. // 格式化资质等级描述第二部分
  800. formatQualificationDescriptionPart2(data) {
  801. if (this.qualificationBarData.length == 0) {
  802. return ''
  803. }
  804. // 从资质柱状图数据中找出"一级"人员最多的科室
  805. let topDeptForLevel1 = ''
  806. let level1Count = 0
  807. let totalDeptCount = 0
  808. let allDeptNames = []
  809. if (this.qualificationBarData && Array.isArray(this.qualificationBarData)) {
  810. // 找出"一级"人员最多的科室
  811. let maxLevel1Count = 0
  812. let maxDeptName = ''
  813. let totalCountForDept = 0
  814. this.qualificationBarData.forEach(dept => {
  815. allDeptNames.push(dept.deptName)
  816. if (dept.levelCounts && Array.isArray(dept.levelCounts)) {
  817. const level1Data = dept.levelCounts.find(level => level.levelName === '高级')
  818. const deptTotalCount = dept.levelCounts.reduce((sum, level) => sum + (level.count || 0), 0)
  819. if (level1Data && level1Data.count > maxLevel1Count) {
  820. maxLevel1Count = level1Data.count
  821. maxDeptName = dept.deptName || ''
  822. totalCountForDept = deptTotalCount
  823. }
  824. }
  825. })
  826. topDeptForLevel1 = maxDeptName
  827. level1Count = maxLevel1Count
  828. totalDeptCount = totalCountForDept
  829. }
  830. // 生成第二部分描述文字
  831. return `全站资质等级为"高级"的人员集中在${topDeptForLevel1}(${level1Count}人)${topDeptForLevel1}的人员规模(共${totalDeptCount}人)高于${allDeptNames.filter(name => name !== topDeptForLevel1).join(', ')}`
  832. },
  833. // 检查是否为用户类型(已废弃,使用computed属性替代)
  834. checkUserType() {
  835. // 此方法已废弃,角色判断现在通过computed属性实现
  836. },
  837. // 获取趋势样式类
  838. getTrendClass(value) {
  839. if (value > 0) return 'trend-up'
  840. if (value < 0) return 'trend-down'
  841. return 'trend-neutral'
  842. },
  843. // 格式化比率显示
  844. formatRate(rate) {
  845. if (rate === null || rate === undefined) return '0%'
  846. const numRate = parseFloat(rate)
  847. if (numRate > 0) {
  848. return `+${numRate.toFixed(2)}%`
  849. } else if (numRate < 0) {
  850. return `${numRate.toFixed(2)}%`
  851. } else {
  852. return '--%'
  853. }
  854. },
  855. }
  856. }
  857. </script>
  858. <style lang="scss" scoped>
  859. .duty-organization {
  860. background: #fff;
  861. border-radius: 16rpx;
  862. }
  863. .section-title {
  864. margin: 24rpx 0;
  865. .title-text {
  866. font-size: 32rpx;
  867. font-weight: bold;
  868. color: #333;
  869. }
  870. }
  871. .attendance-analysis,
  872. .qualification-distribution,
  873. .qualification-ability {
  874. padding: 20rpx;
  875. border: 1px solid #e8e8e8;
  876. border-radius: 10rpx;
  877. margin-bottom: 32rpx;
  878. }
  879. .qualification-content {
  880. display: flex;
  881. flex-direction: column;
  882. gap: 32rpx;
  883. }
  884. .qualification-section {
  885. display: flex;
  886. flex-direction: column;
  887. gap: 16rpx;
  888. }
  889. .analysis-header,
  890. .distribution-header,
  891. .ability-header {
  892. margin-bottom: 16rpx;
  893. .header-text {
  894. font-size: 28rpx;
  895. font-weight: 600;
  896. color: #333;
  897. }
  898. }
  899. .stat-card {
  900. background: #f8f9fa;
  901. border-radius: 12rpx;
  902. padding: 20rpx;
  903. margin-bottom: 16rpx;
  904. .stat-content {
  905. font-size: 26rpx;
  906. line-height: 1.5;
  907. color: #666;
  908. .highlight {
  909. color: #1890ff;
  910. font-weight: 600;
  911. margin: 0 8rpx;
  912. }
  913. }
  914. }
  915. .dept-cards {
  916. display: flex;
  917. flex-direction: row;
  918. gap: 16rpx;
  919. margin-bottom: 24rpx;
  920. }
  921. .dept-card {
  922. flex: 1;
  923. background: #f8f9fa;
  924. border-radius: 12rpx;
  925. padding: 20rpx;
  926. .dept-name {
  927. font-size: 26rpx;
  928. font-weight: 600;
  929. color: #333;
  930. margin-bottom: 12rpx;
  931. }
  932. .dept-count {
  933. display: flex;
  934. align-items: baseline;
  935. margin-bottom: 12rpx;
  936. .count-number {
  937. font-size: 32rpx;
  938. font-weight: bold;
  939. color: #1890ff;
  940. margin-right: 8rpx;
  941. }
  942. .count-unit {
  943. font-size: 24rpx;
  944. color: #999;
  945. }
  946. }
  947. .dept-trend {
  948. display: flex;
  949. gap: 16rpx;
  950. flex-direction: column;
  951. .trend-item {
  952. font-size: 24rpx;
  953. color: #666;
  954. .trend-up {
  955. color: #f5222d;
  956. }
  957. .trend-down {
  958. color: #52c41a;
  959. }
  960. .trend-neutral {
  961. color: #999;
  962. }
  963. }
  964. }
  965. }
  966. .chart-container {
  967. background: #f8f9fa;
  968. border-radius: 12rpx;
  969. padding: 20rpx;
  970. height: 500rpx;
  971. .echarts-chart {
  972. width: 100%;
  973. height: 100%;
  974. }
  975. }
  976. .trend-up {
  977. color: #f5222d;
  978. }
  979. .trend-down {
  980. color: #52c41a;
  981. }
  982. .trend-neutral {
  983. color: #999;
  984. }
  985. </style>