|
|
@@ -1,25 +1,9 @@
|
|
|
<script setup lang="ts">
|
|
|
import dayjs from 'dayjs'
|
|
|
-import type { ECharts } from 'echarts/core'
|
|
|
-import { DataZoomComponent } from 'echarts/components'
|
|
|
-import {
|
|
|
- IotRhDailyReportApi,
|
|
|
- type IotRhDailyReportTotalWorkloadVO
|
|
|
-} from '@/api/pms/iotrhdailyreport'
|
|
|
-import { useDebounceFn } from '@vueuse/core'
|
|
|
-import CountTo from '@/components/count-to1.vue'
|
|
|
-import echarts from '@/plugins/echarts'
|
|
|
-import UnfilledReportDialog from './UnfilledReportDialog.vue'
|
|
|
-
|
|
|
-import { Motion, AnimatePresence } from 'motion-v'
|
|
|
-
|
|
|
import { rangeShortcuts } from '@/utils/formatTime'
|
|
|
-import download from '@/utils/download'
|
|
|
-
|
|
|
import { useUserStore } from '@/store/modules/user'
|
|
|
-import { useTableComponents } from '@/components/ZmTable/useTableComponents'
|
|
|
-
|
|
|
-echarts.use([DataZoomComponent])
|
|
|
+import DailyStatistics from './components/DailyStatistics.vue'
|
|
|
+import NonProductionEfficiency from './components/NonProductionEfficiency.vue'
|
|
|
|
|
|
const deptId = useUserStore().getUser.deptId
|
|
|
|
|
|
@@ -34,738 +18,42 @@ interface Query {
|
|
|
|
|
|
const id = deptId
|
|
|
|
|
|
-const query = ref<Query>({
|
|
|
+const createDefaultQuery = (): Query => ({
|
|
|
pageNo: 1,
|
|
|
pageSize: 10,
|
|
|
deptId: deptId,
|
|
|
+ contractName: '',
|
|
|
+ taskName: '',
|
|
|
createTime: [
|
|
|
...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
|
|
|
]
|
|
|
})
|
|
|
|
|
|
-const totalWorkKeys: [string, string, string, string, number][] = [
|
|
|
- ['totalGasInjection', '万方', '累计注气量', 'i-material-symbols:cloud-outline text-sky', 2],
|
|
|
- [
|
|
|
- 'totalWaterInjection',
|
|
|
- '万方',
|
|
|
- '累计注水量',
|
|
|
- 'i-material-symbols:water-drop-outline-rounded text-sky',
|
|
|
- 2
|
|
|
- ],
|
|
|
- [
|
|
|
- 'utilizationRate',
|
|
|
- '%',
|
|
|
- '设备利用率',
|
|
|
- 'i-material-symbols:check-circle-outline-rounded text-emerald',
|
|
|
- 0
|
|
|
- ],
|
|
|
- [
|
|
|
- 'totalPowerConsumption',
|
|
|
- 'MWh',
|
|
|
- '累计用电量',
|
|
|
- 'i-material-symbols:electric-bolt-outline-rounded text-sky',
|
|
|
- 2
|
|
|
- ],
|
|
|
- [
|
|
|
- 'totalFuelConsumption',
|
|
|
- '升',
|
|
|
- '累计油耗',
|
|
|
- 'i-material-symbols:directions-car-outline-rounded text-sky',
|
|
|
- 2
|
|
|
- ],
|
|
|
- ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky', 0],
|
|
|
- [
|
|
|
- 'alreadyReported',
|
|
|
- '个',
|
|
|
- '已填报',
|
|
|
- 'i-material-symbols:check-circle-outline-rounded text-emerald',
|
|
|
- 0
|
|
|
- ],
|
|
|
- ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose', 0]
|
|
|
-]
|
|
|
-
|
|
|
-const totalWork = ref({
|
|
|
- totalCount: 0,
|
|
|
- alreadyReported: 0,
|
|
|
- notReported: 0,
|
|
|
- totalFuelConsumption: 0,
|
|
|
- totalPowerConsumption: 0,
|
|
|
- totalWaterInjection: 0,
|
|
|
- totalGasInjection: 0,
|
|
|
- utilizationRate: 0
|
|
|
-})
|
|
|
-
|
|
|
-const totalLoading = ref(false)
|
|
|
-
|
|
|
-const totalWorkloadDetail = ref({
|
|
|
- totalN2GasInjection: 0,
|
|
|
- totalNaturalGasInjection: 0
|
|
|
-})
|
|
|
-
|
|
|
-const formatGasInjectionTooltipValue = (value?: number | null) => ((value || 0) / 10000).toFixed(4)
|
|
|
-
|
|
|
-const getTotal = useDebounceFn(async () => {
|
|
|
- totalLoading.value = true
|
|
|
-
|
|
|
- const { pageNo, pageSize, ...other } = query.value
|
|
|
-
|
|
|
- try {
|
|
|
- let res1: any[]
|
|
|
- if (query.value.createTime && query.value.createTime.length === 2) {
|
|
|
- res1 = await IotRhDailyReportApi.rhDailyReportStatistics({
|
|
|
- createTime: query.value.createTime,
|
|
|
- deptId: query.value.deptId
|
|
|
- })
|
|
|
-
|
|
|
- totalWork.value.totalCount = res1[0].count
|
|
|
- totalWork.value.alreadyReported = res1[1].count
|
|
|
- totalWork.value.notReported = res1[2].count
|
|
|
- }
|
|
|
-
|
|
|
- const res2: IotRhDailyReportTotalWorkloadVO = await IotRhDailyReportApi.totalWorkload(other)
|
|
|
-
|
|
|
- totalWorkloadDetail.value = {
|
|
|
- totalN2GasInjection: res2.totalN2GasInjection || 0,
|
|
|
- totalNaturalGasInjection: res2.totalNaturalGasInjection || 0
|
|
|
- }
|
|
|
-
|
|
|
- totalWork.value = {
|
|
|
- ...totalWork.value,
|
|
|
- ...res2,
|
|
|
- totalPowerConsumption: (res2.totalPowerConsumption || 0) / 1000,
|
|
|
- totalWaterInjection: (res2.totalWaterInjection || 0) / 10000,
|
|
|
- totalGasInjection: (res2.totalGasInjection || 0) / 10000,
|
|
|
- totalFuelConsumption: res2.totalFuelConsumption || 0,
|
|
|
- utilizationRate: Number(((res2.utilizationRate || 0) * 100).toFixed(2))
|
|
|
- }
|
|
|
- } finally {
|
|
|
- totalLoading.value = false
|
|
|
- }
|
|
|
-}, 500)
|
|
|
-
|
|
|
-interface List {
|
|
|
- id: number | null
|
|
|
- name: string | null
|
|
|
- type: '1' | '2' | '3'
|
|
|
- cumulativeGasInjection: number | null
|
|
|
- cumulativeWaterInjection: number | null
|
|
|
- cumulativePowerConsumption: number | null
|
|
|
- cumulativeFuelConsumption: number | null
|
|
|
- transitTime: number | null
|
|
|
- nonProductiveTime: number | null
|
|
|
- utilizationRate: number | null
|
|
|
-}
|
|
|
-
|
|
|
-const list = ref<List[]>([])
|
|
|
-
|
|
|
-const type = ref('2')
|
|
|
-
|
|
|
-function checkIsSameDay(createTime: string[]): boolean {
|
|
|
- if (!createTime || createTime.length < 2) {
|
|
|
- return false
|
|
|
- }
|
|
|
-
|
|
|
- const [startTime, endTime] = createTime
|
|
|
-
|
|
|
- return dayjs(startTime).isSame(endTime, 'day')
|
|
|
-}
|
|
|
-
|
|
|
-const columns = (type: string) => {
|
|
|
- return [
|
|
|
- {
|
|
|
- label: type === '2' ? '项目部' : '队伍',
|
|
|
- prop: 'name'
|
|
|
- },
|
|
|
- {
|
|
|
- label: '累计注气量(万方)',
|
|
|
- prop: 'cumulativeGasInjection'
|
|
|
- },
|
|
|
- {
|
|
|
- label: '累计注水量(万方)',
|
|
|
- prop: 'cumulativeWaterInjection'
|
|
|
- },
|
|
|
- {
|
|
|
- label: '累计用电量(MWh)',
|
|
|
- prop: 'cumulativePowerConsumption'
|
|
|
- },
|
|
|
- {
|
|
|
- label: '累计油耗(升)',
|
|
|
- prop: 'cumulativeFuelConsumption'
|
|
|
- },
|
|
|
- {
|
|
|
- label: '平均时效(%)',
|
|
|
- prop: 'transitTime'
|
|
|
- },
|
|
|
- {
|
|
|
- label: '非生产时效(%)',
|
|
|
- prop: 'nonProductiveTime'
|
|
|
- },
|
|
|
- ...(type === '2' && checkIsSameDay(query.value.createTime)
|
|
|
- ? [
|
|
|
- {
|
|
|
- label: '队伍总数',
|
|
|
- prop: 'teamCount'
|
|
|
- },
|
|
|
- {
|
|
|
- label: '驻地待命',
|
|
|
- prop: 'zddmTeamCount'
|
|
|
- },
|
|
|
- {
|
|
|
- label: '施工准备',
|
|
|
- prop: 'zbTeamCount'
|
|
|
- },
|
|
|
- {
|
|
|
- label: '施工队伍',
|
|
|
- prop: 'sgTeamCount'
|
|
|
- }
|
|
|
- ]
|
|
|
- : []),
|
|
|
- {
|
|
|
- label: '设备利用率(%)',
|
|
|
- prop: 'utilizationRate',
|
|
|
- action: true
|
|
|
- }
|
|
|
- ]
|
|
|
-}
|
|
|
-
|
|
|
-const listLoading = ref(false)
|
|
|
-
|
|
|
-const formatter = (row: List, column: any) => {
|
|
|
- if (column.property === 'transitTime') {
|
|
|
- return (Number(row.transitTime ?? 0) * 100).toFixed(2) + '%'
|
|
|
- } else if (column.property === 'nonProductiveTime') {
|
|
|
- return (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%'
|
|
|
- } else if (column.property === 'utilizationRate') {
|
|
|
- return (Number(row.utilizationRate ?? 0) * 100).toFixed(2) + '%'
|
|
|
- } else return row[column.property] ?? 0
|
|
|
-}
|
|
|
-
|
|
|
-const getList = useDebounceFn(async () => {
|
|
|
- listLoading.value = true
|
|
|
- try {
|
|
|
- const res = await IotRhDailyReportApi.getIotRhDailyReportSummary(query.value)
|
|
|
-
|
|
|
- const { list: reslist } = res
|
|
|
-
|
|
|
- type.value = reslist[0]?.type || '2'
|
|
|
-
|
|
|
- list.value = reslist.map(
|
|
|
- ({ id, projectDeptId, projectDeptName, teamId, teamName, sort, taskId, type, ...other }) => {
|
|
|
- return {
|
|
|
- id: type === '2' ? projectDeptId : teamId,
|
|
|
- name: type === '2' ? projectDeptName : teamName,
|
|
|
- ...other,
|
|
|
- cumulativeGasInjection: ((other.cumulativeGasInjection || 0) / 10000).toFixed(4),
|
|
|
- cumulativeWaterInjection: ((other.cumulativeWaterInjection || 0) / 10000).toFixed(2),
|
|
|
- cumulativePowerConsumption: ((other.cumulativePowerConsumption || 0) / 1000).toFixed(2),
|
|
|
- cumulativeFuelConsumption: (other.cumulativeFuelConsumption || 0).toFixed(2),
|
|
|
- utilizationRate: other.utilizationRate || 0
|
|
|
- }
|
|
|
- }
|
|
|
- )
|
|
|
- } finally {
|
|
|
- listLoading.value = false
|
|
|
- }
|
|
|
-}, 500)
|
|
|
-
|
|
|
-const tab = ref<'表格' | '看板'>('表格')
|
|
|
-
|
|
|
-const currentTab = ref<'表格' | '看板'>('表格')
|
|
|
-
|
|
|
+const query = ref<Query>(createDefaultQuery())
|
|
|
+const activeTab = ref<'日报统计' | '非生产时效'>('日报统计')
|
|
|
const deptName = ref('瑞恒兴域')
|
|
|
-
|
|
|
-const direction = ref<'left' | 'right'>('right')
|
|
|
-
|
|
|
-const handleSelectTab = (val: '表格' | '看板') => {
|
|
|
- tab.value = val
|
|
|
- direction.value = val === '看板' ? 'right' : 'left'
|
|
|
- nextTick(() => {
|
|
|
- currentTab.value = val
|
|
|
- setTimeout(() => {
|
|
|
- render()
|
|
|
- })
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-const chartRef = ref<HTMLDivElement | null>(null)
|
|
|
-let chart: ECharts | null = null
|
|
|
-let chartContainerEl: HTMLDivElement | null = null
|
|
|
-
|
|
|
-const xAxisData = ref<string[]>([])
|
|
|
-
|
|
|
-type ChartKey =
|
|
|
- | 'cumulativeFuelConsumption'
|
|
|
- | 'cumulativeGasInjection'
|
|
|
- | 'cumulativePowerConsumption'
|
|
|
- | 'cumulativeWaterInjection'
|
|
|
- | 'transitTime'
|
|
|
- | 'utilizationRate'
|
|
|
-
|
|
|
-interface LegendItem {
|
|
|
- name: string
|
|
|
- key: ChartKey
|
|
|
- unit: string
|
|
|
- decimals: number
|
|
|
-}
|
|
|
-
|
|
|
-const legendItems: LegendItem[] = [
|
|
|
- { name: '累计油耗 (升)', key: 'cumulativeFuelConsumption', unit: '升', decimals: 2 },
|
|
|
- { name: '累计注气量 (万方)', key: 'cumulativeGasInjection', unit: '万方', decimals: 2 },
|
|
|
- { name: '累计用电量 (KWh)', key: 'cumulativePowerConsumption', unit: 'KWh', decimals: 2 },
|
|
|
- { name: '累计注水量 (方)', key: 'cumulativeWaterInjection', unit: '方', decimals: 2 },
|
|
|
- { name: '平均时效 (%)', key: 'transitTime', unit: '%', decimals: 2 },
|
|
|
- { name: '设备利用率 (%)', key: 'utilizationRate', unit: '%', decimals: 2 }
|
|
|
-]
|
|
|
-
|
|
|
-const legendItemMap = legendItems.reduce<Record<string, LegendItem>>((map, item) => {
|
|
|
- map[item.name] = item
|
|
|
- return map
|
|
|
-}, {})
|
|
|
-
|
|
|
-const chartData = ref<Record<ChartKey, number[]>>({
|
|
|
- cumulativeFuelConsumption: [],
|
|
|
- cumulativeGasInjection: [],
|
|
|
- cumulativePowerConsumption: [],
|
|
|
- cumulativeWaterInjection: [],
|
|
|
- transitTime: [],
|
|
|
- utilizationRate: []
|
|
|
-})
|
|
|
-
|
|
|
-let chartLoading = ref(false)
|
|
|
-
|
|
|
-const getChart = useDebounceFn(async () => {
|
|
|
- chartLoading.value = true
|
|
|
-
|
|
|
- try {
|
|
|
- const res = await IotRhDailyReportApi.getIotRhDailyReportSummaryPolyline(query.value)
|
|
|
-
|
|
|
- chartData.value = {
|
|
|
- cumulativeFuelConsumption: res.map((item) => item.cumulativeFuelConsumption || 0),
|
|
|
- // cumulativeFuelConsumption: res.map((item) => (item.cumulativeFuelConsumption || 0) / 10000),
|
|
|
- cumulativeGasInjection: res.map((item) => (item.cumulativeGasInjection || 0) / 10000),
|
|
|
- cumulativePowerConsumption: res.map((item) => item.cumulativePowerConsumption || 0),
|
|
|
- // cumulativePowerConsumption: res.map((item) => (item.cumulativePowerConsumption || 0) / 1000),
|
|
|
- cumulativeWaterInjection: res.map((item) => item.cumulativeWaterInjection || 0),
|
|
|
- // cumulativeWaterInjection: res.map((item) => (item.cumulativeWaterInjection || 0) / 10000),
|
|
|
- transitTime: res.map((item) => (item.transitTime || 0) * 100),
|
|
|
- utilizationRate: res.map((item) => Number(((item.utilizationRate || 0) * 100).toFixed(2)))
|
|
|
- }
|
|
|
-
|
|
|
- xAxisData.value = res.map((item) => item.reportDate || '')
|
|
|
- resetVisibleZoomRange()
|
|
|
- } finally {
|
|
|
- chartLoading.value = false
|
|
|
- }
|
|
|
-}, 500)
|
|
|
-
|
|
|
-const resizer = useDebounceFn(() => {
|
|
|
- chart?.resize()
|
|
|
-}, 100)
|
|
|
-
|
|
|
-const selectedLegends = ref<Record<string, boolean>>({})
|
|
|
-const visibleZoomRange = ref({
|
|
|
- startIndex: 0,
|
|
|
- endIndex: 0
|
|
|
-})
|
|
|
-
|
|
|
-const NORMALIZED_AXIS_MIN = 0
|
|
|
-const NORMALIZED_AXIS_MAX = 100
|
|
|
-const NORMALIZED_AXIS_CENTER = 50
|
|
|
-const NORMALIZED_AXIS_PADDING = 4
|
|
|
-
|
|
|
-const ensureLegendSelection = () => {
|
|
|
- legendItems.forEach(({ name }) => {
|
|
|
- if (selectedLegends.value[name] === undefined) {
|
|
|
- selectedLegends.value[name] = true
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-const resetVisibleZoomRange = () => {
|
|
|
- const lastIndex = Math.max(xAxisData.value.length - 1, 0)
|
|
|
-
|
|
|
- visibleZoomRange.value = {
|
|
|
- startIndex: 0,
|
|
|
- endIndex: lastIndex
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const clampZoomIndex = (value: number, lastIndex: number) => {
|
|
|
- return Math.min(Math.max(Math.round(value), 0), lastIndex)
|
|
|
-}
|
|
|
-
|
|
|
-const syncVisibleZoomRangeFromChart = () => {
|
|
|
- const lastIndex = Math.max(xAxisData.value.length - 1, 0)
|
|
|
-
|
|
|
- if (!chart || lastIndex <= 0) {
|
|
|
- resetVisibleZoomRange()
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const dataZoomOptions = chart.getOption().dataZoom
|
|
|
- const primaryDataZoom = Array.isArray(dataZoomOptions) ? dataZoomOptions[0] : undefined
|
|
|
-
|
|
|
- if (!primaryDataZoom) {
|
|
|
- resetVisibleZoomRange()
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const startValue = Number(primaryDataZoom.startValue)
|
|
|
- const endValue = Number(primaryDataZoom.endValue)
|
|
|
-
|
|
|
- let nextStartIndex = 0
|
|
|
- let nextEndIndex = lastIndex
|
|
|
-
|
|
|
- if (Number.isFinite(startValue) && Number.isFinite(endValue)) {
|
|
|
- nextStartIndex = clampZoomIndex(startValue, lastIndex)
|
|
|
- nextEndIndex = clampZoomIndex(endValue, lastIndex)
|
|
|
- } else {
|
|
|
- const startPercent = Number(primaryDataZoom.start ?? 0)
|
|
|
- const endPercent = Number(primaryDataZoom.end ?? 100)
|
|
|
-
|
|
|
- nextStartIndex = clampZoomIndex((startPercent / 100) * lastIndex, lastIndex)
|
|
|
- nextEndIndex = clampZoomIndex((endPercent / 100) * lastIndex, lastIndex)
|
|
|
- }
|
|
|
-
|
|
|
- visibleZoomRange.value = {
|
|
|
- startIndex: Math.min(nextStartIndex, nextEndIndex),
|
|
|
- endIndex: Math.max(nextStartIndex, nextEndIndex)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const getVisibleDatasetValues = (dataset: number[]) => {
|
|
|
- if (!dataset.length) return []
|
|
|
-
|
|
|
- const lastIndex = dataset.length - 1
|
|
|
- const startIndex = clampZoomIndex(visibleZoomRange.value.startIndex, lastIndex)
|
|
|
- const endIndex = Math.max(startIndex, clampZoomIndex(visibleZoomRange.value.endIndex, lastIndex))
|
|
|
-
|
|
|
- return dataset.slice(startIndex, endIndex + 1).filter((value) => Number.isFinite(value))
|
|
|
-}
|
|
|
-
|
|
|
-const normalizeSeriesData = (dataset: number[]) => {
|
|
|
- if (!dataset.length) return []
|
|
|
-
|
|
|
- const validValues = getVisibleDatasetValues(dataset)
|
|
|
-
|
|
|
- if (!validValues.length) {
|
|
|
- return dataset.map(() => NORMALIZED_AXIS_MIN)
|
|
|
- }
|
|
|
-
|
|
|
- const min = Math.min(...validValues)
|
|
|
- const max = Math.max(...validValues)
|
|
|
-
|
|
|
- if (min === max) {
|
|
|
- return dataset.map(() => (min === 0 ? NORMALIZED_AXIS_MIN : NORMALIZED_AXIS_CENTER))
|
|
|
- }
|
|
|
-
|
|
|
- const usableHeight = NORMALIZED_AXIS_MAX - NORMALIZED_AXIS_MIN - NORMALIZED_AXIS_PADDING * 2
|
|
|
-
|
|
|
- return dataset.map((value) =>
|
|
|
- Number((NORMALIZED_AXIS_PADDING + ((value - min) / (max - min)) * usableHeight).toFixed(2))
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-const formatTrendAxisLabel = (value: number) => {
|
|
|
- if (value === NORMALIZED_AXIS_MIN) return '低'
|
|
|
- if (value === NORMALIZED_AXIS_CENTER) return '中'
|
|
|
- if (value === NORMALIZED_AXIS_MAX) return '高'
|
|
|
- return ''
|
|
|
-}
|
|
|
-
|
|
|
-const formatSeriesValue = (name: string, value: number) => {
|
|
|
- const item = legendItemMap[name]
|
|
|
- const safeValue = Number.isFinite(value) ? value : 0
|
|
|
-
|
|
|
- if (!item) return safeValue.toFixed(2)
|
|
|
-
|
|
|
- return `${safeValue.toFixed(item.decimals)} ${item.unit}`
|
|
|
-}
|
|
|
-
|
|
|
-const getSeries = () => {
|
|
|
- const enableSampling = xAxisData.value.length > 120
|
|
|
-
|
|
|
- return legendItems.map((item) => ({
|
|
|
- name: item.name,
|
|
|
- type: 'line',
|
|
|
- smooth: false,
|
|
|
- showSymbol: true,
|
|
|
- symbol: 'circle',
|
|
|
- symbolSize: 6,
|
|
|
- connectNulls: true,
|
|
|
- lineStyle: {
|
|
|
- width: 2
|
|
|
- },
|
|
|
- emphasis: {
|
|
|
- focus: 'series'
|
|
|
- },
|
|
|
- sampling: enableSampling ? 'lttb' : undefined,
|
|
|
- progressive: 300,
|
|
|
- progressiveThreshold: 1500,
|
|
|
- data: normalizeSeriesData(chartData.value[item.key])
|
|
|
- }))
|
|
|
-}
|
|
|
-
|
|
|
-const getChartOption = () => ({
|
|
|
- animation: xAxisData.value.length <= 120,
|
|
|
- animationDuration: 280,
|
|
|
- animationDurationUpdate: 180,
|
|
|
- grid: {
|
|
|
- top: 72,
|
|
|
- right: 24,
|
|
|
- bottom: 68,
|
|
|
- left: 48,
|
|
|
- containLabel: true
|
|
|
- },
|
|
|
- tooltip: {
|
|
|
- trigger: 'axis',
|
|
|
- axisPointer: { type: 'line' },
|
|
|
- formatter: (params: any) => {
|
|
|
- const list = Array.isArray(params) ? params : [params]
|
|
|
-
|
|
|
- if (!list.length) return ''
|
|
|
-
|
|
|
- const content = list.map((item: any) => {
|
|
|
- const legendItem = legendItemMap[item.seriesName]
|
|
|
- const realValue = legendItem ? (chartData.value[legendItem.key][item.dataIndex] ?? 0) : 0
|
|
|
-
|
|
|
- return `<div class="flex items-center justify-between mt-1 gap-1">
|
|
|
- <span>${item.marker} ${item.seriesName}</span>
|
|
|
- <span>${formatSeriesValue(item.seriesName, realValue)}</span>
|
|
|
- </div>`
|
|
|
- })
|
|
|
-
|
|
|
- return `${list[0].axisValueLabel}<br>${content.join('')}`
|
|
|
- }
|
|
|
- },
|
|
|
- legend: {
|
|
|
- type: 'scroll',
|
|
|
- top: 16,
|
|
|
- data: legendItems.map((item) => item.name),
|
|
|
- selected: selectedLegends.value,
|
|
|
- show: true
|
|
|
- },
|
|
|
- xAxis: {
|
|
|
- type: 'category',
|
|
|
- boundaryGap: false,
|
|
|
- data: xAxisData.value,
|
|
|
- axisLabel: {
|
|
|
- hideOverlap: true
|
|
|
- }
|
|
|
- },
|
|
|
- dataZoom: [
|
|
|
- {
|
|
|
- type: 'inside',
|
|
|
- xAxisIndex: 0,
|
|
|
- filterMode: 'none',
|
|
|
- throttle: 50,
|
|
|
- startValue: visibleZoomRange.value.startIndex,
|
|
|
- endValue: visibleZoomRange.value.endIndex
|
|
|
- },
|
|
|
- {
|
|
|
- type: 'slider',
|
|
|
- xAxisIndex: 0,
|
|
|
- filterMode: 'none',
|
|
|
- height: 18,
|
|
|
- bottom: 8,
|
|
|
- brushSelect: false,
|
|
|
- showDetail: false,
|
|
|
- moveHandleSize: 0,
|
|
|
- throttle: 50,
|
|
|
- startValue: visibleZoomRange.value.startIndex,
|
|
|
- endValue: visibleZoomRange.value.endIndex
|
|
|
- }
|
|
|
- ],
|
|
|
- yAxis: {
|
|
|
- type: 'value',
|
|
|
- min: NORMALIZED_AXIS_MIN,
|
|
|
- max: NORMALIZED_AXIS_MAX,
|
|
|
- splitNumber: 4,
|
|
|
- name: '相对趋势',
|
|
|
- nameGap: 16,
|
|
|
- axisLabel: {
|
|
|
- formatter: formatTrendAxisLabel
|
|
|
- },
|
|
|
- splitLine: {
|
|
|
- lineStyle: {
|
|
|
- type: 'dashed'
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- series: getSeries()
|
|
|
-})
|
|
|
-
|
|
|
-const initChart = () => {
|
|
|
- if (!chartRef.value) return
|
|
|
-
|
|
|
- if (chart && chartContainerEl === chartRef.value) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- chart?.dispose()
|
|
|
- window.removeEventListener('resize', resizer)
|
|
|
- chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas', useDirtyRect: true })
|
|
|
- chartContainerEl = chartRef.value
|
|
|
- window.addEventListener('resize', resizer)
|
|
|
-
|
|
|
- chart.off('legendselectchanged')
|
|
|
- chart.on('legendselectchanged', (params: any) => {
|
|
|
- selectedLegends.value = { ...params.selected }
|
|
|
- })
|
|
|
-
|
|
|
- chart.off('datazoom')
|
|
|
- chart.on('datazoom', () => {
|
|
|
- syncVisibleZoomRangeFromChart()
|
|
|
- chart?.setOption(
|
|
|
- {
|
|
|
- series: getSeries()
|
|
|
- },
|
|
|
- { lazyUpdate: true }
|
|
|
- )
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-const render = () => {
|
|
|
- if (!chartRef.value) return
|
|
|
-
|
|
|
- initChart()
|
|
|
-
|
|
|
- ensureLegendSelection()
|
|
|
-
|
|
|
- chart?.setOption(getChartOption(), { notMerge: true, lazyUpdate: true })
|
|
|
-}
|
|
|
-
|
|
|
-onUnmounted(() => {
|
|
|
- window.removeEventListener('resize', resizer)
|
|
|
- chart?.dispose()
|
|
|
- chart = null
|
|
|
- chartContainerEl = null
|
|
|
-})
|
|
|
+const refreshKey = ref(0)
|
|
|
|
|
|
const handleDeptNodeClick = (node: any) => {
|
|
|
deptName.value = node.name
|
|
|
handleQuery()
|
|
|
}
|
|
|
|
|
|
-const handleQuery = (setPage = true) => {
|
|
|
- if (setPage) {
|
|
|
- query.value.pageNo = 1
|
|
|
- }
|
|
|
- getChart().then(() => {
|
|
|
- render()
|
|
|
- })
|
|
|
- getList()
|
|
|
- getTotal()
|
|
|
+const handleQuery = () => {
|
|
|
+ query.value.pageNo = 1
|
|
|
+ refreshKey.value += 1
|
|
|
}
|
|
|
|
|
|
const resetQuery = () => {
|
|
|
- query.value = {
|
|
|
- pageNo: 1,
|
|
|
- pageSize: 10,
|
|
|
- deptId: deptId,
|
|
|
- contractName: '',
|
|
|
- taskName: '',
|
|
|
- createTime: [
|
|
|
- ...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
|
|
|
- ]
|
|
|
- }
|
|
|
- handleQuery()
|
|
|
-}
|
|
|
-
|
|
|
-watch(
|
|
|
- () => query.value.createTime,
|
|
|
- (val) => {
|
|
|
- if (!val) {
|
|
|
- totalWork.value.totalCount = 0
|
|
|
- totalWork.value.notReported = 0
|
|
|
- totalWork.value.alreadyReported = 0
|
|
|
- }
|
|
|
- handleQuery(false)
|
|
|
- }
|
|
|
-)
|
|
|
-
|
|
|
-watch([() => query.value.contractName, () => query.value.taskName], () => {
|
|
|
- handleQuery(false)
|
|
|
-})
|
|
|
-
|
|
|
-onMounted(() => {
|
|
|
+ query.value = createDefaultQuery()
|
|
|
+ deptName.value = '瑞恒兴域'
|
|
|
handleQuery()
|
|
|
-})
|
|
|
-
|
|
|
-const exportChart = () => {
|
|
|
- if (!chart) return
|
|
|
- let img = new Image()
|
|
|
- img.src = chart.getDataURL({
|
|
|
- type: 'png',
|
|
|
- pixelRatio: 1,
|
|
|
- backgroundColor: '#fff'
|
|
|
- })
|
|
|
-
|
|
|
- img.onload = function () {
|
|
|
- let canvas = document.createElement('canvas')
|
|
|
- canvas.width = img.width
|
|
|
- canvas.height = img.height
|
|
|
- let ctx = canvas.getContext('2d')
|
|
|
- ctx?.drawImage(img, 0, 0)
|
|
|
- let dataURL = canvas.toDataURL('image/png')
|
|
|
-
|
|
|
- let a = document.createElement('a')
|
|
|
-
|
|
|
- let event = new MouseEvent('click')
|
|
|
-
|
|
|
- a.href = dataURL
|
|
|
- a.download = `瑞恒日报统计数据.png`
|
|
|
- a.dispatchEvent(event)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const exportData = async () => {
|
|
|
- const res = await IotRhDailyReportApi.exportRhDailyReportStatistics(query.value)
|
|
|
-
|
|
|
- download.excel(res, '瑞恒日报统计数据.xlsx')
|
|
|
-}
|
|
|
-
|
|
|
-const exportAll = async () => {
|
|
|
- if (tab.value === '看板') exportChart()
|
|
|
- else exportData()
|
|
|
}
|
|
|
-
|
|
|
-const message = useMessage()
|
|
|
-
|
|
|
-const unfilledDialogRef = ref()
|
|
|
-
|
|
|
-const openUnfilledDialog = () => {
|
|
|
- // 检查是否选择了创建时间
|
|
|
- if (!query.value.createTime || query.value.createTime.length === 0) {
|
|
|
- message.warning('请先选择创建时间范围')
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 打开弹窗
|
|
|
- unfilledDialogRef.value?.open()
|
|
|
-}
|
|
|
-
|
|
|
-const router = useRouter()
|
|
|
-
|
|
|
-const tolist = (id: number, non: boolean = false) => {
|
|
|
- const { pageNo, pageSize, ...rest } = query.value
|
|
|
-
|
|
|
- router.push({
|
|
|
- path: '/iotdayilyreport/IotRhDailyReport',
|
|
|
- query: {
|
|
|
- ...rest,
|
|
|
- deptId: id,
|
|
|
- ...(non ? { nonProductFlag: 'Y' } : {})
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-const { ZmTable, ZmTableColumn } = useTableComponents()
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div
|
|
|
- class="grid grid-cols-[auto_1fr] grid-rows-[62px_128px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
|
|
|
+ class="grid grid-cols-[auto_1fr] grid-rows-[62px_48px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
|
|
|
>
|
|
|
<DeptTreeSelect
|
|
|
:deptId="id"
|
|
|
@@ -774,9 +62,6 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
|
|
|
@node-click="handleDeptNodeClick"
|
|
|
class="row-span-3"
|
|
|
/>
|
|
|
- <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-3">
|
|
|
-
|
|
|
- </div> -->
|
|
|
<el-form
|
|
|
size="default"
|
|
|
class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 gap-8 flex items-center justify-between"
|
|
|
@@ -820,147 +105,37 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
|
|
|
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
- <div class="grid grid-cols-8 gap-8">
|
|
|
- <template v-for="info in totalWorkKeys" :key="info[0]">
|
|
|
- <el-tooltip :disabled="info[0] !== 'totalGasInjection'" placement="top">
|
|
|
- <template #content>
|
|
|
- <div>
|
|
|
- 累计氮气注气量:{{
|
|
|
- formatGasInjectionTooltipValue(totalWorkloadDetail.totalN2GasInjection)
|
|
|
- }}
|
|
|
- 万方
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- 累计天然气注气量:{{
|
|
|
- formatGasInjectionTooltipValue(totalWorkloadDetail.totalNaturalGasInjection)
|
|
|
- }}
|
|
|
- 万方
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <div
|
|
|
- class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow p-1 flex flex-col items-center justify-center gap-1"
|
|
|
- >
|
|
|
- <div class="size-7.5" :class="info[3]"></div>
|
|
|
-
|
|
|
- <count-to
|
|
|
- class="text-2xl font-medium"
|
|
|
- :class="{ 'cursor-help': info[0] === 'totalGasInjection' }"
|
|
|
- :start-val="0"
|
|
|
- :end-val="totalWork[info[0]]"
|
|
|
- :decimals="info[4]"
|
|
|
- @click="info[2] === '未填报' ? openUnfilledDialog() : ''"
|
|
|
- >
|
|
|
- <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
|
|
|
- </count-to>
|
|
|
|
|
|
- <div class="text-sm font-medium text-[var(--el-text-color-regular)] whitespace-nowrap">
|
|
|
- {{ info[1] ? info[2] + '(' + info[1] + ')' : info[2] }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-tooltip>
|
|
|
- </template>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col p-4 gap-2">
|
|
|
- <div class="flex h-12 items-center justify-between">
|
|
|
- <el-button-group>
|
|
|
- <el-button
|
|
|
- size="default"
|
|
|
- :type="tab === '表格' ? 'primary' : 'default'"
|
|
|
- @click="handleSelectTab('表格')"
|
|
|
- >表格
|
|
|
- </el-button>
|
|
|
- <el-button
|
|
|
- size="default"
|
|
|
- :type="tab === '看板' ? 'primary' : 'default'"
|
|
|
- @click="handleSelectTab('看板')"
|
|
|
- >看板
|
|
|
- </el-button>
|
|
|
- </el-button-group>
|
|
|
- <h3 class="text-xl font-medium">{{ `${deptName}-${tab}` }}</h3>
|
|
|
- <el-button size="default" type="primary" @click="exportAll">导出</el-button>
|
|
|
- </div>
|
|
|
- <div class="flex-1 relative">
|
|
|
- <el-auto-resizer class="absolute">
|
|
|
- <template #default="{ height }">
|
|
|
- <Motion
|
|
|
- as="div"
|
|
|
- :style="{ position: 'relative', overflow: 'hidden' }"
|
|
|
- :animate="{ height: `${height}px`, width: `100%` }"
|
|
|
- :transition="{ type: 'spring', stiffness: 200, damping: 25, duration: 0.3 }"
|
|
|
- >
|
|
|
- <AnimatePresence :initial="false" mode="sync">
|
|
|
- <Motion
|
|
|
- :key="currentTab"
|
|
|
- as="div"
|
|
|
- :initial="{ x: direction === 'left' ? '-100%' : '100%', opacity: 0 }"
|
|
|
- :animate="{ x: '0%', opacity: 1 }"
|
|
|
- :exit="{ x: direction === 'left' ? '50%' : '-50%', opacity: 0 }"
|
|
|
- :transition="{ type: 'tween', stiffness: 300, damping: 30, duration: 0.3 }"
|
|
|
- :style="{ position: 'absolute', left: 0, right: 0, top: 0 }"
|
|
|
- >
|
|
|
- <div :style="{ width: `100%`, height: `${height}px` }">
|
|
|
- <zm-table
|
|
|
- v-if="currentTab === '表格'"
|
|
|
- :loading="listLoading"
|
|
|
- :data="list"
|
|
|
- :height="height"
|
|
|
- show-border
|
|
|
- >
|
|
|
- <template v-for="item in columns(type)" :key="item.prop">
|
|
|
- <zm-table-column
|
|
|
- v-if="item.prop !== 'name' && item.prop !== 'nonProductiveTime'"
|
|
|
- :label="item.label"
|
|
|
- :prop="item.prop"
|
|
|
- :formatter="formatter"
|
|
|
- :action="item.action"
|
|
|
- />
|
|
|
- <zm-table-column
|
|
|
- v-else-if="item.prop === 'name'"
|
|
|
- :label="item.label"
|
|
|
- :prop="item.prop"
|
|
|
- >
|
|
|
- <template #default="{ row }">
|
|
|
- <el-button text type="primary" @click.prevent="tolist(row.id)">{{
|
|
|
- row.name
|
|
|
- }}</el-button>
|
|
|
- </template>
|
|
|
- </zm-table-column>
|
|
|
- <zm-table-column v-else :label="item.label" :prop="item.prop">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-button
|
|
|
- v-if="row.nonProductiveTime > 0"
|
|
|
- text
|
|
|
- type="primary"
|
|
|
- @click.prevent="tolist(row.id, true)"
|
|
|
- >
|
|
|
- {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
|
|
|
- </el-button>
|
|
|
- <span v-else>
|
|
|
- {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
|
|
|
- </span>
|
|
|
- </template>
|
|
|
- </zm-table-column>
|
|
|
- </template>
|
|
|
- </zm-table>
|
|
|
- <div
|
|
|
- ref="chartRef"
|
|
|
- v-loading="chartLoading"
|
|
|
- v-else
|
|
|
- :style="{ width: `100%`, height: `${height}px` }"
|
|
|
- >
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </Motion>
|
|
|
- </AnimatePresence>
|
|
|
- </Motion>
|
|
|
- </template>
|
|
|
- </el-auto-resizer>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <el-button-group class="justify-self-start self-center">
|
|
|
+ <el-button
|
|
|
+ size="default"
|
|
|
+ :type="activeTab === '日报统计' ? 'primary' : 'default'"
|
|
|
+ @click="activeTab = '日报统计'"
|
|
|
+ >
|
|
|
+ 日报统计
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ size="default"
|
|
|
+ :type="activeTab === '非生产时效' ? 'primary' : 'default'"
|
|
|
+ @click="activeTab = '非生产时效'"
|
|
|
+ >
|
|
|
+ 非生产时效
|
|
|
+ </el-button>
|
|
|
+ </el-button-group>
|
|
|
+
|
|
|
+ <DailyStatistics
|
|
|
+ v-if="activeTab === '日报统计'"
|
|
|
+ :query="query"
|
|
|
+ :dept-name="deptName"
|
|
|
+ :refresh-key="refreshKey"
|
|
|
+ />
|
|
|
+ <NonProductionEfficiency
|
|
|
+ v-else
|
|
|
+ :query="query"
|
|
|
+ :dept-name="deptName"
|
|
|
+ :refresh-key="refreshKey"
|
|
|
+ />
|
|
|
</div>
|
|
|
-
|
|
|
- <UnfilledReportDialog ref="unfilledDialogRef" :query-params="query" />
|
|
|
</template>
|
|
|
|
|
|
<style scoped>
|