소스 검색

5#国日报修改

yanghao 1 주 전
부모
커밋
c57abc560e

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

@@ -0,0 +1,113 @@
+import request from '@/config/axios'
+
+export interface IotProjectTaskPlanSummaryParams {
+  pageNo: number
+  pageSize: number
+  deptId?: number
+  startTime?: string[]
+}
+
+export type IotProjectTaskPlanPageParams = IotProjectTaskPlanSummaryParams
+
+export interface IotProjectTaskPlanPageVO {
+  id: number
+  deptName: string
+  business: string
+  yearMonth: string
+  workloadUnit: string
+  planGasInjection?: number | string | null
+  planLayers?: number | string | null
+  planWellTrips?: number | string | null
+  planFootage?: number | string | null
+}
+
+export interface IotProjectTaskPlanPageResult {
+  list: IotProjectTaskPlanPageVO[]
+  total: number
+}
+
+export interface IotProjectTaskPlanExtProperty {
+  identifier: string
+  [key: string]: unknown
+}
+
+export interface IotProjectTaskPlanCreateData {
+  deptId: number
+  companyId: number
+  startTime: number
+  business: string
+  workloadUnit: string
+  planGasInjection?: number
+  planLayers?: number
+  planWellTrips?: number
+  planFootage?: number
+}
+
+export interface IotProjectTaskPlanUpdateData extends IotProjectTaskPlanCreateData {
+  id: number
+}
+
+export interface IotProjectTaskPlanDetailVO extends IotProjectTaskPlanUpdateData {}
+
+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
+  teamName: string
+  companyName: 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 = {
+  create: async (data: IotProjectTaskPlanCreateData) => {
+    return await request.post<number>({
+      url: '/pms/iot-project-task-plan/create',
+      data
+    })
+  },
+  update: async (data: IotProjectTaskPlanUpdateData) => {
+    return await request.put<boolean>({
+      url: '/pms/iot-project-task-plan/update',
+      data
+    })
+  },
+  get: async (id: number) => {
+    return await request.get<IotProjectTaskPlanDetailVO>({
+      url: `/pms/iot-project-task-plan/get?id=${id}`
+    })
+  },
+  getPlanExtProperties: async (deptId: number) => {
+    return await request.get<IotProjectTaskPlanExtProperty[]>({
+      url: '/pms/iot-project-task-plan/planExtProperties',
+      params: { deptId }
+    })
+  },
+  getPage: async (params: IotProjectTaskPlanPageParams) => {
+    return await request.get<IotProjectTaskPlanPageResult>({
+      url: '/pms/iot-project-task-plan/page',
+      params
+    })
+  },
+  getSummaryStatistics: async (params: IotProjectTaskPlanSummaryParams) => {
+    return await request.get<IotProjectTaskPlanSummaryResult>({
+      url: '/pms/iot-project-task-plan/summaryStatistics',
+      params
+    })
+  }
+}

+ 1 - 0
src/utils/dict.ts

