|
@@ -0,0 +1,291 @@
|
|
|
|
|
+<script lang="ts" setup>
|
|
|
|
|
+import {
|
|
|
|
|
+ IotProjectTaskPlanApi,
|
|
|
|
|
+ type IotProjectTaskPlanSummaryItem,
|
|
|
|
|
+ type IotProjectTaskPlanSummaryParams,
|
|
|
|
|
+ type IotProjectTaskPlanSummaryVO
|
|
|
|
|
+} from '@/api/pms/iotprojecttaskplan'
|
|
|
|
|
+import { useUserStore } from '@/store/modules/user'
|
|
|
|
|
+import { rangeShortcuts } from '@/utils/formatTime'
|
|
|
|
|
+import { useDebounceFn } from '@vueuse/core'
|
|
|
|
|
+import dayjs from 'dayjs'
|
|
|
|
|
+import { TableColumnCtx } from 'element-plus'
|
|
|
|
|
+
|
|
|
|
|
+defineOptions({ name: 'PlanWork' })
|
|
|
|
|
+
|
|
|
|
|
+const deptId = useUserStore().getUser.deptId
|
|
|
|
|
+
|
|
|
|
|
+const createDefaultQuery = (): IotProjectTaskPlanSummaryParams => ({
|
|
|
|
|
+ pageNo: 1,
|
|
|
|
|
+ pageSize: 10,
|
|
|
|
|
+ deptId,
|
|
|
|
|
+ startTime: [...rangeShortcuts[8].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))]
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const query = ref<IotProjectTaskPlanSummaryParams>(createDefaultQuery())
|
|
|
|
|
+const loading = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+type MetricField = Exclude<keyof IotProjectTaskPlanSummaryItem, 'yearMonth'>
|
|
|
|
|
+
|
|
|
|
|
+interface MonthColumn {
|
|
|
|
|
+ key: string
|
|
|
|
|
+ label: string
|
|
|
|
|
+ sortValue: number
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface SummaryTableRow {
|
|
|
|
|
+ projectDeptName: string
|
|
|
|
|
+ business: string
|
|
|
|
|
+ unit: string
|
|
|
|
|
+ monthValues: Record<string, number | null>
|
|
|
|
|
+ total: number | null
|
|
|
|
|
+ projectRowSpan: number
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const BUSINESS_METRICS: Record<string, { field: MetricField; unit: string }> = {
|
|
|
|
|
+ 注气: { field: 'cumulativeGasInjection', unit: '万方' },
|
|
|
|
|
+ 压裂: { field: 'cumulativeFracLayer', unit: '层' },
|
|
|
|
|
+ 连油: { field: 'cumulativeWellCount', unit: '井' },
|
|
|
|
|
+ 钻井: { field: 'cumulativeFootage', unit: '米' },
|
|
|
|
|
+ 修井: { field: 'cumulativeWellCount', unit: '井' },
|
|
|
|
|
+ 井下: { field: 'cumulativeWellCount', unit: '井' }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const monthColumns = ref<MonthColumn[]>([])
|
|
|
|
|
+const tableData = ref<SummaryTableRow[]>([])
|
|
|
|
|
+
|
|
|
|
|
+const normalizeMonth = (yearMonth: string): MonthColumn => {
|
|
|
|
|
+ const matched = String(yearMonth).match(/^(\d{4})[-/](\d{1,2})/)
|
|
|
|
|
+
|
|
|
|
|
+ if (!matched) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ key: String(yearMonth),
|
|
|
|
|
+ label: String(yearMonth),
|
|
|
|
|
+ sortValue: Number.MAX_SAFE_INTEGER
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const year = Number(matched[1])
|
|
|
|
|
+ const month = Number(matched[2])
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ key: `${year}-${String(month).padStart(2, '0')}`,
|
|
|
|
|
+ label: `${year}-${month}`,
|
|
|
|
|
+ sortValue: year * 100 + month
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const toNumber = (value: number | string | null | undefined): number | null => {
|
|
|
|
|
+ if (value === null || value === undefined || value === '') return null
|
|
|
|
|
+
|
|
|
|
|
+ const numberValue = Number(value)
|
|
|
|
|
+ return Number.isFinite(numberValue) ? numberValue : null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const splitBusinesses = (business: string) => {
|
|
|
|
|
+ const businesses = String(business || '')
|
|
|
|
|
+ .split(/[,,、]/)
|
|
|
|
|
+ .map((item) => item.trim())
|
|
|
|
|
+ .filter(Boolean)
|
|
|
|
|
+
|
|
|
|
|
+ return businesses.length ? businesses : ['-']
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const buildTableData = (list: IotProjectTaskPlanSummaryVO[]) => {
|
|
|
|
|
+ const columnMap = new Map<string, MonthColumn>()
|
|
|
|
|
+
|
|
|
|
|
+ list.forEach((project) => {
|
|
|
|
|
+ ;(project.items || []).forEach((item) => {
|
|
|
|
|
+ const column = normalizeMonth(item.yearMonth)
|
|
|
|
|
+ columnMap.set(column.key, column)
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ monthColumns.value = Array.from(columnMap.values()).sort(
|
|
|
|
|
+ (left, right) => left.sortValue - right.sortValue || left.label.localeCompare(right.label)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ tableData.value = list.flatMap((project) => {
|
|
|
|
|
+ const businesses = splitBusinesses(project.business)
|
|
|
|
|
+ const itemsByMonth = new Map(
|
|
|
|
|
+ (project.items || []).map((item) => [normalizeMonth(item.yearMonth).key, item])
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return businesses.map((business, businessIndex) => {
|
|
|
|
|
+ const metric = BUSINESS_METRICS[business]
|
|
|
|
|
+ const monthValues: Record<string, number | null> = {}
|
|
|
|
|
+
|
|
|
|
|
+ monthColumns.value.forEach((month) => {
|
|
|
|
|
+ const item = itemsByMonth.get(month.key)
|
|
|
|
|
+ monthValues[month.key] = metric && item ? toNumber(item[metric.field]) : null
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ projectDeptName: project.projectDeptName || '-',
|
|
|
|
|
+ business,
|
|
|
|
|
+ unit: metric?.unit || '-',
|
|
|
|
|
+ monthValues,
|
|
|
|
|
+ total: metric ? toNumber(project[metric.field]) : null,
|
|
|
|
|
+ projectRowSpan: businessIndex === 0 ? businesses.length : 0
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const formatValue = (value: number | null) => (value === null ? '-' : value)
|
|
|
|
|
+
|
|
|
|
|
+const sumValues = (values: Array<number | null>): string => {
|
|
|
|
|
+ const validValues = values.filter((value): value is number => value !== null)
|
|
|
|
|
+ if (!validValues.length) return '-'
|
|
|
|
|
+
|
|
|
|
|
+ return String(Number(validValues.reduce((sum, value) => sum + value, 0).toFixed(10)))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface SummaryMethodProps<T extends SummaryTableRow = SummaryTableRow> {
|
|
|
|
|
+ columns: TableColumnCtx<T>[]
|
|
|
|
|
+ data: T[]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getColumnSummaries = ({ columns, data }: SummaryMethodProps) => {
|
|
|
|
|
+ return columns.map((column) => {
|
|
|
|
|
+ const property = column.property
|
|
|
|
|
+
|
|
|
|
|
+ if (property === 'projectDeptName') return '合计'
|
|
|
|
|
+ if (property === 'total') return sumValues(data.map((row) => row.total))
|
|
|
|
|
+
|
|
|
|
|
+ if (property?.startsWith('month-')) {
|
|
|
|
|
+ const monthKey = property.slice('month-'.length)
|
|
|
|
|
+ return sumValues(data.map((row) => row.monthValues[monthKey] ?? null))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return ''
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const tableSpanMethod = ({
|
|
|
|
|
+ row,
|
|
|
|
|
+ column
|
|
|
|
|
+}: {
|
|
|
|
|
+ row: SummaryTableRow
|
|
|
|
|
+ column: { property?: string }
|
|
|
|
|
+}) => {
|
|
|
|
|
+ if (column.property !== 'projectDeptName') return [1, 1]
|
|
|
|
|
+ return row.projectRowSpan ? [row.projectRowSpan, 1] : [0, 0]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const loadSummaryStatistics = useDebounceFn(async () => {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = await IotProjectTaskPlanApi.getSummaryStatistics(query.value)
|
|
|
|
|
+ console.log('计划工作汇总统计接口数据:', data)
|
|
|
|
|
+ buildTableData(Array.isArray(data?.list) ? data.list : [])
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}, 200)
|
|
|
|
|
+
|
|
|
|
|
+const handleQuery = () => {
|
|
|
|
|
+ query.value.pageNo = 1
|
|
|
|
|
+ loadSummaryStatistics()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const resetQuery = () => {
|
|
|
|
|
+ query.value = createDefaultQuery()
|
|
|
|
|
+ handleQuery()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ [() => query.value.deptId, () => query.value.startTime],
|
|
|
|
|
+ () => {
|
|
|
|
|
+ handleQuery()
|
|
|
|
|
+ },
|
|
|
|
|
+ { immediate: true }
|
|
|
|
|
+)
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="grid grid-cols-[auto_minmax(0,1fr)] grid-rows-[62px_minmax(0,1fr)] gap-4 w-full min-w-0 overflow-hidden h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
|
|
|
|
|
+ <DeptTreeSelect
|
|
|
|
|
+ :top-id="156"
|
|
|
|
|
+ :deptId="deptId"
|
|
|
|
|
+ v-model="query.deptId"
|
|
|
|
|
+ :show-title="false"
|
|
|
|
|
+ class="row-span-2" />
|
|
|
|
|
+
|
|
|
|
|
+ <el-form
|
|
|
|
|
+ size="default"
|
|
|
|
|
+ class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 flex items-center justify-between min-w-0 overflow-hidden">
|
|
|
|
|
+ <el-form-item label="时间范围">
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="query.startTime"
|
|
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
+ type="daterange"
|
|
|
|
|
+ start-placeholder="开始日期"
|
|
|
|
|
+ end-placeholder="结束日期"
|
|
|
|
|
+ :shortcuts="rangeShortcuts"
|
|
|
|
|
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
|
|
|
|
+ class="!w-260px" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-button type="primary" :loading="loading" @click="handleQuery">
|
|
|
|
|
+ <Icon icon="ep:search" class="mr-5px" />搜索
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow min-h-0 min-w-0 w-full max-w-full p-4 overflow-hidden relative">
|
|
|
|
|
+ <div class="relative w-full h-full min-w-0 min-h-0 overflow-hidden">
|
|
|
|
|
+ <el-auto-resizer class="absolute inset-0">
|
|
|
|
|
+ <template #default="{ width, height }">
|
|
|
|
|
+ <ZmTable
|
|
|
|
|
+ :data="tableData"
|
|
|
|
|
+ :loading="loading"
|
|
|
|
|
+ :width="width"
|
|
|
|
|
+ :height="height"
|
|
|
|
|
+ :max-height="height"
|
|
|
|
|
+ :span-method="tableSpanMethod"
|
|
|
|
|
+ :summary-method="getColumnSummaries"
|
|
|
|
|
+ :settings-cache="false"
|
|
|
|
|
+ empty-text="暂无数据"
|
|
|
|
|
+ show-summary
|
|
|
|
|
+ show-border>
|
|
|
|
|
+ <ZmTableColumn
|
|
|
|
|
+ prop="projectDeptName"
|
|
|
|
|
+ label="项目名称"
|
|
|
|
|
+ :min-width="180"
|
|
|
|
|
+ fixed="left"
|
|
|
|
|
+ show-overflow-tooltip />
|
|
|
|
|
+ <ZmTableColumn prop="business" label="项目业务" fixed="left" />
|
|
|
|
|
+ <ZmTableColumn
|
|
|
|
|
+ v-for="month in monthColumns"
|
|
|
|
|
+ :key="month.key"
|
|
|
|
|
+ :prop="`month-${month.key}`"
|
|
|
|
|
+ :column-key="`month-${month.key}`"
|
|
|
|
|
+ :label="month.label">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ {{ formatValue(row.monthValues[month.key]) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </ZmTableColumn>
|
|
|
|
|
+ <ZmTableColumn prop="total" label="合计" fixed="right">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ {{ formatValue(row.total) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </ZmTableColumn>
|
|
|
|
|
+ <ZmTableColumn prop="unit" label="单位" fixed="right" />
|
|
|
|
|
+ </ZmTable>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-auto-resizer>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+:deep(.el-form-item) {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|