dutyOrganization.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248
  1. <template>
  2. <div class="duty-organization">
  3. <!-- 勤务组织标题 -->
  4. <div class="section-title">
  5. <h2>勤务组织</h2>
  6. </div>
  7. <!-- 横向布局内容区域 -->
  8. <div class="content-layout">
  9. <!-- 左侧:出勤人次分析 -->
  10. <div class="left-panel panel-item">
  11. <div class="panel-header">
  12. <h3>出勤人次分析</h3>
  13. </div>
  14. <!-- 统计卡片 -->
  15. <div class="stat-card">
  16. <div class="stat-content">
  17. {{ attendanceData && attendanceData.length > 0 ? attendanceData[attendanceData.length -
  18. 1]?.deptName : ''
  19. }}出勤<span>{{ attendanceData && attendanceData.length > 0 ? attendanceData[attendanceData.length -
  20. 1]?.currentValue : 0
  21. }}</span>人次,同比
  22. <span
  23. :class="getTrendClass(attendanceData && attendanceData.length > 0 ? attendanceData[attendanceData.length - 1]?.yearOnYearValue : 0)">{{
  24. formatRate(attendanceData && attendanceData.length > 0 ? attendanceData[attendanceData.length -
  25. 1]?.yearOnYearValue : 0) }}</span>
  26. <template v-if="props.queryForm.dateRangeQueryType !== 'YEAR'">
  27. ,环比
  28. <span
  29. :class="getTrendClass(attendanceData && attendanceData.length > 0 ? attendanceData[attendanceData.length - 1]?.chainRatioValue : 0)">{{
  30. formatRate(attendanceData && attendanceData.length > 0 ? attendanceData[attendanceData.length -
  31. 1]?.chainRatioValue : 0)
  32. }}</span>
  33. </template>
  34. </div>
  35. </div>
  36. <!-- 科室数据卡片 -->
  37. <div class="dept-cards">
  38. <template v-if="attendanceData && attendanceData.length > 0">
  39. <div v-for="(item, index) in attendanceData.slice(0, 4)" :key="index" class="dept-card">
  40. <div class="dept-name">{{ item.deptName || selectedDeptObject?.label }}</div>
  41. <div class="dept-count"><span>{{ item.currentValue || 0 }}</span>人次</div>
  42. <div class="dept-trend">
  43. <div>
  44. 同比<span :class="getTrendClass(item.yearOnYearValue)">{{ formatRate(item.yearOnYearValue) }}</span>
  45. </div>
  46. <template v-if="props.queryForm.dateRangeQueryType !== 'YEAR'">
  47. <div>
  48. 环比<span :class="getTrendClass(item.chainRatioValue)">{{ formatRate(item.chainRatioValue) }}</span>
  49. </div>
  50. </template>
  51. </div>
  52. </div>
  53. </template>
  54. <template v-else>
  55. <!-- 空数据时显示占位卡片 -->
  56. <div v-for="i in 3" :key="i" class="dept-card empty">
  57. <div class="dept-name">暂无数据</div>
  58. <div class="dept-count"><span>0</span>人次</div>
  59. <div class="dept-trend">
  60. 同比<span class="trend-neutral">0%</span>
  61. <template v-if="props.queryForm.dateRangeQueryType !== 'YEAR'">
  62. 环比<span class="trend-neutral">0%</span>
  63. </template>
  64. </div>
  65. </div>
  66. </template>
  67. </div>
  68. <!-- 柱状图区域 -->
  69. <div class="chart-container">
  70. <div ref="attendanceBarChartRef" class="echarts-chart"></div>
  71. </div>
  72. </div>
  73. <!-- 右侧:资质等级分布 -->
  74. <div class="right-panel panel-item" v-show="!isUserType">
  75. <div class="panel-header">
  76. <h3>资质等级分布</h3>
  77. </div>
  78. <!-- 资质等级分布内容区域 -->
  79. <div class="qualification-content">
  80. <!-- 左侧:资质等级统计 -->
  81. <div class="qualification-left">
  82. <!-- 上方描述卡片 -->
  83. <div class="stat-card" v-if="qualificationPieDescriptionPart1">
  84. <div class="stat-content">
  85. {{ qualificationPieDescriptionPart1 || '资质等级分布数据加载中...' }}
  86. </div>
  87. </div>
  88. <!-- 下方饼图 -->
  89. <div class="chart-container">
  90. <div ref="pieChartRef" class="echarts-chart"></div>
  91. </div>
  92. </div>
  93. <!-- 右侧:资质分布趋势 -->
  94. <div class="qualification-right" v-show="isStationType">
  95. <!-- 上方描述卡片 -->
  96. <div class="stat-card" v-if="qualificationPieDescriptionPart2">
  97. <div class="stat-content">
  98. {{ qualificationPieDescriptionPart2 || '资质分布趋势数据加载中...' }}
  99. </div>
  100. </div>
  101. <!-- 下方柱状图 -->
  102. <div class="chart-container">
  103. <div ref="barChartRef" class="echarts-chart"></div>
  104. </div>
  105. </div>
  106. <div class="qualification-right" style="justify-content: center;" v-show="isTeamsType">
  107. <!-- 班组人员资质等级表格 -->
  108. <div class="table-container">
  109. <el-table :data="teamsQualificationData" size="small" height="250" border :scroll="{ x: 'max-content' }"
  110. stripe>
  111. <el-table-column prop="deptName" label="姓名" align="center" />
  112. <el-table-column prop="qualificationLevel" label="等级" align="center" />
  113. </el-table>
  114. </div>
  115. </div>
  116. </div>
  117. </div>
  118. <div class="right-panel-ability panel-item" v-show="isUserType">
  119. <div class="panel-header">
  120. <h3>资质能力</h3>
  121. </div>
  122. <!-- 统计卡片 -->
  123. <div class="stat-card">
  124. <div class="stat-content">资质等级:{{ user?.qualificationLevel || '未知' }}</div>
  125. <div class="stat-content">可上岗岗位:{{user?.sysPostList && user?.sysPostList.map(item => item.postName).join('、')
  126. || '未知'
  127. }}</div>
  128. </div>
  129. </div>
  130. </div>
  131. </div>
  132. </template>
  133. <script setup>
  134. import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
  135. import { useEcharts } from '@/hooks/chart.js'
  136. import { getCalculate, getCalculateTrendData, getQualificationPieChart, getQualificationBarChart } from '@/api/assistant/assistant.js'
  137. // 定义props接收queryForm参数
  138. const props = defineProps({
  139. queryForm: {
  140. type: Object,
  141. default: () => ({
  142. dateRangeQueryType: '',
  143. year: '',
  144. quarter: '',
  145. month: ''
  146. })
  147. },
  148. selectedDeptObject: {
  149. type: Object,
  150. default: null
  151. }
  152. })
  153. const user = ref({})
  154. const teamsQualificationData = ref([])
  155. // 饼图容器引用
  156. const pieChartRef = ref(null)
  157. const barChartRef = ref(null)
  158. const attendanceBarChartRef = ref(null)
  159. // 图表实例
  160. const { setOption: setPieOption, dispose: disposePie } = useEcharts(pieChartRef)
  161. const { setOption: setBarOption, dispose: disposeBar } = useEcharts(barChartRef)
  162. const { setOption: setAttendanceOption, dispose: disposeAttendance } = useEcharts(attendanceBarChartRef)
  163. // 响应式数据
  164. const attendanceData = ref({})
  165. const trendData = ref({})
  166. const qualificationPieData = ref({})
  167. const qualificationBarData = ref({})
  168. // 计算属性:处理selectedDeptObject逻辑
  169. const selectedDeptType = computed(() => {
  170. return props.selectedDeptObject && props.selectedDeptObject?.deptType
  171. })
  172. // 计算属性:检查是否为STATION类型
  173. const isStationType = computed(() => {
  174. console.log('selectedDeptType.value', selectedDeptType.value)
  175. return selectedDeptType.value === 'STATION' || !selectedDeptType.value
  176. })
  177. const isDepartmentType = computed(() => {
  178. return selectedDeptType.value === 'MANAGER'
  179. })
  180. const isTeamsType = computed(() => {
  181. return selectedDeptType.value === 'TEAMS'
  182. })
  183. const isUserType = computed(() => {
  184. return selectedDeptType.value === 'USER'
  185. })
  186. // 计算属性:检查是否为非STATION类型
  187. const isNotUserType = computed(() => {
  188. return !selectedDeptType.value !== 'USER'
  189. })
  190. // 计算属性:动态生成资质等级分布描述第一部分(句号前)
  191. const qualificationPieDescriptionPart1 = computed(() => {
  192. if (!qualificationPieData.value || !qualificationPieData.value.data || !Array.isArray(qualificationPieData.value.data)) {
  193. return ''
  194. }
  195. const pieData = qualificationPieData.value.data
  196. // 1. 找出占比最高的等级
  197. const highestLevel = pieData.reduce((max, item) => {
  198. return (item.count || 0) > (max.count || 0) ? item : max
  199. }, { levelName: '高级', count: 0 })
  200. // 2. 计算总人数
  201. const totalCount = pieData.reduce((sum, item) => sum + (item.count || 0), 0)
  202. // 3. 计算占比
  203. const highestPercentage = totalCount > 0 ? ((highestLevel.count / totalCount) * 100).toFixed(2) : '0.0'
  204. let deptName = attendanceData.value && attendanceData.value.length > 0 ? attendanceData.value[attendanceData.value.length -
  205. 1]?.deptName : ''
  206. // 4. 生成第一部分描述文字
  207. return `${deptName}资质等级以"${highestLevel.levelName || '高级'}"为主(占比为${highestPercentage}%)`
  208. })
  209. // 计算属性:动态生成资质等级分布描述第二部分(句号后)
  210. const qualificationPieDescriptionPart2 = computed(() => {
  211. if (!qualificationPieData.value || !qualificationPieData.value.data || !Array.isArray(qualificationPieData.value.data)) {
  212. return ''
  213. }
  214. // 从资质柱状图数据中找出"一级"人员最多的科室
  215. let topDeptForLevel1 = ''
  216. let level1Count = 0
  217. let totalDeptCount = 0
  218. let allDeptNames = []
  219. if (qualificationBarData.value && Array.isArray(qualificationBarData.value)) {
  220. const barData = qualificationBarData.value
  221. // 找出"一级"人员最多的科室
  222. let maxLevel1Count = 0
  223. let maxDeptName = ''
  224. let totalCountForDept = 0
  225. barData.forEach(dept => {
  226. allDeptNames.push(dept.deptName)
  227. if (dept.levelCounts && Array.isArray(dept.levelCounts)) {
  228. const level1Data = dept.levelCounts.find(level => level.levelName === '高级')
  229. const deptTotalCount = dept.levelCounts.reduce((sum, level) => sum + (level.count || 0), 0)
  230. if (level1Data && level1Data.count > maxLevel1Count) {
  231. maxLevel1Count = level1Data.count
  232. maxDeptName = dept.deptName || ''
  233. totalCountForDept = deptTotalCount
  234. }
  235. }
  236. })
  237. topDeptForLevel1 = maxDeptName
  238. level1Count = maxLevel1Count
  239. totalDeptCount = totalCountForDept
  240. }
  241. // 生成第二部分描述文字
  242. return `全站资质等级为"高级"的人员集中在${topDeptForLevel1}(${level1Count}人)${topDeptForLevel1}的人员规模(共${totalDeptCount}人)高于${allDeptNames.filter(name => name !== topDeptForLevel1).join(', ')}`
  243. })
  244. // 处理query参数,当dateRangeQueryType为YEAR时添加yearOnYear: true
  245. const processQueryParams = (queryParams) => {
  246. const processedParams = { ...queryParams }
  247. if (processedParams.dateRangeQueryType === 'YEAR') {
  248. processedParams.yearOnYear = true
  249. } else {
  250. processedParams.chainRatio = true
  251. processedParams.yearOnYear = true
  252. }
  253. return processedParams
  254. }
  255. // 格式化比率显示
  256. const formatRate = (rate) => {
  257. if (rate === null || rate === undefined) return '0%'
  258. const numRate = parseFloat(rate)
  259. if (numRate > 0) {
  260. return `+${numRate.toFixed(2)}%`
  261. } else if (numRate < 0) {
  262. return `${numRate.toFixed(2)}%`
  263. } else {
  264. return '--%'
  265. }
  266. }
  267. // 获取趋势CSS类名
  268. const getTrendClass = (rate) => {
  269. if (rate === null || rate === undefined) return 'neutral'
  270. const numRate = parseFloat(rate)
  271. if (numRate > 0) {
  272. return 'up'
  273. } else if (numRate < 0) {
  274. return 'down'
  275. } else {
  276. return 'neutral'
  277. }
  278. }
  279. // 调用API获取勤务组织数据
  280. const fetchDutyOrganizationData = async (queryParams) => {
  281. try {
  282. // 处理query参数
  283. const processedParams = processQueryParams(queryParams)
  284. const selectedDept = props.selectedDeptObject
  285. const { deptType = "", id } = selectedDept ? selectedDept : { deptType: "", id: "" }
  286. delete processedParams.deptId
  287. let calculateParams = {
  288. ...(['TEAMS', 'DEPARTMENT', 'BRIGADE','MANAGER'].includes(deptType) ? { deptId: id } : {}),
  289. ...(deptType == 'USER' ? { userId: id } : {})
  290. }
  291. // 获取出勤人次分析数据
  292. const attendanceResponse = await getCalculate({ ...processedParams, ...calculateParams })
  293. console.log('出勤人次分析数据:', attendanceResponse)
  294. attendanceData.value = attendanceResponse || []
  295. // 获取出勤人次趋势数据
  296. const trendResponse = await getCalculateTrendData({ ...processedParams, ...calculateParams })
  297. console.log('出勤人次趋势数据:', trendResponse)
  298. trendData.value = trendResponse || []
  299. // 获取资质等级分布饼图数据(如果部门类型不是STATION)
  300. if (isNotUserType.value) {
  301. const pieResponse = await getQualificationPieChart({ ...processedParams, ...calculateParams })
  302. qualificationPieData.value = pieResponse.data || []
  303. } else {
  304. qualificationPieData.value = []
  305. }
  306. if (isStationType.value) {
  307. //获取资质等级分布柱状图数据
  308. const barResponse = await getQualificationBarChart({ ...processedParams, ...calculateParams })
  309. qualificationBarData.value = barResponse.data?.brigades || []
  310. } else if (isTeamsType.value) {
  311. // 获取资质等级分布柱状图数据
  312. const barResponse = await getQualificationBarChart({ ...processedParams, ...calculateParams })
  313. console.log('资质等级分布柱状图数据:', barResponse.data?.brigades)
  314. // 处理班组人员资质等级数据
  315. if (barResponse.data?.brigades && Array.isArray(barResponse.data.brigades)) {
  316. teamsQualificationData.value = barResponse.data.brigades
  317. } else {
  318. teamsQualificationData.value = []
  319. }
  320. } else if (isUserType.value) {
  321. // 获取资质等级分布柱状图数据
  322. const barResponse = await getQualificationBarChart({ ...processedParams, ...calculateParams })
  323. console.log('资质等级分布柱状图数据:', barResponse.data.brigades)
  324. user.value = barResponse?.data?.brigades[0]
  325. }
  326. setTimeout(() => {
  327. // 更新图表和统计信息
  328. updateChartsWithData()
  329. }, 0);
  330. } catch (error) {
  331. console.error('获取勤务组织数据失败:', error)
  332. }
  333. }
  334. // 监听queryForm参数变化,调用API获取数据
  335. watch(() => props.queryForm, (newQueryForm) => {
  336. // 只有当所有必要的查询参数都存在时才调用API
  337. if (newQueryForm.dateRangeQueryType && newQueryForm.year) {
  338. fetchDutyOrganizationData(newQueryForm)
  339. }
  340. }, { deep: true })
  341. // 饼图配置
  342. const pieOptions = {
  343. tooltip: {
  344. trigger: 'item',
  345. formatter: '{a} <br/>{b}: {c}人 ({d}%)'
  346. },
  347. legend: {
  348. orient: 'vertical',
  349. left: 'left',
  350. top: 'center',
  351. textStyle: {
  352. color: '#333',
  353. fontSize: 12
  354. }
  355. },
  356. color: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57'],
  357. series: [
  358. {
  359. name: '资质等级分布',
  360. type: 'pie',
  361. radius: '50%',
  362. center: ['60%', '50%'],
  363. avoidLabelOverlap: false,
  364. itemStyle: {
  365. borderRadius: 10,
  366. borderColor: '#fff',
  367. borderWidth: 2
  368. },
  369. label: {
  370. show: true,
  371. formatter: '{b}: {c}人 ({d}%)',
  372. fontSize: 12
  373. },
  374. emphasis: {
  375. label: {
  376. show: true,
  377. fontSize: 14,
  378. fontWeight: 'bold'
  379. },
  380. itemStyle: {
  381. shadowBlur: 10,
  382. shadowOffsetX: 0,
  383. shadowColor: 'rgba(0, 0, 0, 0.5)'
  384. }
  385. },
  386. labelLine: {
  387. show: true,
  388. length: 10,
  389. length2: 20
  390. },
  391. data: [
  392. ]
  393. }
  394. ]
  395. }
  396. // 资质趋势柱状图配置
  397. const trendBarOptions = {
  398. tooltip: {
  399. trigger: 'axis',
  400. axisPointer: {
  401. type: 'shadow'
  402. },
  403. formatter: function (params) {
  404. let result = `${params[0].axisValue}<br/>`
  405. params.forEach(param => {
  406. result += `${param.seriesName}: <span style="color:${param.color};font-weight:bold">${param.data}</span>人<br/>`
  407. })
  408. return result
  409. }
  410. },
  411. legend: {
  412. data: [],
  413. top: 0
  414. },
  415. grid: {
  416. left: '3%',
  417. right: '4%',
  418. bottom: '3%',
  419. top: '10%',
  420. containLabel: true
  421. },
  422. xAxis: {
  423. type: 'category',
  424. data: [],
  425. axisLine: {
  426. lineStyle: {
  427. color: '#999'
  428. }
  429. },
  430. axisLabel: {
  431. fontSize: 12
  432. }
  433. },
  434. yAxis: {
  435. type: 'value',
  436. name: '人数',
  437. axisLine: {
  438. lineStyle: {
  439. color: '#999'
  440. }
  441. },
  442. splitLine: {
  443. lineStyle: {
  444. color: '#f0f0f0'
  445. }
  446. }
  447. },
  448. series: [
  449. {
  450. name: '旅检一科',
  451. type: 'bar',
  452. barWidth: '10%',
  453. itemStyle: {
  454. color: '#FF6B6B'
  455. },
  456. label: {
  457. show: true,
  458. position: 'top',
  459. formatter: '{c}人'
  460. },
  461. data: []
  462. },
  463. {
  464. name: '旅检二科',
  465. type: 'bar',
  466. barWidth: '10%',
  467. itemStyle: {
  468. color: '#4ECDC4'
  469. },
  470. label: {
  471. show: true,
  472. position: 'top',
  473. formatter: '{c}人'
  474. },
  475. data: []
  476. },
  477. {
  478. name: '旅检三科',
  479. type: 'bar',
  480. barWidth: '10%',
  481. itemStyle: {
  482. color: '#FFD166'
  483. },
  484. label: {
  485. show: true,
  486. position: 'top',
  487. formatter: '{c}人'
  488. },
  489. data: []
  490. }
  491. ]
  492. }
  493. // 出勤人次柱状图配置
  494. const attendanceBarOptions = {
  495. tooltip: {
  496. trigger: 'axis',
  497. axisPointer: {
  498. type: 'shadow'
  499. },
  500. formatter: function (params) {
  501. let result = `${params[0].axisValue}<br/>`
  502. params.forEach(param => {
  503. result += `${param.seriesName}: <span style="color:${param.color};font-weight:bold">${param.data}</span>人<br/>`
  504. })
  505. return result
  506. }
  507. },
  508. legend: {
  509. data: ['安检一大队', '安检二大队', '安检三大队', '安检综合大队', '全站'],
  510. top: 0,
  511. show: true,
  512. },
  513. grid: {
  514. left: '3%',
  515. right: '4%',
  516. bottom: '0%',
  517. top: '80',
  518. containLabel: true
  519. },
  520. xAxis: {
  521. type: 'category',
  522. data: [],
  523. axisLine: {
  524. lineStyle: {
  525. color: '#999'
  526. }
  527. },
  528. axisLabel: {
  529. fontSize: 12
  530. }
  531. },
  532. yAxis: {
  533. type: 'value',
  534. name: '人数',
  535. axisLine: {
  536. lineStyle: {
  537. color: '#999'
  538. }
  539. },
  540. splitLine: {
  541. lineStyle: {
  542. color: '#f0f0f0'
  543. }
  544. }
  545. },
  546. series: [
  547. {
  548. name: '全站',
  549. type: 'bar',
  550. barWidth: '10%',
  551. itemStyle: {
  552. color: '#5470C6'
  553. },
  554. label: {
  555. show: true,
  556. position: 'top',
  557. formatter: '{c}人'
  558. },
  559. data: []
  560. },
  561. {
  562. name: '安检一大队',
  563. type: 'bar',
  564. barWidth: '10%',
  565. itemStyle: {
  566. color: '#FF6B6B'
  567. },
  568. label: {
  569. show: true,
  570. position: 'top',
  571. formatter: '{c}人'
  572. },
  573. data: []
  574. },
  575. {
  576. name: '安检二大队',
  577. type: 'bar',
  578. barWidth: '10%',
  579. itemStyle: {
  580. color: '#4ECDC4'
  581. },
  582. label: {
  583. show: true,
  584. position: 'top',
  585. formatter: '{c}人'
  586. },
  587. data: []
  588. },
  589. {
  590. name: '安检三大队',
  591. type: 'bar',
  592. barWidth: '10%',
  593. itemStyle: {
  594. color: '#FFD166'
  595. },
  596. label: {
  597. show: true,
  598. position: 'top',
  599. formatter: '{c}人'
  600. },
  601. data: []
  602. },
  603. {
  604. name: '安检综合大队',
  605. type: 'bar',
  606. barWidth: '10%',
  607. itemStyle: {
  608. color: '#FFD166'
  609. },
  610. label: {
  611. show: true,
  612. position: 'top',
  613. formatter: '{c}人'
  614. },
  615. data: []
  616. }
  617. ]
  618. }
  619. // 出勤人次柱状图配置
  620. const attendanceBarOtherOptions = {
  621. tooltip: {
  622. trigger: 'axis',
  623. axisPointer: {
  624. type: 'shadow'
  625. },
  626. formatter: function (params) {
  627. let result = `${params[0].axisValue}<br/>`
  628. params.forEach(param => {
  629. result += ` <span style="color:${param.color};font-weight:bold">${param.data}</span>人<br/>`
  630. })
  631. return result
  632. }
  633. },
  634. legend: {
  635. top: 0,
  636. show: true,
  637. },
  638. grid: {
  639. left: '3%',
  640. right: '4%',
  641. bottom: '0%',
  642. top: '80',
  643. containLabel: true
  644. },
  645. xAxis: {
  646. type: 'category',
  647. data: [],
  648. axisLine: {
  649. lineStyle: {
  650. color: '#999'
  651. }
  652. },
  653. axisLabel: {
  654. fontSize: 12
  655. }
  656. },
  657. yAxis: {
  658. type: 'value',
  659. name: '人数',
  660. axisLine: {
  661. lineStyle: {
  662. color: '#999'
  663. }
  664. },
  665. splitLine: {
  666. lineStyle: {
  667. color: '#f0f0f0'
  668. }
  669. }
  670. },
  671. series: [
  672. {
  673. name: '',
  674. type: 'bar',
  675. barWidth: '10%',
  676. itemStyle: {
  677. color: '#5470C6'
  678. },
  679. label: {
  680. show: true,
  681. position: 'top',
  682. formatter: '{c}人'
  683. },
  684. data: []
  685. },
  686. ]
  687. }
  688. // 初始化图表
  689. onMounted(() => {
  690. // 确保DOM完全渲染后再初始化图表
  691. const initCharts = () => {
  692. // 资质等级饼图
  693. if (pieChartRef.value && pieChartRef.value.offsetHeight > 0) {
  694. setPieOption(pieOptions)
  695. }
  696. // 资质趋势柱状图
  697. if (barChartRef.value && barChartRef.value.offsetHeight > 0) {
  698. setBarOption(trendBarOptions)
  699. }
  700. // 出勤人次柱状图
  701. if (attendanceBarChartRef.value && attendanceBarChartRef.value.offsetHeight > 0) {
  702. setAttendanceOption(attendanceBarOptions)
  703. }
  704. }
  705. // 延迟初始化,确保CSS已应用
  706. setTimeout(() => {
  707. initCharts()
  708. }, 100)
  709. })
  710. // 更新资质等级分布饼图数据
  711. const updateQualificationPieChart = () => {
  712. if (qualificationPieData.value && qualificationPieData.value.data) {
  713. const pieData = qualificationPieData.value.data
  714. // 转换数据格式,使用levelName作为名称,count作为值
  715. const formattedData = pieData.map(item => ({
  716. name: item.levelName || '未知等级',
  717. value: item.count || 0
  718. })).filter(item => item.value > 0)
  719. // 更新饼图数据
  720. pieOptions.series[0].data = formattedData
  721. // 重新设置图表选项
  722. setPieOption(pieOptions)
  723. console.log('资质等级分布饼图已更新:', formattedData)
  724. } else {
  725. // 无数据时清空图表
  726. pieOptions.series[0].data = []
  727. setPieOption(pieOptions)
  728. }
  729. }
  730. // 更新资质趋势柱状图数据
  731. const updateTrendBarChart = () => {
  732. if (qualificationBarData.value && Array.isArray(qualificationBarData.value)) {
  733. const barData = qualificationBarData.value
  734. // 提取所有唯一的等级名称(横坐标)
  735. const allLevelNames = []
  736. barData.forEach(dept => {
  737. if (dept.levelCounts && Array.isArray(dept.levelCounts)) {
  738. dept.levelCounts.forEach(level => {
  739. if (level.levelName && !allLevelNames.includes(level.levelName)) {
  740. allLevelNames.push(level.levelName)
  741. }
  742. })
  743. }
  744. })
  745. // 按等级顺序排序(一级、二级、三级、四级、五级)
  746. const levelOrder = ['一级', '二级', '三级', '四级', '五级']
  747. const sortedLevelNames = allLevelNames.sort((a, b) => {
  748. return levelOrder.indexOf(a) - levelOrder.indexOf(b)
  749. })
  750. // 为每个科室创建数据系列
  751. const seriesData = []
  752. const colors = ['#FF6B6B', '#4ECDC4', '#FFD166', '#9B59B6', '#3498DB']
  753. barData.forEach((dept, index) => {
  754. if (dept.deptName && dept.levelCounts) {
  755. // 为每个等级查找对应的数量
  756. const levelCounts = sortedLevelNames.map(levelName => {
  757. const levelData = dept.levelCounts.find(level => level.levelName === levelName)
  758. return levelData ? levelData.count || 0 : 0
  759. })
  760. seriesData.push({
  761. name: dept.deptName,
  762. type: 'bar',
  763. barWidth: '15%',
  764. itemStyle: {
  765. color: colors[index % colors.length]
  766. },
  767. label: {
  768. show: true,
  769. position: 'top',
  770. formatter: function (params) {
  771. return params.value > 0 ? params.value + '人' : '';
  772. }
  773. },
  774. data: levelCounts
  775. })
  776. }
  777. })
  778. // 更新图表配置
  779. trendBarOptions.xAxis.data = sortedLevelNames
  780. trendBarOptions.legend.data = barData.map(dept => dept.deptName).filter(name => name)
  781. trendBarOptions.series = seriesData
  782. // 重新设置图表选项
  783. setBarOption(trendBarOptions)
  784. console.log('资质趋势柱状图已更新:', {
  785. xAxis: sortedLevelNames,
  786. series: seriesData
  787. })
  788. } else {
  789. // 无数据时清空图表
  790. trendBarOptions.xAxis.data = []
  791. trendBarOptions.legend.data = []
  792. trendBarOptions.series = []
  793. setBarOption(trendBarOptions)
  794. }
  795. }
  796. //非站长走这个逻辑
  797. const updateAttendanceBarOtherChart = () => {
  798. if (trendData.value && Array.isArray(trendData.value)) {
  799. const trendList = trendData.value
  800. // 提取横坐标数据(timeLabel字段)
  801. const xAxisData = trendList.map(item => item.timeLabel || '未知时间')
  802. // 提取各科室数据
  803. const allData = trendList.map(item => item.overall || 0)
  804. // 更新图表配置
  805. attendanceBarOtherOptions.xAxis.data = xAxisData
  806. attendanceBarOtherOptions.series[0].data = allData
  807. attendanceBarOtherOptions.legend.show = !!isStationType.value
  808. // 重新设置图表选项
  809. setAttendanceOption(attendanceBarOtherOptions,true)
  810. } else {
  811. // 无数据时清空图表
  812. attendanceBarOtherOptions.xAxis.data = []
  813. attendanceBarOtherOptions.series[0].data = []
  814. attendanceBarOtherOptions.legend.show = !!isStationType.value
  815. setAttendanceOption(attendanceBarOtherOptions,true)
  816. }
  817. }
  818. const updateAttendanceBarChart = () => {
  819. if (trendData.value && Array.isArray(trendData.value)) {
  820. const trendList = trendData.value
  821. // 提取横坐标数据(timeLabel字段)
  822. const xAxisData = trendList.map(item => item.timeLabel || '未知时间')
  823. // 提取各科室数据
  824. const allData = trendList.map(item => item.overall || 0)
  825. const dept1Data = trendList.map(item => item.data1 || 0)
  826. const dept2Data = trendList.map(item => item.data2 || 0)
  827. const dept3Data = trendList.map(item => item.data3 || 0)
  828. const dept4Data = trendList.map(item => item.data4 || 0)
  829. // 更新图表配置
  830. attendanceBarOptions.xAxis.data = xAxisData
  831. attendanceBarOptions.series[0].data = allData
  832. attendanceBarOptions.series[1].data = dept1Data
  833. attendanceBarOptions.series[2].data = dept2Data
  834. attendanceBarOptions.series[3].data = dept3Data
  835. attendanceBarOptions.series[4].data = dept4Data
  836. attendanceBarOptions.legend.show = !!isStationType.value
  837. // 重新设置图表选项
  838. setAttendanceOption(attendanceBarOptions)
  839. } else {
  840. // 无数据时清空图表
  841. attendanceBarOptions.xAxis.data = []
  842. attendanceBarOptions.series[0].data = []
  843. attendanceBarOptions.series[1].data = []
  844. attendanceBarOptions.series[2].data = []
  845. attendanceBarOptions.series[3].data = []
  846. attendanceBarOptions.legend.show = !!isStationType.value
  847. setAttendanceOption(attendanceBarOptions)
  848. }
  849. }
  850. // 根据API数据更新图表和统计信息
  851. const updateChartsWithData = () => {
  852. // 更新出勤人次柱状图
  853. if (isStationType.value) {
  854. updateAttendanceBarChart()
  855. } else{
  856. updateAttendanceBarOtherChart()
  857. }
  858. // 更新资质等级分布饼图
  859. updateQualificationPieChart()
  860. // 更新资质趋势柱状图
  861. updateTrendBarChart()
  862. console.log('数据已更新,图表已刷新')
  863. }
  864. // 组件卸载时销毁图表
  865. onUnmounted(() => {
  866. disposePie()
  867. disposeBar()
  868. disposeAttendance()
  869. })
  870. </script>
  871. <style scoped>
  872. .duty-organization {
  873. width: 100%;
  874. }
  875. /* 勤务组织标题 */
  876. .section-title {
  877. margin: 14px 0 14px 0;
  878. text-align: left;
  879. }
  880. .section-title h2 {
  881. margin: 0;
  882. font-size: 18px;
  883. font-weight: 600;
  884. color: #333;
  885. }
  886. /* 横向布局内容区域 */
  887. .content-layout {
  888. display: flex;
  889. gap: 20px;
  890. /* margin-bottom: 30px; */
  891. }
  892. .left-panel {
  893. flex: 1 1 40%;
  894. min-width: 300px;
  895. background: white;
  896. border-radius: 8px;
  897. padding: 20px;
  898. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  899. }
  900. .right-panel {
  901. flex: 1 1 60%;
  902. min-width: 400px;
  903. background: white;
  904. border-radius: 8px;
  905. padding: 20px;
  906. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  907. }
  908. .right-panel-ability {
  909. flex: 1 1 30%;
  910. min-width: 400px;
  911. background: white;
  912. border-radius: 8px;
  913. padding: 20px;
  914. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  915. }
  916. .panel-header {
  917. margin-bottom: 20px;
  918. padding-bottom: 0;
  919. }
  920. .panel-header h3 {
  921. font-size: 18px;
  922. font-weight: 600;
  923. color: #333;
  924. margin: 0;
  925. }
  926. /* 统计卡片 */
  927. .stat-card {
  928. background: #F8F8F8;
  929. border: 1px dashed #CDCCCC;
  930. border-radius: 6px;
  931. padding: 7px 10px;
  932. margin-bottom: 20px;
  933. text-align: left;
  934. }
  935. .stat-content {
  936. font-size: 13px;
  937. color: #666;
  938. line-height: 1.5;
  939. }
  940. .stat-number {
  941. font-weight: 600;
  942. color: #557DDB;
  943. font-size: 16px;
  944. }
  945. .stat-trend {
  946. font-weight: 500;
  947. }
  948. .stat-trend.up {
  949. color: #67C23A;
  950. }
  951. .stat-trend.down {
  952. color: #F56C6C;
  953. }
  954. /* 科室数据卡片 */
  955. .dept-cards {
  956. display: flex;
  957. gap: 16px;
  958. margin-bottom: 20px;
  959. }
  960. .dept-card {
  961. flex: 1;
  962. background: #F8F8F8;
  963. border-radius: 6px;
  964. padding: 16px;
  965. display: flex;
  966. flex-direction: column;
  967. align-items: flex-start;
  968. text-align: left;
  969. }
  970. .dept-name {
  971. font-size: 16px;
  972. font-weight: 600;
  973. color: #333;
  974. margin-bottom: 8px;
  975. }
  976. .dept-count {
  977. font-size: 13px;
  978. margin-bottom: 8px;
  979. span {
  980. font-weight: 600;
  981. font-size: 22px;
  982. }
  983. }
  984. .dept-trend {
  985. font-size: 14px;
  986. margin-bottom: 4px;
  987. display: flex;
  988. align-items: center;
  989. }
  990. .dept-trend:last-child {
  991. margin-bottom: 0;
  992. &>div:first-child {
  993. margin-right: 8px;
  994. }
  995. }
  996. .dept-trend {
  997. .down {
  998. color: #67C23A;
  999. }
  1000. .up {
  1001. color: #F56C6C;
  1002. }
  1003. .neutral {
  1004. color: #909399;
  1005. }
  1006. }
  1007. /* 柱状图区域 */
  1008. .chart-container {
  1009. /* background: #F8F8F8;
  1010. border-radius: 6px;
  1011. padding: 20px; */
  1012. min-height: 200px;
  1013. height: 300px;
  1014. position: relative;
  1015. box-sizing: border-box;
  1016. overflow: hidden;
  1017. display: flex;
  1018. align-items: center;
  1019. justify-content: center;
  1020. }
  1021. .chart-placeholder {
  1022. text-align: center;
  1023. color: #999;
  1024. }
  1025. .chart-placeholder p {
  1026. margin: 0;
  1027. font-size: 16px;
  1028. }
  1029. /* 右侧资质等级分布内容 */
  1030. .qualification-content {
  1031. display: flex;
  1032. gap: 20px;
  1033. height: 500px;
  1034. }
  1035. .qualification-left,
  1036. .qualification-right {
  1037. flex: 1;
  1038. display: flex;
  1039. flex-direction: column;
  1040. gap: 20px;
  1041. }
  1042. /* 资质等级卡片 */
  1043. .qualification-card {
  1044. background: #F8F8F8;
  1045. border-radius: 6px;
  1046. padding: 16px;
  1047. flex: 0 0 auto;
  1048. }
  1049. .card-content {
  1050. display: flex;
  1051. flex-direction: column;
  1052. gap: 12px;
  1053. }
  1054. .card-title {
  1055. font-size: 16px;
  1056. font-weight: 600;
  1057. color: #333;
  1058. margin: 0;
  1059. }
  1060. .card-stats {
  1061. display: flex;
  1062. flex-direction: column;
  1063. gap: 8px;
  1064. }
  1065. .stat-item {
  1066. display: flex;
  1067. justify-content: space-between;
  1068. align-items: center;
  1069. font-size: 14px;
  1070. }
  1071. .stat-label {
  1072. color: #666;
  1073. flex: 1;
  1074. }
  1075. .stat-value {
  1076. font-weight: 600;
  1077. color: #333;
  1078. margin-right: 8px;
  1079. }
  1080. .stat-value.up {
  1081. color: #67C23A;
  1082. }
  1083. .stat-value.down {
  1084. color: #F56C6C;
  1085. }
  1086. .stat-percent {
  1087. color: #999;
  1088. font-size: 12px;
  1089. }
  1090. /* 资质等级分布图表容器 */
  1091. .qualification-left .chart-container,
  1092. .qualification-right .chart-container {
  1093. flex: 1;
  1094. min-height: 200px;
  1095. }
  1096. /* ECharts图表样式 */
  1097. .echarts-chart {
  1098. width: 100% !important;
  1099. height: 100% !important;
  1100. min-height: 200px;
  1101. display: block;
  1102. }
  1103. /* 响应式设计 */
  1104. @media (max-width: 768px) {
  1105. .content-layout {
  1106. flex-direction: column;
  1107. }
  1108. .left-panel,
  1109. .right-panel {
  1110. flex: 1;
  1111. }
  1112. }
  1113. </style>