Browse Source

feat(pms): 新增计划工作汇总表格

- 接入计划工作汇总统计接口
- 支持组织及年度时间范围筛选
- 动态生成月份列并拆分多项目业务
- 增加行合计、列合计及项目名称合并
- 使用 ZmTable 展示汇总数据
Zimo 1 tuần trước cách đây
mục cha
commit
50f4d9a6fc
2 tập tin đã thay đổi với 331 bổ sung0 xóa
  1. 40 0
      src/api/pms/iotprojecttaskplan/index.ts
  2. 291 0
      src/views/pms/plan-work/index.vue

+ 40 - 0
src/api/pms/iotprojecttaskplan/index.ts

@@ -0,0 +1,40 @@
+import request from '@/config/axios'
+
+export interface IotProjectTaskPlanSummaryParams {
+  pageNo: number
+  pageSize: number
+  deptId?: number
+  startTime?: string[]
+}
+
+export interface IotProjectTaskPlanSummaryItem {
+  yearMonth: string
+  cumulativeGasInjection?: number | string | null
+  cumulativeFootage?: number | string | null
+  cumulativeWellCount?: number | string | null
+  cumulativeFracLayer?: number | string | null
+}
+
+export interface IotProjectTaskPlanSummaryVO {
+  business: string
+  projectDeptName: string
+  items: IotProjectTaskPlanSummaryItem[]
+  cumulativeGasInjection?: number | string | null
+  cumulativeFootage?: number | string | null
+  cumulativeWellCount?: number | string | null
+  cumulativeFracLayer?: number | string | null
+}
+
+export interface IotProjectTaskPlanSummaryResult {
+  list: IotProjectTaskPlanSummaryVO[]
+  total: number
+}
+
+export const IotProjectTaskPlanApi = {
+  getSummaryStatistics: async (params: IotProjectTaskPlanSummaryParams) => {
+    return await request.get<IotProjectTaskPlanSummaryResult>({
+      url: '/pms/iot-project-task-plan/summaryStatistics',
+      params
+    })
+  }
+}

+ 291 - 0
src/views/pms/plan-work/index.vue

@@ -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>