Эх сурвалжийг харах

✨ feat(日报): 瑞恒日报填报和审批

Zimo 3 өдөр өмнө
parent
commit
250060f79f

+ 4 - 0
src/api/pms/iotrhdailyreport/index.ts

@@ -93,6 +93,10 @@ export const IotRhDailyReportApi = {
     return await request.put({ url: `/pms/iot-rh-daily-report/update`, data })
   },
 
+  approvalIotRhDailyReport: async (data: { id: number; auditStatus: 20 | 30; opinion: string }) => {
+    return await request.put({ url: `/pms/iot-rh-daily-report/approval`, data })
+  },
+
   // 删除瑞恒日报
   deleteIotRhDailyReport: async (id: number) => {
     return await request.delete({ url: `/pms/iot-rh-daily-report/delete?id=` + id })

+ 41 - 0
src/components/DailyTableColumn/index.vue

@@ -0,0 +1,41 @@
+<template>
+  <template v-for="(col, index) in columns" :key="index">
+    <el-table-column v-if="col.children" :label="col.label" align="center">
+      <template v-for="(child, childIndex) in col.children" :key="childIndex">
+        <el-table-column v-if="child.isTag" v-bind="child" align="center" resizable>
+          <template #default="scope">
+            <dict-tag :type="child.dictType!" :value="scope.row[child.prop!]" />
+          </template>
+        </el-table-column>
+        <el-table-column v-else v-bind="child" align="center" resizable />
+      </template>
+    </el-table-column>
+    <el-table-column v-else-if="col.isTag" v-bind="col" align="center" resizable>
+      <template #default="scope">
+        <dict-tag :type="col.dictType!" :value="scope.row[col.prop!]" />
+      </template>
+    </el-table-column>
+    <el-table-column v-else v-bind="col" align="center" resizable />
+  </template>
+</template>
+
+<script setup lang="ts">
+defineOptions({
+  name: 'DailyTableColumn'
+})
+
+interface ColumnProps {
+  label: string
+  prop?: string
+  children?: ColumnProps[]
+  isTag?: boolean
+  dictType?: string
+  [key: string]: any
+}
+
+const props = defineProps<{
+  columns: ColumnProps[]
+}>()
+
+const { columns } = toRefs(props)
+</script>

+ 64 - 24
src/components/DeptTreeSelect/index.vue

@@ -7,11 +7,15 @@ import { Search } from '@element-plus/icons-vue'
 const props = defineProps({
   deptId: {
     type: Number,
-    default: 157
+    required: true
   },
   modelValue: {
     type: Number,
     default: undefined
+  },
+  topId: {
+    type: Number,
+    required: true
   }
 })
 
@@ -25,10 +29,37 @@ const treeRef = ref<InstanceType<typeof ElTree>>()
 
 const expandedKeys = ref<number[]>([])
 
+const sortTreeBySort = (treeNodes: Tree[]) => {
+  if (!treeNodes || !Array.isArray(treeNodes)) return treeNodes
+  const sortedNodes = [...treeNodes].sort((a, b) => {
+    const sortA = a.sort != null ? a.sort : 999999
+    const sortB = b.sort != null ? b.sort : 999999
+    return sortA - sortB
+  })
+
+  sortedNodes.forEach((node) => {
+    if (node.children && Array.isArray(node.children)) {
+      node.children = sortTreeBySort(node.children)
+    }
+  })
+  return sortedNodes
+}
+
 const loadTree = async () => {
   try {
-    const res = await DeptApi.specifiedSimpleDepts(props.deptId)
-    deptList.value = handleTree(res)
+    let id = props.deptId
+
+    if (id !== props.topId) {
+      const depts = await DeptApi.specifiedSimpleDepts(props.topId)
+      if (depts.length) {
+        id = props.topId
+      }
+    }
+
+    emits('update:modelValue', id)
+
+    const res = await DeptApi.specifiedSimpleDepts(id)
+    deptList.value = sortTreeBySort(handleTree(res))
 
     // 加载完成后,如果有选中值,尝试高亮并展开
     nextTick(() => {
@@ -83,7 +114,8 @@ watch(
         expandedKeys.value.push(newVal)
       }
     }
-  }
+  },
+  { immediate: true }
 )
 
 /** 初始化 */
@@ -93,25 +125,33 @@ onMounted(() => {
 </script>
 
 <template>
-  <h1 class="text-lg font-medium">部门</h1>
-  <el-input
-    v-model="deptName"
-    size="default"
-    placeholder="请输入部门名称"
-    clearable
-    :prefix-icon="Search"
-  />
-  <el-scrollbar max-height="700px">
-    <el-tree
-      ref="treeRef"
-      :data="deptList"
-      :props="defaultProps"
-      :expand-on-click-node="false"
-      :filter-node-method="filterNode"
-      node-key="id"
-      highlight-current
-      :default-expanded-keys="expandedKeys"
-      @node-click="handleNodeClick"
+  <div class="gap-4 flex flex-col h-full">
+    <h1 class="text-lg font-medium">部门</h1>
+    <el-input
+      v-model="deptName"
+      size="default"
+      placeholder="请输入部门名称"
+      clearable
+      :prefix-icon="Search"
     />
-  </el-scrollbar>
+    <div class="flex-1 relative">
+      <el-auto-resizer class="absolute">
+        <template #default="{ height }">
+          <el-scrollbar :style="{ height: `${height}px` }">
+            <el-tree
+              ref="treeRef"
+              :data="deptList"
+              :props="defaultProps"
+              :expand-on-click-node="false"
+              :filter-node-method="filterNode"
+              node-key="id"
+              highlight-current
+              :default-expanded-keys="expandedKeys"
+              @node-click="handleNodeClick"
+            />
+          </el-scrollbar>
+        </template>
+      </el-auto-resizer>
+    </div>
+  </div>
 </template>

+ 24 - 7
src/layout/components/Message/src/Message.vue

@@ -31,11 +31,12 @@ const goMyList = () => {
     name: 'MyNotifyMessage'
   })
 }
-const routerDetail = (item) =>{
+const routerDetail = (item) => {
+  console.log('item :>> ', item)
   let id = item.businessId
   if (item.businessType === 'generateInspect') {
-    push({ name:'InspectOrderWrite', params:{id} })
-  }else if(item.businessType === 'failureReport') {
+    push({ name: 'InspectOrderWrite', params: { id } })
+  } else if (item.businessType === 'failureReport') {
     push({
       name: 'BpmProcessInstanceDetail',
       query: {
@@ -43,7 +44,7 @@ const routerDetail = (item) =>{
       }
     })
   } else if (item.businessType === 'generateMaintain') {
-    push({ name: 'MaintainEdit', params: {id } })
+    push({ name: 'MaintainEdit', params: { id } })
   } else if (item.businessType === 'maintainOut') {
     push({
       name: 'BpmProcessInstanceDetail',
@@ -73,8 +74,24 @@ const routerDetail = (item) =>{
     })
   } else if (item.businessType === 'generateOperation') {
     const param = item.templateParams
-    id = param.deptId+','+param.userId+','+param.createTime+','+param.businessId+','+param.orderStatus;
-    push({ name: 'FillOrderInfo',params:{id}})
+    id =
+      param.deptId +
+      ',' +
+      param.userId +
+      ',' +
+      param.createTime +
+      ',' +
+      param.businessId +
+      ',' +
+      param.orderStatus
+    push({ name: 'FillOrderInfo', params: { id } })
+  } else if (item.businessType === 'rhDailyReport') {
+    push({ path: '/iotdayilyreport/IotRhDailyReport/fill', query: { id: id } })
+  } else if (item.businessType === 'rhReportApproval') {
+    push({
+      path: '/iotdayilyreport/IotRhDailyReport/approval',
+      query: { id: id }
+    })
   }
 }
 // ========== 初始化 =========
@@ -177,7 +194,7 @@ onMounted(() => {
   .message-item:hover {
     transform: scale(0.95);
     background-color: #dcf8e4;
-    box-shadow: 0 2px 5px rgba(0,0,0,0.2);
+    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
   }
 }
 </style>

+ 7 - 0
src/views/pms/dingding.vue

@@ -168,6 +168,13 @@ onMounted(async () => {
         name: 'DailyReportApprovalForm',
         params: { id }
       })
+    } else if (type === 'rhDailyReport') {
+      push({ path: '/iotdayilyreport/IotRhDailyReport/fill', query: { id: id } })
+    } else if (type === 'rhReportApproval') {
+      push({
+        path: '/iotdayilyreport/IotRhDailyReport/approval',
+        query: { id: id }
+      })
     }
   } else {
     // 默认跳转

+ 873 - 0
src/views/pms/iotrhdailyreport/approval.vue

@@ -0,0 +1,873 @@
+<script lang="ts" setup>
+import { IotRhDailyReportApi } from '@/api/pms/iotrhdailyreport'
+import { rangeShortcuts } from '@/utils/formatTime'
+import { useDebounceFn } from '@vueuse/core'
+import dayjs from 'dayjs'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
+import { FormInstance, FormRules } from 'element-plus'
+import Form from '@/components/Form/src/Form.vue'
+import { useUserStore } from '@/store/modules/user'
+
+interface List {
+  createTime: number // 日期
+  deptName: string // 施工队伍
+  contractName: string // 项目
+  taskName: string // 任务
+  id: number
+  deptId: number
+  projectId: number
+  taskId: number
+  relocationDays: number // 搬迁安装天数
+  designInjection: string // 设计注气量(万方)
+  transitTime: number // 运行时效
+  dailyGasInjection: number // 注气量(万方)
+  dailyWaterInjection: number // 注水量(方)
+  dailyInjectGasTime: number // 注气时间(H)
+  dailyInjectWaterTime: number // 注水时间(H)
+  dailyPowerUsage: number // 日耗电量(度)
+  dailyOilUsage: number // 日耗油量(升)
+  nonProductionTime: number // 非生产时间(小时)
+  nptReason: string // 非生产时间原因
+  constructionStartDate: number // 施工开始日期
+  constructionEndDate: number // 施工结束日期
+  productionStatus: string // 生产动态
+  constructionStatus: string // 施工状态
+  totalGasInjection: number // 注气量(万方)
+  totalWaterInjection: number // 注水量(方)
+  cumulativeCompletion: number // 完工井次
+  capacity: number // 产能(万方)
+  remark: string // 备注
+  auditStatus: number // 审核状态
+  status: number // 状态
+  opinion: string // 审核意见
+}
+
+interface Column {
+  prop?: keyof List
+  label: string
+  'min-width'?: string
+  isTag?: boolean
+  formatter?: (row: List) => any
+  children?: Column[]
+  dictType?: string
+}
+
+const columns = ref<Column[]>([
+  {
+    label: '日期',
+    prop: 'createTime',
+    'min-width': '120px',
+    formatter: (row: List) => dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss')
+  },
+  {
+    label: '施工队伍',
+    prop: 'deptName',
+    'min-width': '120px'
+  },
+  {
+    label: '项目',
+    prop: 'contractName',
+    'min-width': '120px'
+  },
+  {
+    label: '任务',
+    prop: 'taskName',
+    'min-width': '120px'
+  },
+  {
+    label: '施工状态',
+    prop: 'constructionStatus',
+    'min-width': '120px',
+    isTag: true,
+    dictType: DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE
+  }
+  // {
+  //   label: '搬迁安装天数',
+  //   prop: 'relocationDays',
+  //   'min-width': '120px',
+  //   formatter: (row: List) => (row.relocationDays < 0 ? '0' : String(row.relocationDays))
+  // },
+  // {
+  //   label: '设计注气量(万方)',
+  //   prop: 'designInjection',
+  //   'min-width': '120px',
+  //   formatter: (row: List) => row.designInjection || '0'
+  // },
+  // {
+  //   label: '运行时效',
+  //   prop: 'transitTime',
+  //   'min-width': '120px',
+  //   formatter: (row: List) => (row.transitTime * 100).toFixed(2) + '%'
+  // },
+  // {
+  //   label: '当日',
+  //   children: [
+  //     {
+  //       label: '注气量(万方)',
+  //       prop: 'dailyGasInjection',
+  //       'min-width': '120px',
+  //       formatter: (row: List) => (row.dailyGasInjection / 10000).toFixed(2)
+  //     },
+  //     {
+  //       label: '注水量(方)',
+  //       prop: 'dailyWaterInjection',
+  //       'min-width': '120px'
+  //     },
+  //     {
+  //       label: '注气时间(H)',
+  //       prop: 'dailyInjectGasTime',
+  //       'min-width': '120px'
+  //     },
+  //     {
+  //       label: '注水时间(H)',
+  //       prop: 'dailyInjectWaterTime',
+  //       'min-width': '120px'
+  //     },
+  //     {
+  //       label: '用电量(kWh)',
+  //       prop: 'dailyPowerUsage',
+  //       'min-width': '120px'
+  //     },
+  //     {
+  //       label: '油耗(L)',
+  //       prop: 'dailyOilUsage',
+  //       'min-width': '120px'
+  //     }
+  //   ]
+  // },
+  // {
+  //   label: '非生产时间(H)',
+  //   prop: 'nonProductionTime',
+  //   'min-width': '120px'
+  // },
+  // {
+  //   label: '非生产时间原因',
+  //   prop: 'nptReason',
+  //   'min-width': '120px',
+  //   isTag: true,
+  //   dictType: DICT_TYPE.PMS_PROJECT_NPT_REASON
+  // },
+  // {
+  //   label: '施工开始日期',
+  //   prop: 'constructionStartDate',
+  //   'min-width': '120px',
+  //   formatter: (row: List) => dayjs(row.constructionStartDate).format('YYYY-MM-DD HH:mm:ss')
+  // },
+  // {
+  //   label: '施工结束日期',
+  //   prop: 'constructionEndDate',
+  //   'min-width': '120px',
+  //   formatter: (row: List) => dayjs(row.constructionEndDate).format('YYYY-MM-DD HH:mm:ss')
+  // },
+  // {
+  //   label: '生产动态',
+  //   prop: 'productionStatus',
+  //   'min-width': '120px'
+  // },
+  // {
+  //   label: '累计',
+  //   children: [
+  //     {
+  //       label: '注气量(万方)',
+  //       prop: 'totalGasInjection',
+  //       'min-width': '120px',
+  //       formatter: (row: List) => (row.totalGasInjection / 10000).toFixed(2)
+  //     },
+  //     {
+  //       label: '注水量(方)',
+  //       prop: 'totalWaterInjection',
+  //       'min-width': '120px'
+  //     },
+  //     {
+  //       label: '完工井次',
+  //       prop: 'cumulativeCompletion',
+  //       'min-width': '120px'
+  //     }
+  //   ]
+  // },
+  // {
+  //   label: '产能(万方)',
+  //   prop: 'capacity',
+  //   'min-width': '120px',
+  //   formatter: (row: List) => (row.capacity / 10000).toFixed(2)
+  // }
+])
+
+const getTextWidth = (text: string, fontSize = 12) => {
+  const span = document.createElement('span')
+  span.style.visibility = 'hidden'
+  span.style.position = 'absolute'
+  span.style.whiteSpace = 'nowrap'
+  span.style.fontSize = `${fontSize}px`
+  span.style.fontFamily = 'PingFang SC'
+  span.innerText = text
+
+  document.body.appendChild(span)
+  const width = span.offsetWidth
+  document.body.removeChild(span)
+
+  return width
+}
+
+const calculateColumnWidths = (colums: Column[]) => {
+  for (const col of colums) {
+    let { formatter, prop, label, 'min-width': minWidth, isTag, children } = col
+
+    if (children && children.length > 0) {
+      calculateColumnWidths(children)
+      continue
+    }
+
+    minWidth =
+      Math.min(
+        ...[
+          Math.max(
+            ...[
+              getTextWidth(label),
+              ...list.value.map((v) => {
+                return getTextWidth(formatter ? formatter(v) : v[prop!])
+              })
+            ]
+          ) + (isTag ? 30 : 20),
+          200
+        ]
+      ) + 'px'
+
+    col['min-width'] = minWidth
+  }
+}
+
+function checkTimeSumEquals24(row: List) {
+  // 获取三个字段的值,转换为数字,如果为空则视为0
+  const gasTime = row.dailyInjectGasTime || 0
+  const waterTime = row.dailyInjectWaterTime || 0
+  const nonProdTime = row.nonProductionTime || 0
+
+  // 计算总和
+  const sum = gasTime + waterTime + nonProdTime
+
+  // 返回是否等于24(允许一定的浮点数误差)
+  return Math.abs(sum - 24) < 0.01 // 使用0.01作为误差范围
+}
+
+function cellStyle(data: {
+  row: List
+  column: TableColumnCtx<List>
+  rowIndex: number
+  columnIndex: number
+}) {
+  const { row, column } = data
+
+  if (column.property === 'transitTime') {
+    const originalValue = row.transitTime ?? 0
+
+    if (originalValue > 1.2)
+      return {
+        color: 'red',
+        fontWeight: 'bold'
+      }
+  }
+
+  const timeFields = ['dailyInjectGasTime', 'dailyInjectWaterTime', 'nonProductionTime']
+  if (timeFields.includes(column.property)) {
+    if (!checkTimeSumEquals24(row)) {
+      return {
+        color: 'orange',
+        fontWeight: 'bold'
+      }
+    }
+  }
+
+  // 默认返回空对象,不应用特殊样式
+  return {}
+}
+
+const id = useUserStore().getUser.deptId
+
+const deptId = id
+
+interface Query {
+  pageNo: number
+  pageSize: number
+  deptId: number
+  contractName?: string
+  taskName?: string
+  createTime: string[]
+}
+
+const query = ref<Query>({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: id,
+  createTime: [
+    ...rangeShortcuts[3].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
+  ]
+})
+
+function handleSizeChange(val: number) {
+  query.value.pageSize = val
+  handleQuery()
+}
+
+function handleCurrentChange(val: number) {
+  query.value.pageNo = val
+  loadList()
+}
+
+const loading = ref(false)
+
+const list = ref<List[]>([])
+const total = ref(0)
+
+const loadList = useDebounceFn(async function () {
+  loading.value = true
+  try {
+    const data = await IotRhDailyReportApi.getIotRhDailyReportPage(query.value)
+    list.value = data.list
+    total.value = data.total
+
+    nextTick(() => {
+      calculateColumnWidths(columns.value)
+    })
+  } finally {
+    loading.value = false
+  }
+}, 500)
+
+function handleQuery(setPage = true) {
+  if (setPage) {
+    query.value.pageNo = 1
+  }
+  loadList()
+}
+
+function resetQuery() {
+  query.value = {
+    pageNo: 1,
+    pageSize: 10,
+    deptId: 157,
+    contractName: '',
+    taskName: '',
+    createTime: [
+      ...rangeShortcuts[2].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
+    ]
+  }
+  handleQuery()
+}
+
+watch(
+  [
+    () => query.value.createTime,
+    () => query.value.deptId,
+    () => query.value.taskName,
+    () => query.value.contractName
+  ],
+  () => {
+    handleQuery()
+  },
+  { immediate: true }
+)
+
+const FORM_KEYS = [
+  'id',
+  'deptName',
+  'contractName',
+  'taskName',
+  'dailyGasInjection',
+  'dailyWaterInjection',
+  'dailyInjectGasTime',
+  'dailyInjectWaterTime',
+  'nonProductionTime',
+  'nptReason',
+  'productionStatus',
+  'remark',
+  'relocationDays',
+  'capacity',
+  'createTime',
+  'deptId',
+  'projectId',
+  'taskId',
+  'auditStatus',
+  'opinion'
+] as const
+
+type FormKey = (typeof FORM_KEYS)[number]
+type Form = Partial<Pick<List, FormKey>>
+
+const dialogVisible = ref(false)
+const formRef = ref<FormInstance>()
+const formLoading = ref(false)
+const message = useMessage()
+
+const initFormData = (): Form => ({
+  dailyGasInjection: 0,
+  dailyWaterInjection: 0,
+  dailyInjectGasTime: 0,
+  dailyInjectWaterTime: 0,
+  nonProductionTime: 0,
+  relocationDays: 0,
+  capacity: 0
+})
+
+const form = ref<Form>(initFormData())
+
+async function loadDetail(id: number) {
+  try {
+    const res = await IotRhDailyReportApi.getIotRhDailyReport(id)
+    FORM_KEYS.forEach((key) => {
+      form.value[key] = res[key] ?? form.value[key]
+    })
+    form.value.id = id
+
+    if (res.auditStatus !== 10) {
+      formType.value = 'readonly'
+    }
+
+    // if (!form.value.capacity) {
+    //   message.error('请维护增压机产能')
+    // }
+  } finally {
+  }
+}
+
+const formType = ref<'approval' | 'readonly'>('approval')
+
+function handleOpenForm(id: number, type: 'approval' | 'readonly') {
+  form.value = initFormData()
+  formRef.value?.resetFields()
+
+  formType.value = type
+
+  dialogVisible.value = true
+  loadDetail(id)
+}
+
+const route = useRoute()
+
+onMounted(() => {
+  if (Object.keys(route.query).length > 0) {
+    handleOpenForm(Number(route.query.id), 'approval')
+  }
+})
+
+const transitTime = computed(() => {
+  const cap = form.value.capacity
+  const gas = form.value.dailyGasInjection ?? 0
+
+  if (!cap) return { original: 0, value: '0%' }
+
+  const original = gas / cap
+  return { original, value: (original * 100).toFixed(2) + '%' }
+})
+
+const sumTimes = () => {
+  const { dailyInjectGasTime = 0, dailyInjectWaterTime = 0, nonProductionTime = 0 } = form.value
+  return parseFloat((dailyInjectGasTime + dailyInjectWaterTime + nonProductionTime).toFixed(2))
+}
+
+const validateTotalTime = (_rule: any, _value: any, callback: any) => {
+  const total = sumTimes()
+  if (total !== 24) {
+    callback(new Error(`当前合计 ${total} 小时,三项时间之和必须等于 24`))
+  } else {
+    callback()
+  }
+}
+
+const validateNptReason = (_rule: any, value: any, callback: any) => {
+  if ((form.value.nonProductionTime || 0) > 0 && !value) {
+    callback(new Error('非生产时间大于 0 时,必须选择原因'))
+  } else {
+    callback()
+  }
+}
+
+const timeRuleItem = [
+  { required: true, message: '请输入时间', trigger: 'blur' },
+  { validator: validateTotalTime, trigger: 'blur' }
+]
+
+const rules = reactive<FormRules>({
+  dailyGasInjection: [{ required: true, message: '请输入当日注气量', trigger: 'blur' }],
+  dailyWaterInjection: [{ required: true, message: '请输入当日注水量', trigger: 'blur' }],
+  productionStatus: [{ required: true, message: '请输入生产动态', trigger: 'blur' }],
+
+  // 复用规则
+  dailyInjectGasTime: timeRuleItem,
+  dailyInjectWaterTime: timeRuleItem,
+  nonProductionTime: timeRuleItem,
+
+  nptReason: [{ validator: validateNptReason, trigger: ['change', 'blur'] }]
+})
+
+watch(
+  [
+    () => form.value.dailyInjectGasTime,
+    () => form.value.dailyInjectWaterTime,
+    () => form.value.nonProductionTime
+  ],
+  () => {
+    nextTick(() => {
+      formRef.value?.validateField('nptReason')
+      if (sumTimes() === 24) {
+        formRef.value?.clearValidate([
+          'dailyInjectGasTime',
+          'dailyInjectWaterTime',
+          'nonProductionTime'
+        ])
+      }
+    })
+  }
+)
+
+const submitForm = async (auditStatus: 20 | 30) => {
+  if (!formRef.value) return
+
+  try {
+    // await formRef.value.validate()
+    formLoading.value = true
+    const { opinion, id } = form.value
+
+    const data = { id: id, auditStatus, opinion } as any
+
+    await IotRhDailyReportApi.approvalIotRhDailyReport(data)
+    message.success(auditStatus === 20 ? '通过成功' : '拒绝成功')
+    dialogVisible.value = false
+
+    loadList()
+  } catch (error) {
+    console.warn('表单校验未通过或提交出错')
+  } finally {
+    formLoading.value = false
+  }
+}
+</script>
+
+<template>
+  <div class="grid grid-cols-[15%_1fr] gap-4 h-full">
+    <div class="flex flex-col p-4 gap-2 bg-white dark:bg-[#1d1e1f] shadow rounded-lg h-full">
+      <DeptTreeSelect :deptId="deptId" :topId="157" v-model="query.deptId" />
+    </div>
+    <div class="grid grid-rows-[62px_1fr] h-full gap-4">
+      <el-form
+        size="default"
+        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 gap-8 flex items-center justify-between"
+      >
+        <div class="flex items-center gap-8">
+          <el-form-item label="项目">
+            <el-input
+              v-model="query.contractName"
+              placeholder="请输入项目"
+              clearable
+              @keyup.enter="handleQuery()"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="任务">
+            <el-input
+              v-model="query.taskName"
+              placeholder="请输入任务"
+              clearable
+              @keyup.enter="handleQuery()"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="创建时间">
+            <el-date-picker
+              v-model="query.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :shortcuts="rangeShortcuts"
+              class="!w-220px"
+            />
+          </el-form-item>
+        </div>
+        <el-form-item>
+          <el-button type="primary" @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] shadow rounded-lg p-4 flex flex-col">
+        <div class="flex-1 relative">
+          <el-auto-resizer class="absolute">
+            <template #default="{ width, height }">
+              <el-table
+                :data="list"
+                v-loading="loading"
+                stripe
+                class="absolute"
+                :max-height="height"
+                show-overflow-tooltip
+                :width="width"
+                :cell-style="cellStyle"
+                border
+              >
+                <DailyTableColumn :columns="columns" />
+                <el-table-column label="操作" width="120px" align="center" fixed="right">
+                  <template #default="{ row }">
+                    <el-button
+                      link
+                      type="success"
+                      @click="handleOpenForm(row.id, 'readonly')"
+                      v-hasPermi="['pms:iot-rh-daily-report:update']"
+                    >
+                      查看
+                    </el-button>
+                    <el-button
+                      v-show="row.auditStatus === 10"
+                      link
+                      type="primary"
+                      @click="handleOpenForm(row.id, 'approval')"
+                      v-hasPermi="['pms:iot-rh-daily-report:update']"
+                    >
+                      审批
+                    </el-button>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </template>
+          </el-auto-resizer>
+        </div>
+        <div class="h-10 mt-4 flex items-center justify-end">
+          <el-pagination
+            size="default"
+            v-show="total > 0"
+            v-model:current-page="query.pageNo"
+            v-model:page-size="query.pageSize"
+            :background="true"
+            :page-sizes="[10, 20, 30, 50, 100]"
+            :total="total"
+            layout="total, sizes, prev, pager, next, jumper"
+            @size-change="handleSizeChange"
+            @current-change="handleCurrentChange"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+  <Dialog title="编辑" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      label-position="top"
+      size="default"
+      :rules="rules"
+      :model="form"
+      v-loading="formLoading"
+      require-asterisk-position="right"
+    >
+      <div class="flex flex-col gap-3 text-sm">
+        <div
+          class="rounded-md border border-blue-100 bg-blue-50/80 p-3 dark:border-blue-900/30 dark:bg-blue-900/10"
+        >
+          <div class="flex flex-col gap-2.5">
+            <div class="flex items-center justify-between">
+              <div class="text-gray-600 dark:text-gray-400">
+                <span class="font-bold text-gray-800 dark:text-gray-200">运行时效:</span>
+                当日注气量 / 产能
+              </div>
+              <span
+                class="inline-flex items-center rounded border border-red-200 bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"
+              >
+                >120% 红色预警
+              </span>
+            </div>
+            <div class="flex items-center justify-between">
+              <div class="text-gray-600 dark:text-gray-400">
+                <span class="font-bold text-gray-800 dark:text-gray-200">时间平衡:</span>
+                注气 + 注水 + 非生产 = 24H
+              </div>
+              <span
+                class="inline-flex items-center rounded border border-orange-200 bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-600 dark:bg-orange-900/20 dark:border-orange-800 dark:text-orange-400"
+              >
+                ≠24H 橙色预警
+              </span>
+            </div>
+          </div>
+        </div>
+        <!-- <div
+          v-if="form.opinion"
+          class="flex gap-3 rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-900/10"
+        >
+          <Icon
+            icon="ep:warning-filled"
+            class="mt-0.5 shrink-0 text-base text-yellow-600 dark:text-yellow-500"
+          />
+          <div class="flex flex-col">
+            <h4 class="mb-1 font-bold text-yellow-800 dark:text-yellow-500"> 审核意见 </h4>
+            <p class="leading-relaxed text-gray-600 dark:text-gray-400">
+              {{ form.opinion }}
+            </p>
+          </div>
+        </div> -->
+      </div>
+      <div class="grid grid-cols-2 gap-4 mt-5">
+        <el-form-item label="施工队伍" prop="deptName">
+          <el-input v-model="form.deptName" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="项目" prop="contractName">
+          <el-input v-model="form.contractName" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="任务" prop="taskName">
+          <el-input v-model="form.taskName" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="搬迁安装天数(D)" prop="relocationDays">
+          <el-input v-model="form.relocationDays" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="运行时效" prop="transitTime">
+          <el-input
+            :model-value="transitTime.value"
+            placeholder="运行时效"
+            disabled
+            :class="{ 'warning-input': transitTime.original > 1.2 }"
+            id="transitTimeInput"
+          />
+        </el-form-item>
+        <el-form-item label="当日注气量(方)" prop="dailyGasInjection">
+          <el-input-number
+            class="w-full!"
+            :min="0"
+            v-model="form.dailyGasInjection"
+            placeholder="请输入当日注气量(方)"
+            disabled
+          />
+        </el-form-item>
+        <el-form-item label="当日注水量(方)" prop="dailyWaterInjection">
+          <el-input-number
+            class="w-full!"
+            :min="0"
+            v-model="form.dailyWaterInjection"
+            placeholder="请输入当日注水量(方)"
+            disabled
+          />
+        </el-form-item>
+        <el-form-item label="当日注气时间(H)" prop="dailyInjectGasTime">
+          <el-input-number
+            class="w-full!"
+            :min="0"
+            v-model="form.dailyInjectGasTime"
+            placeholder="请输入当日注气时间(H)"
+            disabled
+          />
+        </el-form-item>
+        <el-form-item label="当日注水时间(H)" prop="dailyInjectWaterTime">
+          <el-input-number
+            class="w-full!"
+            :min="0"
+            v-model="form.dailyInjectWaterTime"
+            placeholder="当日注水时间(H)"
+            disabled
+          />
+        </el-form-item>
+        <el-form-item label="非生产时间(H)" prop="nonProductionTime">
+          <el-input-number
+            class="w-full!"
+            :min="0"
+            v-model="form.nonProductionTime"
+            placeholder="非生产时间(H)"
+            disabled
+          />
+        </el-form-item>
+        <el-form-item label="非生产时间原因" prop="nptReason">
+          <el-select v-model="form.nptReason" placeholder="请选择" disabled clearable>
+            <el-option
+              v-for="(dict, index) of getStrDictOptions(DICT_TYPE.PMS_PROJECT_NPT_REASON)"
+              :key="index"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="生产动态" prop="productionStatus">
+          <el-input
+            v-model="form.productionStatus"
+            placeholder="请输入生产动态"
+            type="textarea"
+            autosize
+            :max-length="1000"
+            disabled
+          />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input
+            v-model="form.remark"
+            placeholder="请输入备注"
+            :max-length="1000"
+            type="textarea"
+            autosize
+            disabled
+          />
+        </el-form-item>
+      </div>
+      <el-form-item class="mt-4" label="审批意见" prop="opinion">
+        <el-input
+          v-model="form.opinion"
+          placeholder="请输入审批意见"
+          :max-length="1000"
+          type="textarea"
+          autosize
+          :disabled="formType === 'readonly'"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button
+        size="default"
+        @click="submitForm(20)"
+        type="primary"
+        :disabled="formLoading || formType === 'readonly'"
+      >
+        审批通过
+      </el-button>
+      <el-button
+        size="default"
+        @click="submitForm(30)"
+        type="danger"
+        :disabled="formLoading || formType === 'readonly'"
+      >
+        审批拒绝
+      </el-button>
+      <el-button size="default" @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+:deep(.el-table) {
+  border-top-right-radius: 8px;
+  border-top-left-radius: 8px;
+
+  .el-table__cell {
+    height: 40px;
+  }
+
+  .el-table__header-wrapper {
+    .el-table__cell {
+      background: var(--el-fill-color-light);
+    }
+  }
+}
+
+:deep(.warning-input) {
+  .el-input__inner {
+    color: red !important;
+    -webkit-text-fill-color: red !important;
+  }
+}
+
+:deep(.el-input-number__decrease) {
+  display: none !important;
+}
+
+:deep(.el-input-number__increase) {
+  display: none !important;
+}
+</style>

+ 833 - 4
src/views/pms/iotrhdailyreport/fill.vue

@@ -1,13 +1,842 @@
 <script lang="ts" setup>
-const deptId = 157
+import { IotRhDailyReportApi } from '@/api/pms/iotrhdailyreport'
+import { rangeShortcuts } from '@/utils/formatTime'
+import { useDebounceFn } from '@vueuse/core'
+import dayjs from 'dayjs'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
+import { FormInstance, FormRules } from 'element-plus'
+import Form from '@/components/Form/src/Form.vue'
+import { useUserStore } from '@/store/modules/user'
 
-const modelValue = ref()
+interface List {
+  createTime: number // 日期
+  deptName: string // 施工队伍
+  contractName: string // 项目
+  taskName: string // 任务
+  id: number
+  deptId: number
+  projectId: number
+  taskId: number
+  relocationDays: number // 搬迁安装天数
+  designInjection: string // 设计注气量(万方)
+  transitTime: number // 运行时效
+  dailyGasInjection: number // 注气量(万方)
+  dailyWaterInjection: number // 注水量(方)
+  dailyInjectGasTime: number // 注气时间(H)
+  dailyInjectWaterTime: number // 注水时间(H)
+  dailyPowerUsage: number // 日耗电量(度)
+  dailyOilUsage: number // 日耗油量(升)
+  nonProductionTime: number // 非生产时间(小时)
+  nptReason: string // 非生产时间原因
+  constructionStartDate: number // 施工开始日期
+  constructionEndDate: number // 施工结束日期
+  productionStatus: string // 生产动态
+  constructionStatus: string // 施工状态
+  totalGasInjection: number // 注气量(万方)
+  totalWaterInjection: number // 注水量(方)
+  cumulativeCompletion: number // 完工井次
+  capacity: number // 产能(万方)
+  remark: string // 备注
+  auditStatus: number // 审核状态
+  status: number // 状态
+  opinion: string // 审核意见
+}
+
+interface Column {
+  prop?: keyof List
+  label: string
+  'min-width'?: string
+  isTag?: boolean
+  formatter?: (row: List) => any
+  children?: Column[]
+  dictType?: string
+}
+
+const columns = ref<Column[]>([
+  {
+    label: '日期',
+    prop: 'createTime',
+    'min-width': '120px',
+    formatter: (row: List) => dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss')
+  },
+  {
+    label: '施工队伍',
+    prop: 'deptName',
+    'min-width': '120px'
+  },
+  {
+    label: '项目',
+    prop: 'contractName',
+    'min-width': '120px'
+  },
+  {
+    label: '任务',
+    prop: 'taskName',
+    'min-width': '120px'
+  },
+  {
+    label: '施工状态',
+    prop: 'constructionStatus',
+    'min-width': '120px',
+    isTag: true,
+    dictType: DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE
+  },
+  {
+    label: '搬迁安装天数',
+    prop: 'relocationDays',
+    'min-width': '120px',
+    formatter: (row: List) => (row.relocationDays < 0 ? '0' : String(row.relocationDays))
+  },
+  {
+    label: '设计注气量(万方)',
+    prop: 'designInjection',
+    'min-width': '120px',
+    formatter: (row: List) => row.designInjection || '0'
+  },
+  {
+    label: '运行时效',
+    prop: 'transitTime',
+    'min-width': '120px',
+    formatter: (row: List) => (row.transitTime * 100).toFixed(2) + '%'
+  },
+  {
+    label: '当日',
+    children: [
+      {
+        label: '注气量(万方)',
+        prop: 'dailyGasInjection',
+        'min-width': '120px',
+        formatter: (row: List) => (row.dailyGasInjection / 10000).toFixed(2)
+      },
+      {
+        label: '注水量(方)',
+        prop: 'dailyWaterInjection',
+        'min-width': '120px'
+      },
+      {
+        label: '注气时间(H)',
+        prop: 'dailyInjectGasTime',
+        'min-width': '120px'
+      },
+      {
+        label: '注水时间(H)',
+        prop: 'dailyInjectWaterTime',
+        'min-width': '120px'
+      },
+      {
+        label: '用电量(kWh)',
+        prop: 'dailyPowerUsage',
+        'min-width': '120px'
+      },
+      {
+        label: '油耗(L)',
+        prop: 'dailyOilUsage',
+        'min-width': '120px'
+      }
+    ]
+  },
+  {
+    label: '非生产时间(H)',
+    prop: 'nonProductionTime',
+    'min-width': '120px'
+  },
+  {
+    label: '非生产时间原因',
+    prop: 'nptReason',
+    'min-width': '120px',
+    isTag: true,
+    dictType: DICT_TYPE.PMS_PROJECT_NPT_REASON
+  },
+  {
+    label: '施工开始日期',
+    prop: 'constructionStartDate',
+    'min-width': '120px',
+    formatter: (row: List) => dayjs(row.constructionStartDate).format('YYYY-MM-DD HH:mm:ss')
+  },
+  {
+    label: '施工结束日期',
+    prop: 'constructionEndDate',
+    'min-width': '120px',
+    formatter: (row: List) => dayjs(row.constructionEndDate).format('YYYY-MM-DD HH:mm:ss')
+  },
+  {
+    label: '生产动态',
+    prop: 'productionStatus',
+    'min-width': '120px'
+  },
+  {
+    label: '累计',
+    children: [
+      {
+        label: '注气量(万方)',
+        prop: 'totalGasInjection',
+        'min-width': '120px',
+        formatter: (row: List) => (row.totalGasInjection / 10000).toFixed(2)
+      },
+      {
+        label: '注水量(方)',
+        prop: 'totalWaterInjection',
+        'min-width': '120px'
+      },
+      {
+        label: '完工井次',
+        prop: 'cumulativeCompletion',
+        'min-width': '120px'
+      }
+    ]
+  },
+  {
+    label: '产能(万方)',
+    prop: 'capacity',
+    'min-width': '120px',
+    formatter: (row: List) => (row.capacity / 10000).toFixed(2)
+  }
+])
+
+const getTextWidth = (text: string, fontSize = 12) => {
+  const span = document.createElement('span')
+  span.style.visibility = 'hidden'
+  span.style.position = 'absolute'
+  span.style.whiteSpace = 'nowrap'
+  span.style.fontSize = `${fontSize}px`
+  span.style.fontFamily = 'PingFang SC'
+  span.innerText = text
+
+  document.body.appendChild(span)
+  const width = span.offsetWidth
+  document.body.removeChild(span)
+
+  return width
+}
+
+const calculateColumnWidths = (colums: Column[]) => {
+  for (const col of colums) {
+    let { formatter, prop, label, 'min-width': minWidth, isTag, children } = col
+
+    if (children && children.length > 0) {
+      calculateColumnWidths(children)
+      continue
+    }
+
+    minWidth =
+      Math.min(
+        ...[
+          Math.max(
+            ...[
+              getTextWidth(label),
+              ...list.value.map((v) => {
+                return getTextWidth(formatter ? formatter(v) : v[prop!])
+              })
+            ]
+          ) + (isTag ? 30 : 20),
+          200
+        ]
+      ) + 'px'
+
+    col['min-width'] = minWidth
+  }
+}
+
+function checkTimeSumEquals24(row: List) {
+  // 获取三个字段的值,转换为数字,如果为空则视为0
+  const gasTime = row.dailyInjectGasTime || 0
+  const waterTime = row.dailyInjectWaterTime || 0
+  const nonProdTime = row.nonProductionTime || 0
+
+  // 计算总和
+  const sum = gasTime + waterTime + nonProdTime
+
+  // 返回是否等于24(允许一定的浮点数误差)
+  return Math.abs(sum - 24) < 0.01 // 使用0.01作为误差范围
+}
+
+function cellStyle(data: {
+  row: List
+  column: TableColumnCtx<List>
+  rowIndex: number
+  columnIndex: number
+}) {
+  const { row, column } = data
+
+  if (column.property === 'transitTime') {
+    const originalValue = row.transitTime ?? 0
+
+    if (originalValue > 1.2)
+      return {
+        color: 'red',
+        fontWeight: 'bold'
+      }
+  }
+
+  const timeFields = ['dailyInjectGasTime', 'dailyInjectWaterTime', 'nonProductionTime']
+  if (timeFields.includes(column.property)) {
+    if (!checkTimeSumEquals24(row)) {
+      return {
+        color: 'orange',
+        fontWeight: 'bold'
+      }
+    }
+  }
+
+  // 默认返回空对象,不应用特殊样式
+  return {}
+}
+
+const id = useUserStore().getUser.deptId ?? 157
+
+const deptId = id
+
+interface Query {
+  pageNo: number
+  pageSize: number
+  deptId: number
+  contractName?: string
+  taskName?: string
+  createTime: string[]
+}
+
+const query = ref<Query>({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: id,
+  createTime: [
+    ...rangeShortcuts[3].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
+  ]
+})
+
+function handleSizeChange(val: number) {
+  query.value.pageSize = val
+  handleQuery()
+}
+
+function handleCurrentChange(val: number) {
+  query.value.pageNo = val
+  loadList()
+}
+
+const loading = ref(false)
+
+const list = ref<List[]>([])
+const total = ref(0)
+
+const loadList = useDebounceFn(async function () {
+  loading.value = true
+  try {
+    const data = await IotRhDailyReportApi.getIotRhDailyReportPage(query.value)
+    list.value = data.list
+    total.value = data.total
+
+    nextTick(() => {
+      calculateColumnWidths(columns.value)
+    })
+  } finally {
+    loading.value = false
+  }
+}, 500)
+
+function handleQuery(setPage = true) {
+  if (setPage) {
+    query.value.pageNo = 1
+  }
+  loadList()
+}
+
+function resetQuery() {
+  query.value = {
+    pageNo: 1,
+    pageSize: 10,
+    deptId: 157,
+    contractName: '',
+    taskName: '',
+    createTime: [
+      ...rangeShortcuts[2].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
+    ]
+  }
+  handleQuery()
+}
+
+watch(
+  [
+    () => query.value.createTime,
+    () => query.value.deptId,
+    () => query.value.taskName,
+    () => query.value.contractName
+  ],
+  () => {
+    handleQuery()
+  },
+  { immediate: true }
+)
+
+const FORM_KEYS = [
+  'id',
+  'deptName',
+  'contractName',
+  'taskName',
+  'dailyGasInjection',
+  'dailyWaterInjection',
+  'dailyInjectGasTime',
+  'dailyInjectWaterTime',
+  'nonProductionTime',
+  'nptReason',
+  'productionStatus',
+  'remark',
+  'relocationDays',
+  'capacity',
+  'createTime',
+  'deptId',
+  'projectId',
+  'taskId',
+  'opinion'
+] as const
+
+type FormKey = (typeof FORM_KEYS)[number]
+type Form = Partial<Pick<List, FormKey>>
+
+const dialogVisible = ref(false)
+const formRef = ref<FormInstance>()
+const formLoading = ref(false)
+const message = useMessage()
+
+const initFormData = (): Form => ({
+  dailyGasInjection: 0,
+  dailyWaterInjection: 0,
+  dailyInjectGasTime: 0,
+  dailyInjectWaterTime: 0,
+  nonProductionTime: 0,
+  relocationDays: 0,
+  capacity: 0
+})
+
+const form = ref<Form>(initFormData())
+
+async function loadDetail(id: number) {
+  try {
+    const res = await IotRhDailyReportApi.getIotRhDailyReport(id)
+    FORM_KEYS.forEach((key) => {
+      form.value[key] = res[key] ?? form.value[key]
+    })
+    form.value.id = id
+
+    if (res.status !== 0) {
+      formType.value = 'readonly'
+    }
+
+    if (!form.value.capacity) {
+      message.error('请维护增压机产能')
+    }
+  } finally {
+  }
+}
+
+const formType = ref<'edit' | 'readonly'>('edit')
+
+function handleOpenForm(id: number, type: 'edit' | 'readonly') {
+  form.value = initFormData()
+  formRef.value?.resetFields()
+
+  formType.value = type
+
+  dialogVisible.value = true
+  loadDetail(id)
+}
+
+const route = useRoute()
+
+onMounted(() => {
+  if (Object.keys(route.query).length > 0) {
+    handleOpenForm(Number(route.query.id), 'edit')
+  }
+})
+
+const transitTime = computed(() => {
+  const cap = form.value.capacity
+  const gas = form.value.dailyGasInjection ?? 0
+
+  if (!cap) return { original: 0, value: '0%' }
+
+  const original = gas / cap
+  return { original, value: (original * 100).toFixed(2) + '%' }
+})
+
+const sumTimes = () => {
+  const { dailyInjectGasTime = 0, dailyInjectWaterTime = 0, nonProductionTime = 0 } = form.value
+  return parseFloat((dailyInjectGasTime + dailyInjectWaterTime + nonProductionTime).toFixed(2))
+}
+
+const validateTotalTime = (_rule: any, _value: any, callback: any) => {
+  const total = sumTimes()
+  if (total !== 24) {
+    callback(new Error(`当前合计 ${total} 小时,三项时间之和必须等于 24`))
+  } else {
+    callback()
+  }
+}
+
+const validateNptReason = (_rule: any, value: any, callback: any) => {
+  if ((form.value.nonProductionTime || 0) > 0 && !value) {
+    callback(new Error('非生产时间大于 0 时,必须选择原因'))
+  } else {
+    callback()
+  }
+}
+
+const timeRuleItem = [
+  { required: true, message: '请输入时间', trigger: 'blur' },
+  { validator: validateTotalTime, trigger: 'blur' }
+]
+
+const rules = reactive<FormRules>({
+  dailyGasInjection: [{ required: true, message: '请输入当日注气量', trigger: 'blur' }],
+  dailyWaterInjection: [{ required: true, message: '请输入当日注水量', trigger: 'blur' }],
+  productionStatus: [{ required: true, message: '请输入生产动态', trigger: 'blur' }],
+
+  // 复用规则
+  dailyInjectGasTime: timeRuleItem,
+  dailyInjectWaterTime: timeRuleItem,
+  nonProductionTime: timeRuleItem,
+
+  nptReason: [{ validator: validateNptReason, trigger: ['change', 'blur'] }]
+})
+
+watch(
+  [
+    () => form.value.dailyInjectGasTime,
+    () => form.value.dailyInjectWaterTime,
+    () => form.value.nonProductionTime
+  ],
+  () => {
+    nextTick(() => {
+      formRef.value?.validateField('nptReason')
+      if (sumTimes() === 24) {
+        formRef.value?.clearValidate([
+          'dailyInjectGasTime',
+          'dailyInjectWaterTime',
+          'nonProductionTime'
+        ])
+      }
+    })
+  }
+)
+
+const { t } = useI18n()
+
+const submitForm = async () => {
+  if (!formRef.value) return
+
+  try {
+    await formRef.value.validate()
+    formLoading.value = true
+    const { createTime, ...other } = form.value
+    const data = { ...other, fillOrderCreateTime: createTime } as any
+    await IotRhDailyReportApi.createIotRhDailyReport(data)
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    loadList()
+  } catch (error) {
+    console.warn('表单校验未通过或提交出错')
+  } finally {
+    formLoading.value = false
+  }
+}
 </script>
 
 <template>
   <div class="grid grid-cols-[15%_1fr] gap-4 h-full">
-    <div class="flex flex-col p-4 gap-2 bg-white dark:bg-[#1d1e1f] shadow rounded-lg h-full">
-      <DeptTreeSelect :deptId="deptId" v-model="modelValue" />
+    <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg h-full">
+      <DeptTreeSelect :top-id="157" :deptId="deptId" v-model="query.deptId" />
+    </div>
+    <div class="grid grid-rows-[62px_1fr] h-full gap-4">
+      <el-form
+        size="default"
+        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 gap-8 flex items-center justify-between"
+      >
+        <div class="flex items-center gap-8">
+          <el-form-item label="项目">
+            <el-input
+              v-model="query.contractName"
+              placeholder="请输入项目"
+              clearable
+              @keyup.enter="handleQuery()"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="任务">
+            <el-input
+              v-model="query.taskName"
+              placeholder="请输入任务"
+              clearable
+              @keyup.enter="handleQuery()"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="创建时间">
+            <el-date-picker
+              v-model="query.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :shortcuts="rangeShortcuts"
+              class="!w-220px"
+            />
+          </el-form-item>
+        </div>
+        <el-form-item>
+          <el-button type="primary" @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] shadow rounded-lg p-4 flex flex-col">
+        <div class="flex-1 relative">
+          <el-auto-resizer class="absolute">
+            <template #default="{ width, height }">
+              <el-table
+                :data="list"
+                v-loading="loading"
+                stripe
+                class="absolute"
+                :max-height="height"
+                show-overflow-tooltip
+                :width="width"
+                :cell-style="cellStyle"
+                border
+              >
+                <DailyTableColumn :columns="columns" />
+                <el-table-column label="操作" width="120px" align="center" fixed="right">
+                  <template #default="{ row }">
+                    <el-button
+                      link
+                      type="success"
+                      @click="handleOpenForm(row.id, 'readonly')"
+                      v-hasPermi="['pms:iot-rh-daily-report:query']"
+                    >
+                      查看
+                    </el-button>
+                    <el-button
+                      v-show="row.status === 0"
+                      link
+                      type="primary"
+                      @click="handleOpenForm(row.id, 'edit')"
+                      v-hasPermi="['pms:iot-rh-daily-report:create']"
+                    >
+                      编辑
+                    </el-button>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </template>
+          </el-auto-resizer>
+        </div>
+        <div class="h-10 mt-4 flex items-center justify-end">
+          <el-pagination
+            size="default"
+            v-show="total > 0"
+            v-model:current-page="query.pageNo"
+            v-model:page-size="query.pageSize"
+            :background="true"
+            :page-sizes="[10, 20, 30, 50, 100]"
+            :total="total"
+            layout="total, sizes, prev, pager, next, jumper"
+            @size-change="handleSizeChange"
+            @current-change="handleCurrentChange"
+          />
+        </div>
+      </div>
     </div>
   </div>
+  <Dialog title="编辑" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      label-position="top"
+      size="default"
+      :rules="rules"
+      :model="form"
+      v-loading="formLoading"
+      require-asterisk-position="right"
+      :disabled="formType === 'readonly'"
+    >
+      <div class="flex flex-col gap-3 text-sm">
+        <div
+          class="rounded-md border border-blue-100 bg-blue-50/80 p-3 dark:border-blue-900/30 dark:bg-blue-900/10"
+        >
+          <div class="flex flex-col gap-2.5">
+            <div class="flex items-center justify-between">
+              <div class="text-gray-600 dark:text-gray-400">
+                <span class="font-bold text-gray-800 dark:text-gray-200">运行时效:</span>
+                当日注气量 / 产能
+              </div>
+              <span
+                class="inline-flex items-center rounded border border-red-200 bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"
+              >
+                >120% 红色预警
+              </span>
+            </div>
+            <div class="flex items-center justify-between">
+              <div class="text-gray-600 dark:text-gray-400">
+                <span class="font-bold text-gray-800 dark:text-gray-200">时间平衡:</span>
+                注气 + 注水 + 非生产 = 24H
+              </div>
+              <span
+                class="inline-flex items-center rounded border border-orange-200 bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-600 dark:bg-orange-900/20 dark:border-orange-800 dark:text-orange-400"
+              >
+                ≠24H 橙色预警
+              </span>
+            </div>
+          </div>
+        </div>
+        <div
+          v-if="form.opinion"
+          class="flex gap-3 rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-900/10"
+        >
+          <Icon
+            icon="ep:warning-filled"
+            class="mt-0.5 shrink-0 text-base text-yellow-600 dark:text-yellow-500"
+          />
+          <div class="flex flex-col">
+            <h4 class="mb-1 font-bold text-yellow-800 dark:text-yellow-500"> 审核意见 </h4>
+            <p class="leading-relaxed text-gray-600 dark:text-gray-400">
+              {{ form.opinion }}
+            </p>
+          </div>
+        </div>
+      </div>
+      <div class="grid grid-cols-2 gap-4 mt-5">
+        <el-form-item label="施工队伍" prop="deptName">
+          <el-input v-model="form.deptName" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="项目" prop="contractName">
+          <el-input v-model="form.contractName" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="任务" prop="taskName">
+          <el-input v-model="form.taskName" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="搬迁安装天数(D)" prop="relocationDays">
+          <el-input v-model="form.relocationDays" placeholder="" disabled />
+        </el-form-item>
+        <el-form-item label="运行时效" prop="transitTime">
+          <el-input
+            :model-value="transitTime.value"
+            placeholder="运行时效"
+            disabled
+            :class="{ 'warning-input': transitTime.original > 1.2 }"
+            id="transitTimeInput"
+          />
+        </el-form-item>
+        <el-form-item label="当日注气量(方)" prop="dailyGasInjection">
+          <el-input-number
+            class="w-full!"
+            :min="0"
+            v-model="form.dailyGasInjection"
+            placeholder="请输入当日注气量(方)"
+          />
+        </el-form-item>
+        <el-form-item label="当日注水量(方)" prop="dailyWaterInjection">
+          <el-input-number
+            class="w-full!"
+            :min="0"
+            v-model="form.dailyWaterInjection"
+            placeholder="请输入当日注水量(方)"
+          />
+        </el-form-item>
+        <el-form-item label="当日注气时间(H)" prop="dailyInjectGasTime">
+          <el-input-number
+            class="w-full!"
+            :min="0"
+            v-model="form.dailyInjectGasTime"
+            placeholder="请输入当日注气时间(H)"
+          />
+        </el-form-item>
+        <el-form-item label="当日注水时间(H)" prop="dailyInjectWaterTime">
+          <el-input-number
+            class="w-full!"
+            :min="0"
+            v-model="form.dailyInjectWaterTime"
+            placeholder="当日注水时间(H)"
+          />
+        </el-form-item>
+        <el-form-item label="非生产时间(H)" prop="nonProductionTime">
+          <el-input-number
+            class="w-full!"
+            :min="0"
+            v-model="form.nonProductionTime"
+            placeholder="非生产时间(H)"
+          />
+        </el-form-item>
+        <el-form-item label="非生产时间原因" prop="nptReason">
+          <el-select v-model="form.nptReason" placeholder="请选择" clearable>
+            <el-option
+              v-for="(dict, index) of getStrDictOptions(DICT_TYPE.PMS_PROJECT_NPT_REASON)"
+              :key="index"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="生产动态" prop="productionStatus">
+          <el-input
+            v-model="form.productionStatus"
+            placeholder="请输入生产动态"
+            type="textarea"
+            autosize
+            :max-length="1000"
+          />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input
+            v-model="form.remark"
+            placeholder="请输入备注"
+            :max-length="1000"
+            type="textarea"
+            autosize
+          />
+        </el-form-item>
+      </div>
+    </el-form>
+    <template #footer>
+      <el-button size="default" @click="submitForm" type="primary" :disabled="formLoading">
+        确 定
+      </el-button>
+      <el-button size="default" @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
 </template>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+:deep(.el-table) {
+  border-top-right-radius: 8px;
+  border-top-left-radius: 8px;
+
+  .el-table__cell {
+    height: 40px;
+  }
+
+  .el-table__header-wrapper {
+    .el-table__cell {
+      background: var(--el-fill-color-light);
+    }
+  }
+}
+
+:deep(.warning-input) {
+  .el-input__inner {
+    color: red !important;
+    -webkit-text-fill-color: red !important;
+  }
+}
+
+:deep(.el-input-number__decrease) {
+  display: none !important;
+}
+
+:deep(.el-input-number__increase) {
+  display: none !important;
+}
+</style>

+ 20 - 13
src/views/pms/iotrhdailyreport/index.vue

@@ -1,9 +1,18 @@
 <template>
-  <el-row :gutter="20">
+  <el-row :gutter="20" class="h-full">
     <el-col :span="4" :xs="24">
-      <ContentWrap class="h-1/1">
-        <DeptTree2 :deptId="rootDeptId" @node-click="handleDeptNodeClick" />
-      </ContentWrap>
+      <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow p-4 h-full">
+        <DeptTreeSelect
+          :deptId="rootDeptId"
+          :top-id="157"
+          v-model="queryParams.deptId"
+          @node-click="handleDeptNodeClick"
+        />
+      </div>
+
+      <!-- <DeptTree2 :deptId="rootDeptId" @node-click="handleDeptNodeClick" /> -->
+
+      <!-- </ContentWrap> -->
     </el-col>
     <el-col :span="20" :xs="24">
       <ContentWrap>
@@ -378,11 +387,12 @@ import IotRhDailyReportForm from './IotRhDailyReportForm.vue'
 import UnfilledReportDialog from './UnfilledReportDialog.vue'
 import { DICT_TYPE } from '@/utils/dict'
 import { ref, reactive, onMounted, onUnmounted } from 'vue'
-import DeptTree2 from '@/views/pms/iotrhdailyreport/DeptTree2.vue'
 import { useDebounceFn } from '@vueuse/core'
 import quarterOfYear from 'dayjs/plugin/quarterOfYear'
 import dayjs from 'dayjs'
 
+import { useUserStore } from '@/store/modules/user'
+
 dayjs.extend(quarterOfYear)
 
 /** 瑞恒日报 列表 */
@@ -400,7 +410,7 @@ const total = ref(0) // 列表的总页数
 let queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  deptId: undefined,
+  deptId: useUserStore().getUser.deptId,
   contractName: undefined,
   projectId: undefined,
   taskName: undefined,
@@ -439,7 +449,7 @@ const exportLoading = ref(false) // 导出的加载中
 // 添加弹窗引用
 const unfilledDialogRef = ref()
 
-const rootDeptId = ref(157)
+const rootDeptId = ref(useUserStore().getUser.deptId)
 
 // 新增统计相关变量
 const statistics = ref({
@@ -883,7 +893,7 @@ const selectedDept = ref<{ id: number; name: string }>()
 const handleDeptNodeClick = async (row) => {
   // 记录选中的部门信息
   selectedDept.value = { id: row.id, name: row.name }
-  queryParams.deptId = row.id
+  // queryParams.deptId = row.id
   await getList()
 }
 
@@ -908,11 +918,8 @@ let resizeObserver: ResizeObserver | null = null
 /** 初始化 **/
 onMounted(() => {
   if (Object.keys(route.query).length > 0) {
-    queryParams = {
-      ...queryParams,
-      ...route.query,
-      deptId: Number(route.query.deptId) as any
-    }
+    queryParams.deptId = Number(route.query.deptId) as any
+    queryParams.createTime = route.query.createTime as string[]
     handleQuery()
   } else getList()
   // 创建 ResizeObserver 监听表格容器尺寸变化

+ 29 - 10
src/views/pms/iotrhdailyreport/summary.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import DeptTree2 from '@/views/pms/iotrhdailyreport/DeptTree2.vue'
 import dayjs from 'dayjs'
 import { IotRhDailyReportApi } from '@/api/pms/iotrhdailyreport'
 import { useDebounceFn } from '@vueuse/core'
@@ -12,6 +11,10 @@ import { Motion, AnimatePresence } from 'motion-v'
 import { rangeShortcuts } from '@/utils/formatTime'
 import download from '@/utils/download'
 
+import { useUserStore } from '@/store/modules/user'
+
+const deptId = useUserStore().getUser.deptId
+
 interface Query {
   pageNo: number
   pageSize: number
@@ -21,12 +24,12 @@ interface Query {
   createTime: string[]
 }
 
-const id = 157
+const id = deptId
 
 const query = ref<Query>({
   pageNo: 1,
   pageSize: 10,
-  deptId: 157,
+  deptId: deptId,
   createTime: [
     ...rangeShortcuts[2].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
   ]
@@ -85,7 +88,7 @@ const getTotal = useDebounceFn(async () => {
 
   try {
     let res1: any[]
-    if (query.value.createTime.length !== 0) {
+    if (query.value.createTime && query.value.createTime.length === 2) {
       res1 = await IotRhDailyReportApi.rhDailyReportStatistics({
         createTime: query.value.createTime,
         deptId: query.value.deptId
@@ -106,7 +109,7 @@ const getTotal = useDebounceFn(async () => {
   } finally {
     totalLoading.value = false
   }
-}, 1000)
+}, 500)
 
 interface List {
   id: number | null
@@ -176,7 +179,7 @@ const getList = useDebounceFn(async () => {
   } finally {
     listLoading.value = false
   }
-}, 1000)
+}, 500)
 
 const tab = ref<'表格' | '看板'>('表格')
 
@@ -238,7 +241,7 @@ const getChart = useDebounceFn(async () => {
   } finally {
     chartLoading.value = false
   }
-}, 1000)
+}, 500)
 
 const resizer = () => {
   chart?.resize()
@@ -341,7 +344,6 @@ const render = () => {
 
 const handleDeptNodeClick = (node: any) => {
   deptName.value = node.name
-  query.value.deptId = node.id
   handleQuery()
 }
 
@@ -370,9 +372,20 @@ const resetQuery = () => {
 
 watch(
   () => query.value.createTime,
-  () => handleQuery(false)
+  (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(() => {
   handleQuery()
 })
@@ -448,7 +461,13 @@ const tolist = (id: number) => {
 <template>
   <div class="grid grid-cols-[16%_1fr] gap-5">
     <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow p-4">
-      <DeptTree2 :deptId="id" @node-click="handleDeptNodeClick" />
+      <!-- <DeptTree2 :deptId="id" @node-click="handleDeptNodeClick" /> -->
+      <DeptTreeSelect
+        :deptId="id"
+        :top-id="157"
+        v-model="query.deptId"
+        @node-click="handleDeptNodeClick"
+      />
     </div>
     <div class="grid grid-rows-[62px_164px_1fr] h-full gap-4">
       <el-form

+ 1 - 0
types/global.d.ts

@@ -48,6 +48,7 @@ declare global {
   interface Tree {
     id: number
     name: string
+    sort?: number
     children?: Tree[] | any[]
   }
   // 分页数据公共返回