HomePageOverview.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  1. <template>
  2. <ChartsContainer title="整体情况">
  3. <template #title>
  4. <div class="download-section">
  5. <el-button type="primary" size="small" @click="handleDownloadReport" :loading="downloadLoading">
  6. 下载整体报表
  7. </el-button>
  8. </div>
  9. </template>
  10. <div class="overview-content">
  11. <!-- 选择区域 -->
  12. <div class="selection-section">
  13. <div class="selection-row" v-for="(row, rowIndex) in selectionRows" :key="rowIndex">
  14. <div v-for="(item, index) in row" :key="index" class="selection-item">
  15. <CustomStyleSelect type="tree" :propsConfig="{ value: 'id', showPrefix: false }" v-model="item.selectedId"
  16. :options="departments" @change="handleSelectionChange(rowIndex * 2 + index, $event)" />
  17. </div>
  18. </div>
  19. </div>
  20. <!-- 雷达图 -->
  21. <div class="radar-chart" ref="radarChartRef"></div>
  22. <!-- 遍历展示的数据 -->
  23. <div class="data-list" v-if="dataItems.length > 0">
  24. <div class="data-item" v-for="(item, index) in dataItems" :key="index">
  25. <div class="data-title" :style="{ color: '#70D3EE' }">{{ item.title }}</div>
  26. <div class="data-values">
  27. <div class="value-label" v-for="(data, dataIndex) in item.list" :key="dataIndex">
  28. <div class="value">{{ data.value }}</div>
  29. <div class="label">{{ data.label }}</div>
  30. </div>
  31. </div>
  32. </div>
  33. </div>
  34. </div>
  35. </ChartsContainer>
  36. </template>
  37. <script setup>
  38. import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
  39. import * as echarts from 'echarts'
  40. import ChartsContainer from '../ChartsContainer.vue'
  41. import CustomStyleSelect from './CustomStyleSelect.vue'
  42. import { getDeptUserTree } from '@/api/item/items'
  43. import { getHomePageDetail, getHomeReportDownload } from '@/api/largeScreen/largeScreen'
  44. const radarChartRef = ref(null)
  45. let radarChart = null
  46. // 下载相关变量
  47. const downloadLoading = ref(false)
  48. // 定义props,从父组件接收数据
  49. const props = defineProps({
  50. timeRange: {
  51. type: String,
  52. default: 'year'
  53. },
  54. startDate: {
  55. type: String,
  56. default: ''
  57. },
  58. endDate: {
  59. type: String,
  60. default: ''
  61. },
  62. selectedRole: {
  63. type: String,
  64. default: 'individual'
  65. }
  66. })
  67. // 遍历展示的数据
  68. const dataItems = ref([])
  69. // 选择相关数据
  70. const selectionItems = ref([
  71. ])
  72. // 计算属性,将选择项分成两行,每行两个
  73. const selectionRows = computed(() => {
  74. const rows = []
  75. for (let i = 0; i < selectionItems.value.length; i += 2) {
  76. rows.push(selectionItems.value.slice(i, i + 2))
  77. }
  78. return rows
  79. })
  80. const departments = ref([])
  81. // 计算日期范围(移植自HomePage.vue)
  82. const calculateDateRange = (timeRange, customStartDate, customEndDate) => {
  83. const today = new Date()
  84. const yesterday = new Date(today)
  85. yesterday.setDate(today.getDate() - 1)
  86. let startDate = new Date(yesterday)
  87. let endDate = new Date(yesterday)
  88. switch (timeRange) {
  89. case 'week':
  90. startDate.setDate(yesterday.getDate() - 6)
  91. break
  92. case 'month':
  93. startDate.setDate(yesterday.getDate() - 29)
  94. break
  95. case 'quarter':
  96. startDate.setDate(yesterday.getDate() - 89)
  97. break
  98. case 'halfYear':
  99. startDate.setDate(yesterday.getDate() - 179)
  100. break
  101. case 'year':
  102. startDate.setDate(yesterday.getDate() - 364)
  103. break
  104. case 'custom':
  105. if (customStartDate && customEndDate) {
  106. startDate = new Date(customStartDate)
  107. endDate = new Date(customEndDate)
  108. }
  109. break
  110. default:
  111. startDate.setDate(yesterday.getDate() - 364)
  112. }
  113. return {
  114. startDate: formatDateForInput(startDate),
  115. endDate: formatDateForInput(endDate)
  116. }
  117. }
  118. // 格式化日期为输入框格式
  119. const formatDateForInput = (date) => {
  120. const year = date.getFullYear()
  121. const month = String(date.getMonth() + 1).padStart(2, '0')
  122. const day = String(date.getDate()).padStart(2, '0')
  123. return `${year}-${month}-${day}`
  124. }
  125. // 监听时间参数变化
  126. watch(() => [props.timeRange, props.startDate, props.endDate, props.selectedRole], () => {
  127. // 当时间参数变化时,重新获取数据
  128. if (selectionItems.value.some(item => item.selectedId)) {
  129. fetchHomePageDetailData()
  130. }
  131. }, { immediate: true })
  132. // 更新雷达图数据
  133. const updateChartWithData = (data) => {
  134. if (!radarChartRef.value) {
  135. console.warn('雷达图容器未找到')
  136. return
  137. }
  138. if (!radarChart) {
  139. radarChart = echarts.init(radarChartRef.value)
  140. }
  141. // 根据接口数据更新雷达图
  142. if (data && data.length > 0) {
  143. // 使用Graph后缀的数据来显示雷达图
  144. const seriesData = data.map(item => ({
  145. name: item.name,
  146. value: [
  147. item.seizureCountGraph || 0,
  148. item.workingHoursGraph || 0,
  149. item.checkPassRateGraph || 0,
  150. item.answersAccuracyGraph || 0,
  151. item.learningGrowthScoreGraph || 0
  152. ]
  153. }))
  154. // 计算每个指标的最大值,用于雷达图的max值
  155. const indicators = [
  156. { name: '查获能力', max: Math.max(...seriesData.map(d => d.value[0])) },
  157. { name: '在岗时长', max: Math.max(...seriesData.map(d => d.value[1])) },
  158. { name: '巡检合格率', max: Math.max(...seriesData.map(d => d.value[2])) },
  159. { name: '抽问抽答', max: Math.max(...seriesData.map(d => d.value[3])) },
  160. { name: '培训答题', max: Math.max(...seriesData.map(d => d.value[4])) }
  161. ]
  162. const option = {
  163. legend: {
  164. type: 'scroll',
  165. orient: 'horizontal',
  166. bottom: 10,
  167. textStyle: {
  168. fontSize: 12,
  169. color: '#fff'
  170. },
  171. itemWidth: 12,
  172. itemHeight: 12,
  173. data: seriesData.map(item => item.name)
  174. },
  175. radar: {
  176. shape: 'circle',
  177. indicator: indicators,
  178. splitNumber: 4,
  179. center: ['50%', '45%'],
  180. radius: '65%',
  181. axisName: {
  182. color: '#fff',
  183. fontSize: 12
  184. },
  185. splitLine: {
  186. lineStyle: {
  187. color: ['rgba(255, 255, 255, 0.3)', 'rgba(255, 255, 255, 0.2)', 'rgba(255, 255, 255, 0.1)']
  188. }
  189. },
  190. splitArea: {
  191. show: true,
  192. areaStyle: {
  193. color: ['rgba(255, 255, 255, 0.1)', 'rgba(255, 255, 255, 0.05)']
  194. }
  195. },
  196. axisLine: {
  197. lineStyle: {
  198. color: 'rgba(255, 255, 255, 0.5)'
  199. }
  200. }
  201. },
  202. series: [
  203. {
  204. name: '能力对比',
  205. type: 'radar',
  206. data: seriesData.map((item, index) => ({
  207. value: item.value,
  208. name: item.name,
  209. areaStyle: {
  210. color: getChartColor(index, 0.3)
  211. },
  212. lineStyle: {
  213. color: getChartColor(index),
  214. width: 2
  215. },
  216. itemStyle: {
  217. color: getChartColor(index)
  218. }
  219. }))
  220. }
  221. ],
  222. tooltip: {
  223. show: false,
  224. // trigger: 'item',
  225. // formatter: function (params) {
  226. // const data = params.data
  227. // const name = params.name
  228. // const value = params.value
  229. // let tooltipHtml = `<div style="font-weight: bold; margin-bottom: 8px;">${name}</div>`
  230. // const labels = ['查获数量', '在岗时长', '巡检合格率', '抽问抽答', '培训答题']
  231. // value.forEach((val, index) => {
  232. // tooltipHtml += `<div style="display: flex; justify-content: space-between; margin: 4px 0;">
  233. // <span>${labels[index]}:</span>
  234. // <span style="font-weight: bold;">${val}</span>
  235. // </div>`
  236. // })
  237. // return tooltipHtml
  238. // }
  239. }
  240. }
  241. radarChart.setOption(option)
  242. }
  243. }
  244. // 获取图表颜色
  245. const getChartColor = (index, opacity = 1) => {
  246. const colors = ['#8FA5EC', '#FF9F7F', '#6ECEB2', '#FFD700', '#BA55D3']
  247. const color = colors[index % colors.length]
  248. if (opacity < 1) {
  249. // 转换为RGBA格式
  250. const r = parseInt(color.slice(1, 3), 16)
  251. const g = parseInt(color.slice(3, 5), 16)
  252. const b = parseInt(color.slice(5, 7), 16)
  253. return `rgba(${r}, ${g}, ${b}, ${opacity})`
  254. }
  255. return color
  256. }
  257. // 获取部门和人员树数据
  258. const fetchDepartments = async () => {
  259. try {
  260. const res = await getDeptUserTree()
  261. departments.value = res.data
  262. // 数据加载完成后,查找deptType为BRIGADE的对象
  263. if (res.data && res.data.length > 0) {
  264. // 查找deptType为BRIGADE的对象
  265. const findBrigadeItems = (items) => {
  266. const brigadeItems = []
  267. for (const item of items) {
  268. if (item.deptType === 'BRIGADE') {
  269. brigadeItems.push(item)
  270. }
  271. if (item.children && item.children.length > 0) {
  272. const childBrigadeItems = findBrigadeItems(item.children)
  273. brigadeItems.push(...childBrigadeItems)
  274. }
  275. }
  276. return brigadeItems
  277. }
  278. const brigadeItems = findBrigadeItems(res.data)
  279. // 设置前4个BRIGADE对象作为默认选项
  280. if (brigadeItems.length > 0) {
  281. for (let i = 0; i < Math.min(brigadeItems.length, 4); i++) {
  282. selectionItems.value[i] = {
  283. ...brigadeItems[i],
  284. selectedId: brigadeItems[i].id,
  285. selectedName: brigadeItems[i].label || brigadeItems[i].name
  286. }
  287. }
  288. // 触发数据请求
  289. fetchHomePageDetailData()
  290. }
  291. }
  292. } catch (error) {
  293. console.error('获取部门和人员树失败:', error)
  294. }
  295. }
  296. // 选择变化处理
  297. const handleSelectionChange = (index, selectedItem) => {
  298. console.log(selectedItem, "selectedItem")
  299. if (selectedItem && selectedItem.value) {
  300. // 查找选中的部门或人员信息
  301. const findSelectedItem = (items, targetId) => {
  302. for (const item of items) {
  303. if (item.id === targetId) {
  304. return item
  305. }
  306. if (item.children && item.children.length > 0) {
  307. const found = findSelectedItem(item.children, targetId)
  308. if (found) return found
  309. }
  310. }
  311. return null
  312. }
  313. const itemInfo = findSelectedItem(departments.value, selectedItem.value)
  314. if (itemInfo) {
  315. selectionItems.value[index] = {
  316. ...selectionItems.value[index],
  317. selectedId: selectedItem.value,
  318. selectedName: itemInfo.label || itemInfo.name
  319. }
  320. }
  321. } else {
  322. // 清空选择
  323. selectionItems.value[index] = {
  324. ...selectionItems.value[index],
  325. selectedId: '',
  326. selectedName: ''
  327. }
  328. }
  329. // 触发数据请求
  330. fetchHomePageDetailData()
  331. }
  332. // 获取首页详情数据
  333. const fetchHomePageDetailData = async () => {
  334. // 构建参数数组
  335. const params = []
  336. // 计算开始和结束日期
  337. let startDate = props.startDate
  338. let endDate = props.endDate
  339. // 如果使用预设时间范围,计算日期
  340. if (props.timeRange !== 'custom' && (!startDate || !endDate)) {
  341. const dateRange = calculateDateRange(props.timeRange)
  342. startDate = dateRange.startDate
  343. endDate = dateRange.endDate
  344. }
  345. for (const item of selectionItems.value) {
  346. if (item.selectedId) {
  347. // 查找选中的项目类型
  348. const findItemType = (items, targetId) => {
  349. for (const item of items) {
  350. if (item.id === targetId) {
  351. return item.nodeType === 'user' ? 'USER' : 'DEPT'
  352. }
  353. if (item.children && item.children.length > 0) {
  354. const type = findItemType(item.children, targetId)
  355. if (type) return type
  356. }
  357. }
  358. }
  359. const type = findItemType(departments.value, item.selectedId)
  360. params.push({
  361. startDate: startDate,
  362. endDate: endDate,
  363. type: type,
  364. id: item.selectedId,
  365. name: item.selectedName,
  366. dataSource: props.selectedRole
  367. })
  368. }
  369. }
  370. console.log(params)
  371. if (params.length > 0) {
  372. try {
  373. const res = await getHomePageDetail(params)
  374. console.log('首页详情数据:', res)
  375. // 处理响应数据,更新图表和数据显示
  376. handleHomePageDetailResponse(res)
  377. } catch (error) {
  378. console.error('获取首页详情数据失败:', error)
  379. }
  380. } else {
  381. // 如果没有选择任何项,清空显示的数据
  382. dataItems.value = []
  383. if (radarChart) {
  384. radarChart.clear()
  385. }
  386. }
  387. }
  388. // 处理首页详情响应数据
  389. const handleHomePageDetailResponse = (res) => {
  390. if (res.data && Array.isArray(res.data)) {
  391. // 更新数据展示
  392. dataItems.value = res.data.map(item => {
  393. return {
  394. title: item.name + "数据明细",
  395. list: [
  396. { label: '人均查获数量', value: item.seizureCount || 0 },
  397. { label: '人均在岗时长', value: item.workingHours || 0 },
  398. { label: '巡检合格率', value: `${((item.checkPassRate * 100) || 0).toFixed(2)}%` },
  399. { label: '抽问抽答正确率', value: `${((item.answersAccuracy * 100) || 0).toFixed(2)}%` },
  400. { label: '培训答题平均分', value: item.learningGrowthScore || 0 }
  401. ]
  402. }
  403. })
  404. // 更新雷达图数据
  405. updateChartWithData(res.data)
  406. }
  407. }
  408. const handleResize = () => {
  409. if (radarChart) {
  410. radarChart.resize()
  411. }
  412. }
  413. onMounted(() => {
  414. window.addEventListener('resize', handleResize)
  415. // 获取部门和人员树数据
  416. fetchDepartments()
  417. })
  418. onUnmounted(() => {
  419. if (radarChart) {
  420. radarChart.dispose()
  421. }
  422. window.removeEventListener('resize', handleResize)
  423. })
  424. // 下载整体报表
  425. const handleDownloadReport = async () => {
  426. try {
  427. downloadLoading.value = true
  428. // 构建下载参数
  429. const downloadParams = {
  430. startDate: props.startDate,
  431. endDate: props.endDate,
  432. }
  433. // 调用下载接口
  434. const response = await getHomeReportDownload(downloadParams)
  435. // 处理下载响应 - 使用项目标准的blob处理方式
  436. if (response) {
  437. // 生成文件名
  438. const fileName = `整体报表_${new Date().toISOString().slice(0, 10)}.xlsx`
  439. // 使用file-saver的saveAs函数下载文件
  440. const { saveAs } = await import('file-saver')
  441. saveAs(response, fileName)
  442. }
  443. } catch (error) {
  444. console.error('下载报表失败:', error)
  445. // 这里可以添加错误提示
  446. } finally {
  447. downloadLoading.value = false
  448. }
  449. }
  450. </script>
  451. <style lang="scss" scoped>
  452. .download-section {
  453. display: flex;
  454. justify-content: flex-end;
  455. padding: 0 10px;
  456. .el-button {
  457. background: linear-gradient(135deg, #1CB6FF 0%, #0A8CD9 100%);
  458. border: none;
  459. border-radius: 4px;
  460. font-size: 12px;
  461. padding: 6px 12px;
  462. &:hover {
  463. background: linear-gradient(135deg, #0A8CD9 0%, #1CB6FF 100%);
  464. }
  465. }
  466. }
  467. .overview-content {
  468. height: 100%;
  469. display: flex;
  470. flex-direction: column;
  471. gap: 15px;
  472. .selection-section {
  473. display: flex;
  474. flex-direction: column;
  475. gap: 10px;
  476. margin-bottom: 15px;
  477. .selection-row {
  478. display: flex;
  479. gap: 15px;
  480. .selection-item {
  481. flex: 1;
  482. display: flex;
  483. flex-direction: column;
  484. gap: 8px;
  485. .selection-label {
  486. font-size: 14px;
  487. color: #70D3EE;
  488. font-weight: bold;
  489. text-align: center;
  490. }
  491. :deep(.custom-select) {
  492. .el-input__wrapper {
  493. background: #0C4F7A;
  494. border: none;
  495. box-shadow: none;
  496. .el-input__inner {
  497. color: #fff;
  498. font-size: 12px;
  499. }
  500. }
  501. }
  502. }
  503. }
  504. }
  505. .radar-chart {
  506. height: 350px;
  507. width: 100%;
  508. display: flex;
  509. justify-content: center;
  510. align-items: center;
  511. }
  512. .data-list {
  513. flex: 1;
  514. display: flex;
  515. flex-direction: column;
  516. gap: 8px;
  517. overflow-y: auto;
  518. .data-item {
  519. background: rgba(255, 255, 255, 0.05);
  520. border-radius: 6px;
  521. padding: 11px 25px;
  522. background: url('../../../../../assets/images/cardBg.png') no-repeat !important;
  523. background-size: 100% 105% !important;
  524. .data-title {
  525. font-size: 16px;
  526. font-weight: bold;
  527. margin-bottom: 10px;
  528. color: #70D3EE;
  529. }
  530. .data-values {
  531. display: flex;
  532. justify-content: space-around;
  533. .value-label {
  534. display: flex;
  535. flex-direction: column;
  536. align-items: center;
  537. .value {
  538. font-size: 17px;
  539. // font-weight: bold;
  540. color: #70D3EE;
  541. margin-bottom: 2px;
  542. }
  543. .label {
  544. font-size: 13px;
  545. color: rgba(255, 255, 255, 0.7);
  546. }
  547. }
  548. }
  549. }
  550. }
  551. }
  552. // /* 响应式设计 */
  553. // @media (max-width: 1280px) {
  554. // .overview-content {
  555. // .data-list {
  556. // flex-wrap: wrap;
  557. // .data-item {
  558. // flex: 0 0 calc(50% - 5px);
  559. // min-width: 120px;
  560. // }
  561. // }
  562. // }
  563. // }
  564. // @media (max-width: 768px) {
  565. // .overview-content {
  566. // .data-list {
  567. // .data-item {
  568. // flex: 0 0 100%;
  569. // }
  570. // }
  571. // }
  572. // }</style>