@@ -282,6 +282,7 @@ export enum DICT_TYPE {
   RQ_IOT_MODEL_TEMPLATE_ATTR = 'rq_iot_model_template_attr',
   RQ_IOT_MODEL_PLUS = 'rq_iot_model_plus',
   RQ_IOT_SUM = 'rq_iot_isSum', //是否累计
+  RQ_IOT_WORKLOAD_PLAN_BIZ = 'rq_iot_workload_plan_biz', // 计划工作业务
   PMS_ORDER_PROCESS_MODE = 'pms_main_work_order_process_mode', // 保养方式 0内部保养  1委外保养
   PMS_THING_MODEL_UNIT = 'pms_thing_model_unit', // pms属性模板单位
   PMS_PROJECT_TASK_SCHEDULE = 'constructionStatus', // 日报 项目管理 任务计划

+ 26 - 3
src/views/pms/iotrddailyreport/index.vue

@@ -2,7 +2,7 @@
 import { IotRdDailyReportApi } from '@/api/pms/iotrddailyreport'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { useUserStore } from '@/store/modules/user'
-import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import { DICT_TYPE, getDictOptions, getStrDictOptions } from '@/utils/dict'
 import { formatT, rangeShortcuts } from '@/utils/formatTime'
 import { useDebounceFn } from '@vueuse/core'
 import dayjs from 'dayjs'
@@ -29,6 +29,7 @@ interface Query {
   createTime?: string[]
   wellName?: string
   taskId?: number
+  rdStatus?: string
   pageNo: number
   pageSize: number
 }
@@ -53,13 +54,24 @@ const getRouteTaskName = () => {
   return typeof taskName === 'string' ? taskName : undefined
 }
 
+const getRouteRdStatus = () => {
+  const nptName = route.query.nptName
+
+  if (Array.isArray(nptName)) {
+    return nptName.find((item): item is string => typeof item === 'string')
+  }
+
+  return typeof nptName === 'string' ? nptName : undefined
+}
+
 const initQuery: Query = {
   pageNo: 1,
   pageSize: 10,
   deptId: route.query.deptId ? Number(route.query.deptId) : id,
   createTime: getRouteCreateTime(),
   taskName: getRouteTaskName(),
-  taskId: route.query.taskId ? Number(route.query.taskId) : undefined
+  taskId: route.query.taskId ? Number(route.query.taskId) : undefined,
+  rdStatus: getRouteRdStatus()
 }
 
 const query = ref<Query>({ ...initQuery })
@@ -109,6 +121,7 @@ const list = ref<ListItem[]>([])
 const total = ref(0)
 
 const loading = ref(false)
+const rdStatusOptions = getStrDictOptions(DICT_TYPE.PMS_PROJECT_RD_STATUS)
 
 const getReportDetailsDuration = (reportDetails: any[] = []) =>
   Number(reportDetails.reduce((sum, item) => sum + Number(item.duration || 0), 0).toFixed(2))
@@ -171,6 +184,7 @@ watch(
     () => query.value.deptId,
     () => query.value.contractName,
     () => query.value.taskName,
+    () => query.value.rdStatus,
     () => query.value.createTime
   ],
   () => {
@@ -180,11 +194,12 @@ watch(
 )
 
 watch(
-  () => [route.query.deptId, route.query.createTime, route.query.taskName],
+  () => [route.query.deptId, route.query.createTime, route.query.taskName, route.query.nptName],
   () => {
     query.value.deptId = route.query.deptId ? Number(route.query.deptId) : id
     query.value.createTime = getRouteCreateTime()
     query.value.taskName = getRouteTaskName()
+    query.value.rdStatus = getRouteRdStatus()
   }
 )
 
@@ -329,6 +344,14 @@ onMounted(() => {
             class="!w-300px max-w-full"
             :shortcuts="rangeShortcuts" />
         </el-form-item>
+        <el-form-item label="施工状态" prop="rdStatus">
+          <el-select
+            v-model="query.rdStatus"
+            :options="rdStatusOptions"
+            placeholder="请选择施工状态"
+            clearable
+            class="!w-180px" />
+        </el-form-item>
       </div>
       <el-form-item class="daily-report-actions shrink-0">
         <div class="flex flex-wrap items-center justify-end gap-3">

+ 659 - 0
src/views/pms/plan-work/fill.vue

@@ -0,0 +1,659 @@
+<script lang="ts" setup>
+import {
+  IotProjectTaskPlanApi,
+  type IotProjectTaskPlanCreateData,
+  type IotProjectTaskPlanExtProperty,
+  type IotProjectTaskPlanPageParams,
+  type IotProjectTaskPlanPageVO,
+  type IotProjectTaskPlanUpdateData
+} from '@/api/pms/iotprojecttaskplan'
+import * as DeptApi from '@/api/system/dept'
+import { useUserStore } from '@/store/modules/user'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { rangeShortcuts } from '@/utils/formatTime'
+import { defaultProps, handleTree } from '@/utils/tree'
+import { useDebounceFn } from '@vueuse/core'
+import dayjs from 'dayjs'
+import type { FormInstance, FormRules } from 'element-plus'
+
+defineOptions({ name: 'PlanWorkFill' })
+
+const TOP_DEPT_ID = 156
+const deptId = useUserStore().getUser.deptId
+const message = useMessage()
+
+const createDefaultQuery = (): IotProjectTaskPlanPageParams => ({
+  pageNo: 1,
+  pageSize: 10,
+  deptId,
+  startTime: [...rangeShortcuts[8].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))]
+})
+
+const query = ref<IotProjectTaskPlanPageParams>(createDefaultQuery())
+const loading = ref(false)
+const list = ref<IotProjectTaskPlanPageVO[]>([])
+const total = ref(0)
+
+type PlanField = 'planGasInjection' | 'planLayers' | 'planWellTrips' | 'planFootage'
+
+interface BusinessPlanConfig {
+  field: PlanField
+  unit: string
+}
+
+interface DeptOption {
+  id: number
+  name: string
+  parentId?: number
+  type?: string
+  disabled?: boolean
+  children?: DeptOption[]
+}
+
+interface CreatePlanForm {
+  id?: number
+  deptId?: number
+  companyId?: number
+  startTime?: number | string
+  business: string
+  workloadUnit: string
+  planWorkload?: number
+}
+
+type FormMode = 'create' | 'edit' | 'view'
+
+const BUSINESS_PLAN_CONFIG: Record<string, BusinessPlanConfig> = {
+  注气: { field: 'planGasInjection', unit: '万方' },
+  压裂: { field: 'planLayers', unit: '层' },
+  连油: { field: 'planWellTrips', unit: '井' },
+  钻井: { field: 'planFootage', unit: '米' },
+  修井: { field: 'planWellTrips', unit: '井' },
+  井下: { field: 'planWellTrips', unit: '井' }
+}
+
+const businessOptions = getStrDictOptions(DICT_TYPE.RQ_IOT_WORKLOAD_PLAN_BIZ)
+const deptTreeProps = { ...defaultProps, disabled: 'disabled' }
+
+const getBusinessPlanConfig = (business: string) => {
+  const businessValue = String(business || '').trim()
+  const businessLabel = businessOptions.find((option) => option.value === businessValue)?.label
+  return BUSINESS_PLAN_CONFIG[businessValue] || BUSINESS_PLAN_CONFIG[businessLabel || '']
+}
+
+const getPlanWorkload = (row: IotProjectTaskPlanPageVO) => {
+  const config = getBusinessPlanConfig(row.business)
+  if (!config) return '-'
+
+  const value = row[config.field]
+  return value === null || value === undefined || value === '' ? '-' : value
+}
+
+const createDialogVisible = ref(false)
+const createLoading = ref(false)
+const formMode = ref<FormMode>('create')
+const deptOptionsLoading = ref(false)
+const extPropertiesLoading = ref(false)
+const deptOptions = ref<DeptOption[]>([])
+const flatDeptOptions = ref<DeptOption[]>([])
+const planExtProperties = ref<IotProjectTaskPlanExtProperty[]>([])
+const createFormRef = ref<FormInstance>()
+
+const isReadonly = computed(() => formMode.value === 'view')
+const dialogTitle = computed(() => {
+  if (formMode.value === 'edit') return '编辑计划工作'
+  if (formMode.value === 'view') return '查看计划工作'
+  return '新增计划工作'
+})
+const dialogDescription = computed(() =>
+  isReadonly.value ? '查看部门项目业务计划工作量详情' : '维护部门各月份的项目业务计划工作量'
+)
+
+const selectableBusinessOptions = computed(() => {
+  const identifiers = new Set(planExtProperties.value.map((property) => property.identifier))
+
+  return businessOptions.filter((option) => {
+    const config = getBusinessPlanConfig(option.value)
+    return config ? identifiers.has(config.field) : false
+  })
+})
+
+const createInitialForm = (): CreatePlanForm => ({
+  id: undefined,
+  deptId: undefined,
+  companyId: undefined,
+  startTime: dayjs().startOf('month').valueOf(),
+  business: '',
+  workloadUnit: '',
+  planWorkload: undefined
+})
+
+const createFormData = ref<CreatePlanForm>(createInitialForm())
+const createFormRules: FormRules = {
+  deptId: [{ required: true, message: '请选择部门', trigger: 'change' }],
+  startTime: [{ required: true, message: '请选择时间', trigger: 'change' }],
+  business: [{ required: true, message: '请选择项目业务', trigger: 'change' }],
+  planWorkload: [{ required: true, message: '请输入计划工作量', trigger: 'blur' }]
+}
+
+const loadDeptOptions = async () => {
+  if (deptOptions.value.length) return
+
+  deptOptionsLoading.value = true
+  try {
+    const data = (await DeptApi.specifiedSimpleDepts(TOP_DEPT_ID)) as DeptOption[]
+    flatDeptOptions.value = data.map((dept) => ({
+      ...dept,
+      type: String(dept.type || ''),
+      disabled: String(dept.type || '') !== '3'
+    }))
+    deptOptions.value = handleTree(flatDeptOptions.value)
+  } finally {
+    deptOptionsLoading.value = false
+  }
+}
+
+const findCompanyId = (selectedDeptId: number) => {
+  const deptMap = new Map(flatDeptOptions.value.map((dept) => [dept.id, dept]))
+  const visitedIds = new Set<number>()
+  let currentDept = deptMap.get(selectedDeptId)
+
+  while (currentDept && !visitedIds.has(currentDept.id)) {
+    visitedIds.add(currentDept.id)
+    if (String(currentDept.type) === '1') return currentDept.id
+    currentDept = currentDept.parentId ? deptMap.get(currentDept.parentId) : undefined
+  }
+
+  return undefined
+}
+
+const loadPlanExtProperties = async (selectedDeptId?: number) => {
+  planExtProperties.value = []
+  if (!selectedDeptId) return
+
+  extPropertiesLoading.value = true
+  try {
+    const data = await IotProjectTaskPlanApi.getPlanExtProperties(selectedDeptId)
+    planExtProperties.value = Array.isArray(data) ? data : []
+  } finally {
+    extPropertiesLoading.value = false
+  }
+}
+
+const handleDeptChange = async (selectedDeptId?: number) => {
+  createFormData.value.companyId = selectedDeptId ? findCompanyId(selectedDeptId) : undefined
+  createFormData.value.business = ''
+  createFormData.value.workloadUnit = ''
+  createFormData.value.planWorkload = undefined
+  await loadPlanExtProperties(selectedDeptId)
+}
+
+const normalizeStartTime = (value: unknown) => {
+  const timestamp = Number(value)
+  if (Number.isFinite(timestamp) && timestamp > 0) return timestamp
+
+  const parsedTimestamp = dayjs(value as string).valueOf()
+  return Number.isFinite(parsedTimestamp) ? parsedTimestamp : undefined
+}
+
+const handleOpenForm = async (mode: FormMode, id?: number) => {
+  formMode.value = mode
+  createFormData.value = createInitialForm()
+  createDialogVisible.value = true
+  createLoading.value = true
+  try {
+    await loadDeptOptions()
+
+    if (id) {
+      const data = await IotProjectTaskPlanApi.get(id)
+      const business = String(data.business || '')
+      const config = getBusinessPlanConfig(business)
+      const workloadValue = config ? data[config.field] : undefined
+
+      createFormData.value = {
+        id: data.id,
+        deptId: data.deptId,
+        companyId: data.companyId || findCompanyId(data.deptId),
+        startTime: normalizeStartTime(data.startTime),
+        business,
+        workloadUnit: data.workloadUnit || config?.unit || '',
+        planWorkload:
+          workloadValue === null || workloadValue === undefined ? undefined : Number(workloadValue)
+      }
+      await loadPlanExtProperties(data.deptId)
+    }
+
+    await nextTick()
+    createFormRef.value?.clearValidate()
+  } finally {
+    createLoading.value = false
+  }
+}
+
+const handleOpenCreate = () => handleOpenForm('create')
+
+const handleBusinessChange = () => {
+  const config = getBusinessPlanConfig(createFormData.value.business)
+  createFormData.value.workloadUnit = config?.unit || ''
+  createFormData.value.planWorkload = undefined
+}
+
+const submitCreate = async () => {
+  if (isReadonly.value) return
+  await createFormRef.value?.validate()
+
+  const {
+    deptId: formDeptId,
+    companyId,
+    startTime,
+    business,
+    workloadUnit,
+    planWorkload
+  } = createFormData.value
+  const config = getBusinessPlanConfig(business)
+
+  if (
+    !formDeptId ||
+    !companyId ||
+    !startTime ||
+    !workloadUnit ||
+    planWorkload === undefined ||
+    !config
+  ) {
+    message.error('表单数据不完整或项目业务未配置计划工作量字段')
+    return
+  }
+
+  const data: IotProjectTaskPlanCreateData = {
+    deptId: formDeptId,
+    companyId,
+    startTime: dayjs(Number(startTime)).startOf('month').valueOf(),
+    business,
+    workloadUnit
+  }
+  data[config.field] = planWorkload
+
+  createLoading.value = true
+  try {
+    if (formMode.value === 'edit') {
+      if (!createFormData.value.id) return
+      await IotProjectTaskPlanApi.update({
+        ...data,
+        id: createFormData.value.id
+      } as IotProjectTaskPlanUpdateData)
+      message.success('编辑成功')
+    } else {
+      await IotProjectTaskPlanApi.create(data)
+      message.success('新增成功')
+    }
+    createDialogVisible.value = false
+    handleQuery()
+  } finally {
+    createLoading.value = false
+  }
+}
+
+const loadPage = useDebounceFn(async () => {
+  loading.value = true
+  try {
+    const data = await IotProjectTaskPlanApi.getPage(query.value)
+    console.log('计划工作分页接口数据:', data)
+    list.value = Array.isArray(data?.list) ? data.list : []
+    total.value = Number(data?.total || 0)
+  } finally {
+    loading.value = false
+  }
+}, 200)
+
+const handleQuery = () => {
+  query.value.pageNo = 1
+  loadPage()
+}
+
+const handleSizeChange = (pageSize: number) => {
+  query.value.pageSize = pageSize
+  handleQuery()
+}
+
+const handleCurrentChange = (pageNo: number) => {
+  query.value.pageNo = pageNo
+  loadPage()
+}
+
+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="TOP_DEPT_ID"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-2" />
+
+    <el-form
+      size="default"
+      class="plan-query-form 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" plain @click="handleOpenCreate">
+          <Icon icon="ep:plus" class="mr-5px" />新增
+        </el-button>
+        <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 flex flex-col">
+      <div class="relative flex-1 min-h-0 min-w-0 overflow-hidden">
+        <el-auto-resizer class="absolute inset-0">
+          <template #default="{ width, height }">
+            <ZmTable
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              empty-text="暂无数据"
+              show-border>
+              <ZmTableColumn prop="deptName" label="部门名称" fixed="left" show-overflow-tooltip />
+              <ZmTableColumn prop="business" label="项目业务" />
+              <ZmTableColumn prop="yearMonth" label="时间" />
+              <ZmTableColumn label="计划工作量" cover-formatter :real-value="getPlanWorkload" />
+              <ZmTableColumn prop="workloadUnit" label="工作量单位" />
+              <ZmTableColumn label="操作" :width="140" fixed="right" action>
+                <template #default="{ row }">
+                  <el-button
+                    size="default"
+                    type="success"
+                    link
+                    @click="handleOpenForm('view', row.id)">
+                    查看
+                  </el-button>
+                  <el-button
+                    size="default"
+                    type="primary"
+                    link
+                    @click="handleOpenForm('edit', row.id)">
+                    编辑
+                  </el-button>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
+
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          size="default"
+          v-show="total > 0"
+          :current-page="query.pageNo"
+          :page-size="query.pageSize"
+          :page-sizes="[10, 20, 30, 50, 100]"
+          :total="total"
+          background
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </div>
+  </div>
+
+  <Dialog v-model="createDialogVisible" :title="dialogTitle" width="780px" class="plan-work-dialog">
+    <template #title>
+      <div class="dialog-title">
+        <div class="dialog-title__icon">
+          <Icon icon="ep:calendar" :size="22" />
+        </div>
+        <div>
+          <div class="dialog-title__text">{{ dialogTitle }}</div>
+          <div class="dialog-title__desc">{{ dialogDescription }}</div>
+        </div>
+      </div>
+    </template>
+
+    <div v-loading="createLoading" class="plan-form-body">
+      <section class="plan-form-card">
+        <div class="plan-form-card__header">
+          <div class="plan-form-card__title">
+            <span class="plan-form-card__marker"></span>
+            <span>计划信息</span>
+          </div>
+        </div>
+
+        <el-form
+          ref="createFormRef"
+          :model="createFormData"
+          :rules="createFormRules"
+          :disabled="isReadonly"
+          size="default"
+          label-position="top"
+          class="plan-create-form">
+          <el-row :gutter="20">
+            <el-col :xs="24" :sm="12">
+              <el-form-item label="选择部门" prop="deptId">
+                <el-tree-select
+                  v-model="createFormData.deptId"
+                  :data="deptOptions"
+                  :props="deptTreeProps"
+                  :loading="deptOptionsLoading"
+                  :disabled="isReadonly"
+                  node-key="id"
+                  check-strictly
+                  default-expand-all
+                  filterable
+                  clearable
+                  placeholder="请选择部门"
+                  class="w-full"
+                  @change="handleDeptChange" />
+              </el-form-item>
+            </el-col>
+
+            <el-col :xs="24" :sm="12">
+              <el-form-item label="选择时间" prop="startTime">
+                <el-date-picker
+                  v-model="createFormData.startTime"
+                  type="month"
+                  value-format="x"
+                  format="YYYY年MM月"
+                  :disabled="isReadonly"
+                  clearable
+                  placeholder="请选择年月"
+                  class="!w-full" />
+              </el-form-item>
+            </el-col>
+
+            <el-col :xs="24" :sm="12">
+              <el-form-item label="项目业务" prop="business">
+                <el-select
+                  v-model="createFormData.business"
+                  :placeholder="createFormData.deptId ? '请选择项目业务' : '请先选择部门'"
+                  :loading="extPropertiesLoading"
+                  :disabled="isReadonly || !createFormData.deptId"
+                  clearable
+                  class="w-full"
+                  @change="handleBusinessChange">
+                  <el-option
+                    v-for="option in selectableBusinessOptions"
+                    :key="option.value"
+                    :label="option.label"
+                    :value="option.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+
+            <el-col :xs="24" :sm="12">
+              <el-form-item label="工作量单位">
+                <el-input
+                  v-model="createFormData.workloadUnit"
+                  readonly
+                  placeholder="选择项目业务后自动填入" />
+              </el-form-item>
+            </el-col>
+
+            <el-col :xs="24">
+              <el-form-item label="计划工作量" prop="planWorkload">
+                <el-input-number
+                  align="left"
+                  v-model="createFormData.planWorkload"
+                  :min="0"
+                  :controls="false"
+                  :disabled="isReadonly || !createFormData.business"
+                  :placeholder="createFormData.business ? '请输入计划工作量' : '请先选择项目业务'"
+                  class="!w-full" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+      </section>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button size="default" :disabled="createLoading" @click="createDialogVisible = false">
+          {{ isReadonly ? '关 闭' : '取 消' }}
+        </el-button>
+        <el-button
+          v-if="!isReadonly"
+          size="default"
+          type="primary"
+          :loading="createLoading"
+          @click="submitCreate">
+          确 定
+        </el-button>
+      </div>
+    </template>
+  </Dialog>
+</template>
+
+<style scoped>
+.plan-query-form :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.dialog-title {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.dialog-title__icon {
+  display: flex;
+  width: 40px;
+  height: 40px;
+  color: var(--el-color-primary);
+  background: rgb(64 158 255 / 12%);
+  border: 1px solid rgb(64 158 255 / 18%);
+  border-radius: 8px;
+  align-items: center;
+  justify-content: center;
+}
+
+.dialog-title__text {
+  font-size: 17px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+}
+
+.dialog-title__desc {
+  margin-top: 3px;
+  font-size: 13px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.plan-form-body {
+  padding: 4px 0;
+}
+
+.plan-form-card {
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: 0 8px 24px rgb(31 45 61 / 5%);
+}
+
+.plan-form-card__header {
+  display: flex;
+  min-height: 52px;
+  padding: 0 18px;
+  background: linear-gradient(90deg, rgb(64 158 255 / 8%) 0%, rgb(255 255 255 / 0%) 72%);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+  align-items: center;
+}
+
+.plan-form-card__title {
+  display: flex;
+  gap: 10px;
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  align-items: center;
+}
+
+.plan-form-card__marker {
+  width: 4px;
+  height: 17px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.plan-create-form {
+  padding: 20px 22px 4px;
+}
+
+.plan-create-form :deep(.el-form-item) {
+  margin-bottom: 18px;
+}
+
+.plan-create-form :deep(.el-form-item__label) {
+  margin-bottom: 6px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.dialog-footer {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+}
+
+@media (width <= 768px) {
+  .dialog-title__desc {
+    display: none;
+  }
+
+  .plan-create-form {
+    padding: 16px 16px 0;
+  }
+}
+</style>

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

@@ -0,0 +1,282 @@
+<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
+}
+
+interface SummaryTableRow {
+  name: 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 tableColumnKey = computed(() => monthColumns.value.map((month) => month.key).join('|'))
+
+const normalizeMonth = (yearMonth: string): MonthColumn => {
+  const matched = String(yearMonth).match(/^(\d{4})[-/](\d{1,2})/)
+
+  if (!matched) {
+    return {
+      key: String(yearMonth),
+      label: String(yearMonth)
+    }
+  }
+
+  const year = Number(matched[1])
+  const month = Number(matched[2])
+
+  return {
+    key: `${year}-${String(month).padStart(2, '0')}`,
+    label: `${year}-${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())
+
+  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 {
+        name: project.projectDeptName ?? project.teamName ?? project.companyName ?? '-',
+        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 === 'name') 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 !== 'name') 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)
+    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
+              :key="tableColumnKey"
+              :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="name" label="部门名称" 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" action />
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+</style>

+ 20 - 3
src/views/pms/stat/rdkb/rd-exception-prompt.vue

@@ -9,9 +9,11 @@ type TimeRange = 'day' | 'month' | 'quarter' | 'year'
 
 type NptCountItem = {
   nptName: string
+  displayName: string
   teamCount: number
 }
 
+const router = useRouter()
 const activeTimeRange = ref<TimeRange>('day')
 const loading = ref(false)
 const productionList = ref<NptCountItem[]>([])
@@ -48,6 +50,16 @@ function getCreateTime(type: TimeRange) {
   return [start.format('YYYY-MM-DD HH:mm:ss'), end.format('YYYY-MM-DD HH:mm:ss')]
 }
 
+function handleProductionClick(item: NptCountItem) {
+  router.push({
+    name: 'IotRdDailyReport',
+    query: {
+      nptName: item.nptName,
+      createTime: getCreateTime(activeTimeRange.value)
+    }
+  })
+}
+
 async function loadData() {
   loading.value = true
   try {
@@ -57,7 +69,8 @@ async function loadData() {
 
     productionList.value = Array.isArray(res)
       ? res.map((item) => ({
-          nptName: getNptDisplayName(item.nptName),
+          nptName: item.nptName || '',
+          displayName: getNptDisplayName(item.nptName),
           teamCount: Number(item.teamCount || 0)
         }))
       : []
@@ -106,9 +119,13 @@ onMounted(() => {
           <div
             v-for="(item, index) in productionTopList"
             :key="item.nptName"
-            class="production-item">
+            class="production-item cursor-pointer"
+            role="button"
+            tabindex="0"
+            @click="handleProductionClick(item)"
+            @keyup.enter="handleProductionClick(item)">
             <span class="production-item__rank">{{ index + 1 }}</span>
-            <span class="production-item__name">{{ item.nptName }}</span>
+            <span class="production-item__name">{{ item.displayName }}</span>
             <span class="production-item__count">
               <span class="production-item__value">{{ item.teamCount }}</span>
               <span class="production-item__unit">个</span>