DailyStatistics.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868
  1. <script setup lang="ts">
  2. import dayjs from 'dayjs'
  3. import type { ECharts } from 'echarts/core'
  4. import { DataZoomComponent } from 'echarts/components'
  5. import {
  6. IotRhDailyReportApi,
  7. type IotRhDailyReportTotalWorkloadVO
  8. } from '@/api/pms/iotrhdailyreport'
  9. import { useDebounceFn } from '@vueuse/core'
  10. import CountTo from '@/components/count-to1.vue'
  11. import echarts from '@/plugins/echarts'
  12. import UnfilledReportDialog from '../UnfilledReportDialog.vue'
  13. import { Motion, AnimatePresence } from 'motion-v'
  14. import download from '@/utils/download'
  15. import { useTableComponents } from '@/components/ZmTable/useTableComponents'
  16. echarts.use([DataZoomComponent])
  17. interface Query {
  18. pageNo: number
  19. pageSize: number
  20. deptId: number
  21. contractName?: string
  22. taskName?: string
  23. createTime: string[]
  24. }
  25. const props = defineProps<{
  26. query: Query
  27. deptName: string
  28. refreshKey: number
  29. }>()
  30. const totalWorkKeys: [string, string, string, string, number][] = [
  31. ['totalGasInjection', '万方', '累计注气量', 'i-material-symbols:cloud-outline text-sky', 2],
  32. [
  33. 'totalWaterInjection',
  34. '万方',
  35. '累计注水量',
  36. 'i-material-symbols:water-drop-outline-rounded text-sky',
  37. 2
  38. ],
  39. [
  40. 'utilizationRate',
  41. '%',
  42. '设备利用率',
  43. 'i-material-symbols:check-circle-outline-rounded text-emerald',
  44. 0
  45. ],
  46. [
  47. 'totalPowerConsumption',
  48. 'MWh',
  49. '累计用电量',
  50. 'i-material-symbols:electric-bolt-outline-rounded text-sky',
  51. 2
  52. ],
  53. [
  54. 'totalFuelConsumption',
  55. '升',
  56. '累计油耗',
  57. 'i-material-symbols:directions-car-outline-rounded text-sky',
  58. 2
  59. ],
  60. ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky', 0],
  61. [
  62. 'alreadyReported',
  63. '个',
  64. '已填报',
  65. 'i-material-symbols:check-circle-outline-rounded text-emerald',
  66. 0
  67. ],
  68. ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose', 0]
  69. ]
  70. const totalWork = ref({
  71. totalCount: 0,
  72. alreadyReported: 0,
  73. notReported: 0,
  74. totalFuelConsumption: 0,
  75. totalPowerConsumption: 0,
  76. totalWaterInjection: 0,
  77. totalGasInjection: 0,
  78. utilizationRate: 0
  79. })
  80. const totalLoading = ref(false)
  81. const totalWorkloadDetail = ref({
  82. totalN2GasInjection: 0,
  83. totalNaturalGasInjection: 0
  84. })
  85. const formatGasInjectionTooltipValue = (value?: number | null) => ((value || 0) / 10000).toFixed(4)
  86. const getTotal = useDebounceFn(async () => {
  87. totalLoading.value = true
  88. const { pageNo, pageSize, ...other } = props.query
  89. try {
  90. let res1: any[]
  91. if (props.query.createTime && props.query.createTime.length === 2) {
  92. res1 = await IotRhDailyReportApi.rhDailyReportStatistics({
  93. createTime: props.query.createTime,
  94. deptId: props.query.deptId
  95. })
  96. totalWork.value.totalCount = res1[0].count
  97. totalWork.value.alreadyReported = res1[1].count
  98. totalWork.value.notReported = res1[2].count
  99. }
  100. const res2: IotRhDailyReportTotalWorkloadVO = await IotRhDailyReportApi.totalWorkload(other)
  101. totalWorkloadDetail.value = {
  102. totalN2GasInjection: res2.totalN2GasInjection || 0,
  103. totalNaturalGasInjection: res2.totalNaturalGasInjection || 0
  104. }
  105. totalWork.value = {
  106. ...totalWork.value,
  107. ...res2,
  108. totalPowerConsumption: (res2.totalPowerConsumption || 0) / 1000,
  109. totalWaterInjection: (res2.totalWaterInjection || 0) / 10000,
  110. totalGasInjection: (res2.totalGasInjection || 0) / 10000,
  111. totalFuelConsumption: res2.totalFuelConsumption || 0,
  112. utilizationRate: Number(((res2.utilizationRate || 0) * 100).toFixed(2))
  113. }
  114. } finally {
  115. totalLoading.value = false
  116. }
  117. }, 500)
  118. interface List {
  119. id: number | null
  120. name: string | null
  121. type: '1' | '2' | '3'
  122. cumulativeGasInjection: number | null
  123. cumulativeWaterInjection: number | null
  124. cumulativePowerConsumption: number | null
  125. cumulativeFuelConsumption: number | null
  126. transitTime: number | null
  127. nonProductiveTime: number | null
  128. utilizationRate: number | null
  129. }
  130. const list = ref<List[]>([])
  131. const type = ref('2')
  132. function checkIsSameDay(createTime: string[]): boolean {
  133. if (!createTime || createTime.length < 2) {
  134. return false
  135. }
  136. const [startTime, endTime] = createTime
  137. return dayjs(startTime).isSame(endTime, 'day')
  138. }
  139. const columns = (type: string) => {
  140. return [
  141. {
  142. label: type === '2' ? '项目部' : '队伍',
  143. prop: 'name'
  144. },
  145. {
  146. label: '累计注气量(万方)',
  147. prop: 'cumulativeGasInjection'
  148. },
  149. {
  150. label: '累计注水量(万方)',
  151. prop: 'cumulativeWaterInjection'
  152. },
  153. {
  154. label: '累计用电量(MWh)',
  155. prop: 'cumulativePowerConsumption'
  156. },
  157. {
  158. label: '累计油耗(升)',
  159. prop: 'cumulativeFuelConsumption'
  160. },
  161. {
  162. label: '平均时效(%)',
  163. prop: 'hourUtilizationRate'
  164. },
  165. {
  166. label: '非生产时效(%)',
  167. prop: 'nonProductiveTime'
  168. },
  169. ...(type === '2' && checkIsSameDay(props.query.createTime)
  170. ? [
  171. {
  172. label: '队伍总数',
  173. prop: 'teamCount'
  174. },
  175. {
  176. label: '驻地待命',
  177. prop: 'zddmTeamCount'
  178. },
  179. {
  180. label: '施工准备',
  181. prop: 'zbTeamCount'
  182. },
  183. {
  184. label: '施工队伍',
  185. prop: 'sgTeamCount'
  186. }
  187. ]
  188. : []),
  189. {
  190. label: '设备利用率(%)',
  191. prop: 'utilizationRate',
  192. action: true
  193. }
  194. ]
  195. }
  196. const listLoading = ref(false)
  197. const formatter = (row: List, column: any) => {
  198. if (column.property === 'transitTime') {
  199. return (Number(row.transitTime ?? 0) * 100).toFixed(2) + '%'
  200. } else if (column.property === 'nonProductiveTime') {
  201. return (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%'
  202. } else if (column.property === 'utilizationRate') {
  203. return (Number(row.utilizationRate ?? 0) * 100).toFixed(2) + '%'
  204. } else return row[column.property] ?? 0
  205. }
  206. const getList = useDebounceFn(async () => {
  207. listLoading.value = true
  208. try {
  209. const res = await IotRhDailyReportApi.getIotRhDailyReportSummary(props.query)
  210. const { list: reslist } = res
  211. type.value = reslist[0]?.type || '2'
  212. list.value = reslist.map(
  213. ({ id, projectDeptId, projectDeptName, teamId, teamName, sort, taskId, type, ...other }) => {
  214. return {
  215. id: type === '2' ? projectDeptId : teamId,
  216. name: type === '2' ? projectDeptName : teamName,
  217. ...other,
  218. cumulativeGasInjection: ((other.cumulativeGasInjection || 0) / 10000).toFixed(4),
  219. cumulativeWaterInjection: ((other.cumulativeWaterInjection || 0) / 10000).toFixed(2),
  220. cumulativePowerConsumption: ((other.cumulativePowerConsumption || 0) / 1000).toFixed(2),
  221. cumulativeFuelConsumption: (other.cumulativeFuelConsumption || 0).toFixed(2),
  222. utilizationRate: other.utilizationRate || 0,
  223. hourUtilizationRate: ((other.hourUtilizationRate || 0) * 100).toFixed(2) + '%'
  224. }
  225. }
  226. )
  227. } finally {
  228. listLoading.value = false
  229. }
  230. }, 500)
  231. const tab = ref<'表格' | '看板'>('表格')
  232. const currentTab = ref<'表格' | '看板'>('表格')
  233. const direction = ref<'left' | 'right'>('right')
  234. const handleSelectTab = (val: '表格' | '看板') => {
  235. tab.value = val
  236. direction.value = val === '看板' ? 'right' : 'left'
  237. nextTick(() => {
  238. currentTab.value = val
  239. setTimeout(() => {
  240. render()
  241. })
  242. })
  243. }
  244. const chartRef = ref<HTMLDivElement | null>(null)
  245. let chart: ECharts | null = null
  246. let chartContainerEl: HTMLDivElement | null = null
  247. const xAxisData = ref<string[]>([])
  248. type ChartKey =
  249. | 'cumulativeFuelConsumption'
  250. | 'cumulativeGasInjection'
  251. | 'cumulativePowerConsumption'
  252. | 'cumulativeWaterInjection'
  253. | 'transitTime'
  254. | 'utilizationRate'
  255. interface LegendItem {
  256. name: string
  257. key: ChartKey
  258. unit: string
  259. decimals: number
  260. }
  261. const legendItems: LegendItem[] = [
  262. { name: '累计油耗 (升)', key: 'cumulativeFuelConsumption', unit: '升', decimals: 2 },
  263. { name: '累计注气量 (万方)', key: 'cumulativeGasInjection', unit: '万方', decimals: 2 },
  264. { name: '累计用电量 (KWh)', key: 'cumulativePowerConsumption', unit: 'KWh', decimals: 2 },
  265. { name: '累计注水量 (方)', key: 'cumulativeWaterInjection', unit: '方', decimals: 2 },
  266. { name: '平均时效 (%)', key: 'transitTime', unit: '%', decimals: 2 },
  267. { name: '设备利用率 (%)', key: 'utilizationRate', unit: '%', decimals: 2 }
  268. ]
  269. const legendItemMap = legendItems.reduce<Record<string, LegendItem>>((map, item) => {
  270. map[item.name] = item
  271. return map
  272. }, {})
  273. const chartData = ref<Record<ChartKey, number[]>>({
  274. cumulativeFuelConsumption: [],
  275. cumulativeGasInjection: [],
  276. cumulativePowerConsumption: [],
  277. cumulativeWaterInjection: [],
  278. transitTime: [],
  279. utilizationRate: []
  280. })
  281. let chartLoading = ref(false)
  282. const getChart = useDebounceFn(async () => {
  283. chartLoading.value = true
  284. try {
  285. const res = await IotRhDailyReportApi.getIotRhDailyReportSummaryPolyline(props.query)
  286. chartData.value = {
  287. cumulativeFuelConsumption: res.map((item) => item.cumulativeFuelConsumption || 0),
  288. // cumulativeFuelConsumption: res.map((item) => (item.cumulativeFuelConsumption || 0) / 10000),
  289. cumulativeGasInjection: res.map((item) => (item.cumulativeGasInjection || 0) / 10000),
  290. cumulativePowerConsumption: res.map((item) => item.cumulativePowerConsumption || 0),
  291. // cumulativePowerConsumption: res.map((item) => (item.cumulativePowerConsumption || 0) / 1000),
  292. cumulativeWaterInjection: res.map((item) => item.cumulativeWaterInjection || 0),
  293. // cumulativeWaterInjection: res.map((item) => (item.cumulativeWaterInjection || 0) / 10000),
  294. transitTime: res.map((item) => (item.transitTime || 0) * 100),
  295. utilizationRate: res.map((item) => Number(((item.utilizationRate || 0) * 100).toFixed(2)))
  296. }
  297. xAxisData.value = res.map((item) => item.reportDate || '')
  298. resetVisibleZoomRange()
  299. } finally {
  300. chartLoading.value = false
  301. }
  302. }, 500)
  303. const resizer = useDebounceFn(() => {
  304. chart?.resize()
  305. }, 100)
  306. const selectedLegends = ref<Record<string, boolean>>({})
  307. const visibleZoomRange = ref({
  308. startIndex: 0,
  309. endIndex: 0
  310. })
  311. const NORMALIZED_AXIS_MIN = 0
  312. const NORMALIZED_AXIS_MAX = 100
  313. const NORMALIZED_AXIS_CENTER = 50
  314. const NORMALIZED_AXIS_PADDING = 4
  315. const ensureLegendSelection = () => {
  316. legendItems.forEach(({ name }) => {
  317. if (selectedLegends.value[name] === undefined) {
  318. selectedLegends.value[name] = true
  319. }
  320. })
  321. }
  322. const resetVisibleZoomRange = () => {
  323. const lastIndex = Math.max(xAxisData.value.length - 1, 0)
  324. visibleZoomRange.value = {
  325. startIndex: 0,
  326. endIndex: lastIndex
  327. }
  328. }
  329. const clampZoomIndex = (value: number, lastIndex: number) => {
  330. return Math.min(Math.max(Math.round(value), 0), lastIndex)
  331. }
  332. const syncVisibleZoomRangeFromChart = () => {
  333. const lastIndex = Math.max(xAxisData.value.length - 1, 0)
  334. if (!chart || lastIndex <= 0) {
  335. resetVisibleZoomRange()
  336. return
  337. }
  338. const dataZoomOptions = chart.getOption().dataZoom
  339. const primaryDataZoom = Array.isArray(dataZoomOptions) ? dataZoomOptions[0] : undefined
  340. if (!primaryDataZoom) {
  341. resetVisibleZoomRange()
  342. return
  343. }
  344. const startValue = Number(primaryDataZoom.startValue)
  345. const endValue = Number(primaryDataZoom.endValue)
  346. let nextStartIndex = 0
  347. let nextEndIndex = lastIndex
  348. if (Number.isFinite(startValue) && Number.isFinite(endValue)) {
  349. nextStartIndex = clampZoomIndex(startValue, lastIndex)
  350. nextEndIndex = clampZoomIndex(endValue, lastIndex)
  351. } else {
  352. const startPercent = Number(primaryDataZoom.start ?? 0)
  353. const endPercent = Number(primaryDataZoom.end ?? 100)
  354. nextStartIndex = clampZoomIndex((startPercent / 100) * lastIndex, lastIndex)
  355. nextEndIndex = clampZoomIndex((endPercent / 100) * lastIndex, lastIndex)
  356. }
  357. visibleZoomRange.value = {
  358. startIndex: Math.min(nextStartIndex, nextEndIndex),
  359. endIndex: Math.max(nextStartIndex, nextEndIndex)
  360. }
  361. }
  362. const getVisibleDatasetValues = (dataset: number[]) => {
  363. if (!dataset.length) return []
  364. const lastIndex = dataset.length - 1
  365. const startIndex = clampZoomIndex(visibleZoomRange.value.startIndex, lastIndex)
  366. const endIndex = Math.max(startIndex, clampZoomIndex(visibleZoomRange.value.endIndex, lastIndex))
  367. return dataset.slice(startIndex, endIndex + 1).filter((value) => Number.isFinite(value))
  368. }
  369. const normalizeSeriesData = (dataset: number[]) => {
  370. if (!dataset.length) return []
  371. const validValues = getVisibleDatasetValues(dataset)
  372. if (!validValues.length) {
  373. return dataset.map(() => NORMALIZED_AXIS_MIN)
  374. }
  375. const min = Math.min(...validValues)
  376. const max = Math.max(...validValues)
  377. if (min === max) {
  378. return dataset.map(() => (min === 0 ? NORMALIZED_AXIS_MIN : NORMALIZED_AXIS_CENTER))
  379. }
  380. const usableHeight = NORMALIZED_AXIS_MAX - NORMALIZED_AXIS_MIN - NORMALIZED_AXIS_PADDING * 2
  381. return dataset.map((value) =>
  382. Number((NORMALIZED_AXIS_PADDING + ((value - min) / (max - min)) * usableHeight).toFixed(2))
  383. )
  384. }
  385. const formatTrendAxisLabel = (value: number) => {
  386. if (value === NORMALIZED_AXIS_MIN) return '低'
  387. if (value === NORMALIZED_AXIS_CENTER) return '中'
  388. if (value === NORMALIZED_AXIS_MAX) return '高'
  389. return ''
  390. }
  391. const formatSeriesValue = (name: string, value: number) => {
  392. const item = legendItemMap[name]
  393. const safeValue = Number.isFinite(value) ? value : 0
  394. if (!item) return safeValue.toFixed(2)
  395. return `${safeValue.toFixed(item.decimals)} ${item.unit}`
  396. }
  397. const getSeries = () => {
  398. const enableSampling = xAxisData.value.length > 120
  399. return legendItems.map((item) => ({
  400. name: item.name,
  401. type: 'line',
  402. smooth: false,
  403. showSymbol: true,
  404. symbol: 'circle',
  405. symbolSize: 6,
  406. connectNulls: true,
  407. lineStyle: {
  408. width: 2
  409. },
  410. emphasis: {
  411. focus: 'series'
  412. },
  413. sampling: enableSampling ? 'lttb' : undefined,
  414. progressive: 300,
  415. progressiveThreshold: 1500,
  416. data: normalizeSeriesData(chartData.value[item.key])
  417. }))
  418. }
  419. const getChartOption = () => ({
  420. animation: xAxisData.value.length <= 120,
  421. animationDuration: 280,
  422. animationDurationUpdate: 180,
  423. grid: {
  424. top: 72,
  425. right: 24,
  426. bottom: 68,
  427. left: 48,
  428. containLabel: true
  429. },
  430. tooltip: {
  431. trigger: 'axis',
  432. axisPointer: { type: 'line' },
  433. formatter: (params: any) => {
  434. const list = Array.isArray(params) ? params : [params]
  435. if (!list.length) return ''
  436. const content = list.map((item: any) => {
  437. const legendItem = legendItemMap[item.seriesName]
  438. const realValue = legendItem ? (chartData.value[legendItem.key][item.dataIndex] ?? 0) : 0
  439. return `<div class="flex items-center justify-between mt-1 gap-1">
  440. <span>${item.marker} ${item.seriesName}</span>
  441. <span>${formatSeriesValue(item.seriesName, realValue)}</span>
  442. </div>`
  443. })
  444. return `${list[0].axisValueLabel}<br>${content.join('')}`
  445. }
  446. },
  447. legend: {
  448. type: 'scroll',
  449. top: 16,
  450. data: legendItems.map((item) => item.name),
  451. selected: selectedLegends.value,
  452. show: true
  453. },
  454. xAxis: {
  455. type: 'category',
  456. boundaryGap: false,
  457. data: xAxisData.value,
  458. axisLabel: {
  459. hideOverlap: true
  460. }
  461. },
  462. dataZoom: [
  463. {
  464. type: 'inside',
  465. xAxisIndex: 0,
  466. filterMode: 'none',
  467. throttle: 50,
  468. startValue: visibleZoomRange.value.startIndex,
  469. endValue: visibleZoomRange.value.endIndex
  470. },
  471. {
  472. type: 'slider',
  473. xAxisIndex: 0,
  474. filterMode: 'none',
  475. height: 18,
  476. bottom: 8,
  477. brushSelect: false,
  478. showDetail: false,
  479. moveHandleSize: 0,
  480. throttle: 50,
  481. startValue: visibleZoomRange.value.startIndex,
  482. endValue: visibleZoomRange.value.endIndex
  483. }
  484. ],
  485. yAxis: {
  486. type: 'value',
  487. min: NORMALIZED_AXIS_MIN,
  488. max: NORMALIZED_AXIS_MAX,
  489. splitNumber: 4,
  490. name: '相对趋势',
  491. nameGap: 16,
  492. axisLabel: {
  493. formatter: formatTrendAxisLabel
  494. },
  495. splitLine: {
  496. lineStyle: {
  497. type: 'dashed'
  498. }
  499. }
  500. },
  501. series: getSeries()
  502. })
  503. const initChart = () => {
  504. if (!chartRef.value) return
  505. if (chart && chartContainerEl === chartRef.value) {
  506. return
  507. }
  508. chart?.dispose()
  509. window.removeEventListener('resize', resizer)
  510. chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas', useDirtyRect: true })
  511. chartContainerEl = chartRef.value
  512. window.addEventListener('resize', resizer)
  513. chart.off('legendselectchanged')
  514. chart.on('legendselectchanged', (params: any) => {
  515. selectedLegends.value = { ...params.selected }
  516. })
  517. chart.off('datazoom')
  518. chart.on('datazoom', () => {
  519. syncVisibleZoomRangeFromChart()
  520. chart?.setOption(
  521. {
  522. series: getSeries()
  523. },
  524. { lazyUpdate: true }
  525. )
  526. })
  527. }
  528. const render = () => {
  529. if (!chartRef.value) return
  530. initChart()
  531. ensureLegendSelection()
  532. chart?.setOption(getChartOption(), { notMerge: true, lazyUpdate: true })
  533. }
  534. onUnmounted(() => {
  535. window.removeEventListener('resize', resizer)
  536. chart?.dispose()
  537. chart = null
  538. chartContainerEl = null
  539. })
  540. const handleQuery = () => {
  541. getChart().then(() => {
  542. render()
  543. })
  544. getList()
  545. getTotal()
  546. }
  547. watch(
  548. () => [
  549. props.refreshKey,
  550. props.query.deptId,
  551. props.query.contractName,
  552. props.query.taskName,
  553. props.query.createTime?.[0],
  554. props.query.createTime?.[1]
  555. ],
  556. () => {
  557. if (!props.query.createTime) {
  558. totalWork.value.totalCount = 0
  559. totalWork.value.notReported = 0
  560. totalWork.value.alreadyReported = 0
  561. }
  562. handleQuery()
  563. },
  564. { immediate: true }
  565. )
  566. const exportChart = () => {
  567. if (!chart) return
  568. let img = new Image()
  569. img.src = chart.getDataURL({
  570. type: 'png',
  571. pixelRatio: 1,
  572. backgroundColor: '#fff'
  573. })
  574. img.onload = function () {
  575. let canvas = document.createElement('canvas')
  576. canvas.width = img.width
  577. canvas.height = img.height
  578. let ctx = canvas.getContext('2d')
  579. ctx?.drawImage(img, 0, 0)
  580. let dataURL = canvas.toDataURL('image/png')
  581. let a = document.createElement('a')
  582. let event = new MouseEvent('click')
  583. a.href = dataURL
  584. a.download = `瑞恒日报统计数据.png`
  585. a.dispatchEvent(event)
  586. }
  587. }
  588. const exportData = async () => {
  589. const res = await IotRhDailyReportApi.exportRhDailyReportStatistics(props.query)
  590. download.excel(res, '瑞恒日报统计数据.xlsx')
  591. }
  592. const exportAll = async () => {
  593. if (tab.value === '看板') exportChart()
  594. else exportData()
  595. }
  596. const message = useMessage()
  597. const unfilledDialogRef = ref()
  598. const openUnfilledDialog = () => {
  599. // 检查是否选择了创建时间
  600. if (!props.query.createTime || props.query.createTime.length === 0) {
  601. message.warning('请先选择创建时间范围')
  602. return
  603. }
  604. // 打开弹窗
  605. unfilledDialogRef.value?.open()
  606. }
  607. const router = useRouter()
  608. const tolist = (id: number, non: boolean = false) => {
  609. const { pageNo, pageSize, ...rest } = props.query
  610. router.push({
  611. path: '/iotdayilyreport/IotRhDailyReport',
  612. query: {
  613. ...rest,
  614. deptId: id,
  615. ...(non ? { nonProductFlag: 'Y' } : {})
  616. }
  617. })
  618. }
  619. const { ZmTable, ZmTableColumn } = useTableComponents()
  620. </script>
  621. <template>
  622. <div class="grid grid-rows-[128px_1fr] gap-4 h-full min-h-0">
  623. <div class="grid grid-cols-8 gap-8">
  624. <template v-for="info in totalWorkKeys" :key="info[0]">
  625. <el-tooltip :disabled="info[0] !== 'totalGasInjection'" placement="top">
  626. <template #content>
  627. <div>
  628. 累计氮气注气量:{{
  629. formatGasInjectionTooltipValue(totalWorkloadDetail.totalN2GasInjection)
  630. }}
  631. 万方
  632. </div>
  633. <div>
  634. 累计天然气注气量:{{
  635. formatGasInjectionTooltipValue(totalWorkloadDetail.totalNaturalGasInjection)
  636. }}
  637. 万方
  638. </div>
  639. </template>
  640. <div
  641. class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow p-1 flex flex-col items-center justify-center gap-1">
  642. <div class="size-7.5" :class="info[3]"></div>
  643. <count-to
  644. class="text-2xl font-medium"
  645. :class="{ 'cursor-help': info[0] === 'totalGasInjection' }"
  646. :start-val="0"
  647. :end-val="totalWork[info[0]]"
  648. :decimals="info[4]"
  649. @click="info[2] === '未填报' ? openUnfilledDialog() : ''">
  650. <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
  651. </count-to>
  652. <div class="text-sm font-medium text-[var(--el-text-color-regular)] whitespace-nowrap">
  653. {{ info[1] ? info[2] + '(' + info[1] + ')' : info[2] }}
  654. </div>
  655. </div>
  656. </el-tooltip>
  657. </template>
  658. </div>
  659. <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col p-4 gap-2 min-h-0">
  660. <div class="flex h-12 items-center justify-between">
  661. <el-button-group>
  662. <el-button
  663. size="default"
  664. :type="tab === '表格' ? 'primary' : 'default'"
  665. @click="handleSelectTab('表格')"
  666. >表格
  667. </el-button>
  668. <el-button
  669. size="default"
  670. :type="tab === '看板' ? 'primary' : 'default'"
  671. @click="handleSelectTab('看板')"
  672. >看板
  673. </el-button>
  674. </el-button-group>
  675. <h3 class="text-xl font-medium">{{ `${props.deptName}-${tab}` }}</h3>
  676. <el-button size="default" type="primary" @click="exportAll">导出</el-button>
  677. </div>
  678. <div class="flex-1 relative min-h-0">
  679. <el-auto-resizer class="absolute">
  680. <template #default="{ height }">
  681. <Motion
  682. as="div"
  683. :style="{ position: 'relative', overflow: 'hidden' }"
  684. :animate="{ height: `${height}px`, width: `100%` }"
  685. :transition="{ type: 'spring', stiffness: 200, damping: 25, duration: 0.3 }">
  686. <AnimatePresence :initial="false" mode="sync">
  687. <Motion
  688. :key="currentTab"
  689. as="div"
  690. :initial="{ x: direction === 'left' ? '-100%' : '100%', opacity: 0 }"
  691. :animate="{ x: '0%', opacity: 1 }"
  692. :exit="{ x: direction === 'left' ? '50%' : '-50%', opacity: 0 }"
  693. :transition="{ type: 'tween', stiffness: 300, damping: 30, duration: 0.3 }"
  694. :style="{ position: 'absolute', left: 0, right: 0, top: 0 }">
  695. <div :style="{ width: `100%`, height: `${height}px` }">
  696. <zm-table
  697. v-if="currentTab === '表格'"
  698. :loading="listLoading"
  699. :data="list"
  700. :height="height"
  701. show-border>
  702. <template v-for="item in columns(type)" :key="item.prop">
  703. <zm-table-column
  704. v-if="item.prop !== 'name' && item.prop !== 'nonProductiveTime'"
  705. :label="item.label"
  706. :prop="item.prop"
  707. :formatter="formatter"
  708. :action="item.action" />
  709. <zm-table-column
  710. v-else-if="item.prop === 'name'"
  711. :label="item.label"
  712. :prop="item.prop">
  713. <template #default="{ row }">
  714. <el-button text type="primary" @click.prevent="tolist(row.id)">{{
  715. row.name
  716. }}</el-button>
  717. </template>
  718. </zm-table-column>
  719. <zm-table-column v-else :label="item.label" :prop="item.prop">
  720. <template #default="{ row }">
  721. <el-button
  722. v-if="row.nonProductiveTime > 0"
  723. text
  724. type="primary"
  725. @click.prevent="tolist(row.id, true)">
  726. {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
  727. </el-button>
  728. <span v-else>
  729. {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
  730. </span>
  731. </template>
  732. </zm-table-column>
  733. </template>
  734. </zm-table>
  735. <div
  736. ref="chartRef"
  737. v-loading="chartLoading"
  738. v-else
  739. :style="{ width: `100%`, height: `${height}px` }">
  740. </div>
  741. </div>
  742. </Motion>
  743. </AnimatePresence>
  744. </Motion>
  745. </template>
  746. </el-auto-resizer>
  747. </div>
  748. </div>
  749. </div>
  750. <UnfilledReportDialog ref="unfilledDialogRef" :query-params="props.query" />
  751. </template>