Jelajahi Sumber

feat(iotopeationfill): 重构运行记录填报页面及表单交互

- 重构运行记录填报页面为左侧填报列表、右侧表单区域布局
- 解析并保存路由中的部门、用户、日期、订单及填报状态
- 接入公司标识、设备填报列表、设备属性及已有填报值查询
- 使用 dayjs 统一处理日期格式
- 支持设备切换并动态加载对应表单数据
- 增加右侧表单区域加载状态及独立滚动效果
- 根据填报状态和已填报状态控制表单禁用及操作栏显示
- 累计信息改为展示 sumList 的 totalRunTime
- 根据 nonSumList 动态生成输入框、数字框、文本域和字典选择器
- double 类型字段支持最小值、阈值及时间字段最大 24 小时限制
- 增加固定必填、设备采集字段及非生产时间相关校验
- 增加 RH 和 RY 每日时间合计 24 小时校验
- 增加其他非生产时间与原因联动校验
- 增加 RH 当日注气量条件必填校验
- 增加采集字段 PLC 来源悬浮提示
- 增加 RY 生产日报生产动态表格及 reportDetails 数据加载
- 支持生产动态增删行、时间计算、结束井深同步和逐行校验
- 增加 RD 累计公里数及运行时长建议上限计算
- RD 超限改为软警告,不阻塞基础表单校验
- 增加 RD 超限数据二次确认弹窗及分类卡片展示
- 优化列表选中状态、累计信息、表单标题和底部操作栏样式
- 新增 index1-backend.vue 作为原填报页面逻辑备份
- 移除旧的 index1 copy.vue 备份文件
Zimo 3 hari lalu
induk
melakukan
e50147a36f

+ 1 - 1
.env.local

@@ -4,7 +4,7 @@ NODE_ENV=development
 VITE_DEV=true
 
 # 请求路径  http://192.168.188.200:48080  https://iot.deepoil.cc:5443  http://172.26.0.56:48080  http://192.168.188.198:48080
-VITE_BASE_URL='https://iot.deepoil.cc:5443'
+VITE_BASE_URL='https://aims.deepoil.cc'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
 VITE_UPLOAD_TYPE=server

+ 2 - 1
src/views/pms/iotopeationfill/index.vue

@@ -44,6 +44,7 @@
             v-model="queryParams.createTime"
             value-format="YYYY-MM-DD HH:mm:ss"
             type="daterange"
+            :shortcuts="rangeShortcuts"
             :start-placeholder="t('operationFill.start')"
             :end-placeholder="t('operationFill.end')"
             :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
@@ -283,7 +284,7 @@
 
 <script setup lang="ts">
 import type { FormInstance } from 'element-plus'
-import { dateFormatter } from '@/utils/formatTime'
+import { dateFormatter, rangeShortcuts } from '@/utils/formatTime'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { onMounted, ref } from 'vue'
 import { IotOpeationFillApi, IotOpeationFillVO } from '@/api/pms/iotopeationfill'

+ 0 - 686
src/views/pms/iotopeationfill/index1 copy.vue

@@ -1,686 +0,0 @@
-<template>
-  <ContentWrap>
-    <el-tabs type="border-card" tab-position="left" v-loading="loading" style="height: 84vh">
-      <el-tab-pane
-        style="height: 100%"
-        v-for="(deviceItem, deviceIndex) in list"
-        :key="deviceIndex"
-      >
-        <template #label>
-          <span
-            :class="['custom-label', { 'has-border': deviceItem.deviceName === '生产日报' }]"
-            v-if="deviceItem.isFill === 1"
-            @click="
-              openFill(
-                deviceItem.deviceCategoryId,
-                deviceItem.deviceId,
-                deviceItem.deptId,
-                deviceItem.deviceName,
-                deviceItem.deviceCode
-              )
-            "
-          >
-            {{ deviceItem.deviceCode }} ({{ deviceItem.deviceName }})
-          </span>
-          <span
-            :class="['custom-label1', { 'has-border': deviceItem.deviceName === '生产日报' }]"
-            v-else
-            @click="
-              openFill(
-                deviceItem.deviceCategoryId,
-                deviceItem.deviceId,
-                deviceItem.deptId,
-                deviceItem.deviceName,
-                deviceItem.deviceCode
-              )
-            "
-          >
-            {{ deviceItem.deviceCode }} ({{ deviceItem.deviceName }})
-          </span>
-        </template>
-        <div class="form-wrapper">
-          <el-form label-width="120px" class="scrollable-form">
-            <div style="margin-left: 24px">
-              <el-form class="demo-form-inline" :inline="true">
-                <el-form-item :label="t('common.createTime')" class="custom-label1">
-                  <span style="text-decoration: underline">
-                    {{ createTime }}
-                  </span>
-                </el-form-item>
-                <el-form-item :label="t('operationFillForm.team')" class="custom-label1">
-                  <span style="text-decoration: underline">
-                    {{ deviceItem.orgName }}
-                  </span>
-                </el-form-item>
-                <el-row :gutter="20">
-                  <el-col
-                    v-for="(summaryItem, summaryIndex) in attrList1"
-                    :key="summaryIndex"
-                    :span="24"
-                  >
-                    <el-form-item :label="summaryItem.name" class="custom-label1">
-                      <span style="text-decoration: underline">
-                        {{ summaryItem.totalRunTime }}
-                      </span>
-                    </el-form-item>
-                  </el-col>
-                </el-row>
-                <!--                <el-form-item :label="t('operationFillForm.sumTime')" class="custom-label1">
-                  <span style="text-decoration: underline;">
-                  {{totalRunTime1}}h
-                  </span>
-                </el-form-item>-->
-              </el-form>
-            </div>
-
-            <div
-              v-for="(attrItem, attrIndex) in attrList"
-              :key="attrIndex"
-              style="margin-left: 24px"
-            >
-              {{ attrItem }}
-              <!-- 添加提示文字 -->
-              <div v-if="attrItem.isCollection === 1" class="plc-tip">
-                <el-alert
-                  :title="t('operationFillForm.alert')"
-                  type="warning"
-                  :closable="false"
-                  center
-                  show-icon
-                  style="width: 320px"
-                />
-              </div>
-              <el-form-item :label="attrItem.name" prop="deviceId" label-position="top">
-                <div v-if="fillStatus === '1'">
-                  <el-select
-                    disabled
-                    v-model="attrItem.fillContent"
-                    v-if="attrItem.type === 'enum' && attrItem.description !== null"
-                    style="width: 200px"
-                  >
-                    <el-option
-                      v-for="dict in attrItem.name === '非生产原因'
-                        ? getIntDictOptions(attrItem.description)
-                        : getStrDictOptions(attrItem.description)"
-                      :key="dict.label"
-                      :label="dict.label"
-                      :value="
-                        attrItem.name === '非生产原因' ? Number(dict.value) : dict.value.toString()
-                      "
-                    />
-                  </el-select>
-                  <el-input
-                    v-else
-                    v-model="attrItem.fillContent"
-                    clearable
-                    style="width: 200px; margin-right: 10px"
-                    disabled
-                  />
-                </div>
-
-                <el-input
-                  v-else-if="attrItem.type === 'textarea'"
-                  v-model="attrItem.fillContent"
-                  type="textarea"
-                  clearable
-                  style="width: 200px"
-                />
-                <el-select
-                  v-model="attrItem.fillContent"
-                  clearable
-                  v-else-if="attrItem.type === 'enum' && attrItem.description !== null"
-                  style="width: 200px"
-                  filterable
-                >
-                  <el-option
-                    v-for="dict in attrItem.name === '非生产原因'
-                      ? getIntDictOptions(attrItem.description)
-                      : getStrDictOptions(attrItem.description)"
-                    :key="dict.label"
-                    :label="dict.label"
-                    :value="
-                      attrItem.name === '非生产原因' ? Number(dict.value) : dict.value.toString()
-                    "
-                  />
-                </el-select>
-                <el-input
-                  v-else
-                  v-model="attrItem.fillContent"
-                  clearable
-                  style="width: 200px"
-                  :placeholder="
-                    attrItem.type === 'double'
-                      ? t('operationFillForm.enterNumber')
-                      : t('operationFillForm.enterContent')
-                  "
-                  @input="handleInput(attrItem)"
-                  :maxlength="attrItem.type === 'double' ? calculateMaxLength(attrItem) : undefined"
-                />
-              </el-form-item>
-            </div>
-            <el-form-item>
-              <el-button type="primary" @click="getFillInfo" v-show="showStatus">{{
-                t('operationFillForm.confirm')
-              }}</el-button>
-              <el-button type="info" @click="deleteFillInfo" v-show="showStatus">{{
-                t('operationFill.clear')
-              }}</el-button>
-            </el-form-item>
-          </el-form>
-        </div>
-      </el-tab-pane>
-    </el-tabs>
-  </ContentWrap>
-</template>
-
-<script setup lang="ts">
-import { IotOpeationFillApi, IotOpeationFillVO } from '@/api/pms/iotopeationfill'
-import { ElMessage } from 'element-plus'
-import moment from 'moment'
-import { getIntDictOptions, getStrDictOptions } from '@/utils/dict'
-import { useRoute } from 'vue-router'
-
-/** 运行记录填报 列表 */
-defineOptions({ name: 'FillOrderInfo' })
-
-const route = useRoute()
-
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const loading = ref(true) // 列表的加载中
-const { params } = useRoute() // 查询参数
-const deptId = params.id as string
-const list = ref<IotOpeationFillVO[]>([]) // 列表的数据
-const attrList = ref<IotOpeationFillVO[]>([]) // 非累计属性集合
-const attrList1 = ref<IotOpeationFillVO[]>([]) // 累计属性集合
-const attrList2 = ref<IotOpeationFillVO[]>([]) // 属性集合
-let companyName = ref('')
-
-let fillStatus = deptId.split(',')[4]
-let createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
-let showStatus = true
-const queryParams = reactive<any>({
-  pageNo: 1,
-  pageSize: 10,
-  deviceCode: undefined,
-  deviceName: undefined,
-  fillContent: undefined,
-  deviceType: undefined,
-  deviceComponent: undefined,
-  deptId: undefined,
-  orgName: undefined,
-  proId: undefined,
-  proName: undefined,
-  teamId: undefined,
-  teamName: undefined,
-  dutyName: undefined,
-  creDate: [],
-  createTime: [],
-  deviceCategoryId: 1,
-  deviceId: undefined,
-  threshold: undefined,
-  defaultValue: undefined,
-  isSum: undefined
-})
-
-let cxStatus = true
-
-// 计算数字输入的最大长度(根据阈值动态计算)
-const calculateMaxLength = (item: any) => {
-  if (item.type !== 'double' || !item.threshold) return undefined
-
-  const max = parseFloat(item.threshold)
-  if (isNaN(max)) return undefined
-
-  // 整数部分长度 + 可能的小数点 + 两位小数
-  return max.toString().length + (max.toString().includes('.') ? 0 : 3)
-}
-
-// 简单的节流函数,避免提示信息过于频繁
-const throttle = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
-  let lastTime = 0
-
-  return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
-    const now = Date.now()
-    if (now - lastTime >= delay) {
-      fn.apply(this, args)
-      lastTime = now
-    }
-  }
-}
-
-const showComponent = () => {
-  if (JSON.parse(fillStatus) === 1 || JSON.parse(fillStatus) === 3) {
-    showStatus = false
-  }
-}
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    queryParams.deptId = deptId.split(',')[0]
-    queryParams.userId = deptId.split(',')[1]
-    queryParams.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
-    queryParams.orderId = deptId.split(',')[3]
-    const data = await IotOpeationFillApi.getIotOpeationFillPage(queryParams)
-    list.value = data
-    if (cxStatus) {
-      queryParams.deviceCategoryId = list.value[0].deviceCategoryId
-      queryParams.deptId = list.value[0].deptId
-      queryParams.deviceCode = list.value[0].deviceCode
-      queryParams.deviceName = list.value[0].deviceName
-      queryParams.deviceId = list.value[0].deviceId
-    }
-    getAttrList()
-  } finally {
-    loading.value = false
-  }
-}
-function formatTimestamp(timestamp) {
-  // const date = new Date(timestamp * 1000)
-  return moment.unix(timestamp).format('YYYY-MM-DD')
-}
-
-const open = async (_type: string, id?: number) => {
-  alert(id)
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-let devName = ''
-const openFill = (
-  deviceCategoryId?: number,
-  deviceId?: number,
-  deptId?: number,
-  deviceName?: string,
-  deviceCode?: string
-) => {
-  queryParams.deviceCategoryId = deviceCategoryId
-  queryParams.deptId = deptId
-  queryParams.deviceCode = deviceCode
-  queryParams.deviceName = deviceName
-  if (queryParams.deviceName == '生产日报') {
-    devName = '生产日报'
-  }
-  queryParams.deviceId = deviceId
-  getAttrList()
-}
-// 处理输入事件,实时限制输入格式和最大值
-const handleInput = (item: any) => {
-  if (item.type === 'double') {
-    // 保存原始值用于后续比较
-    const originalValue = item.fillContent
-
-    // 1. 格式验证:只允许数字和小数点
-    item.fillContent = item.fillContent.replace(/[^\d.]/g, '')
-    // 确保只有一个小数点
-    item.fillContent = item.fillContent.replace(/\.{2,}/g, '.')
-    // 确保小数点不在开头
-    item.fillContent = item.fillContent.replace(/^\./g, '')
-    // 限制小数位数为两位
-    item.fillContent = item.fillContent.replace(/(\d+)\.(\d{2}).*/, '$1.$2')
-
-    // 2. 最大值验证
-    if (item.threshold) {
-      const value = parseFloat(item.fillContent)
-      const max = parseFloat(item.threshold)
-
-      if (!isNaN(value) && !isNaN(max) && value > max) {
-        // 输入值超过阈值时,恢复到修改前的值
-        item.fillContent = originalValue
-          .replace(/[^\d.]/g, '')
-          .replace(/\.{2,}/g, '.')
-          .replace(/^\./g, '')
-          .replace(/(\d+)\.(\d{2}).*/, '$1.$2')
-
-        if (parseFloat(item.fillContent) > max) {
-          item.fillContent = max.toString()
-        }
-
-        throttle(() => {
-          ElMessage.warning(t('operationFillForm.exceedMax', { max }))
-        }, 1000)()
-      }
-    }
-
-    if (companyName.value === 'rd') {
-      // 3. 累计值限制验证(改为弹窗提示但允许继续)
-      if (item.maxAllowedValue !== undefined) {
-        const value = parseFloat(item.fillContent)
-        if (!isNaN(value) && value > item.maxAllowedValue) {
-          // 不自动修改值,而是显示警告弹窗
-          let limitDescription = ''
-          if (item.limitType === 'km') {
-            limitDescription = `当前累计值${item.currentSumValue} + 3000`
-          } else if (item.limitType === 'time') {
-            limitDescription = `当前累计值${item.currentSumValue} + 100`
-          }
-
-          ElMessage.warning(
-            `填报值 ${value} 超过限制 ${item.maxAllowedValue} (${limitDescription}),请确认是否正确!`
-          )
-        }
-      }
-    }
-  }
-}
-
-const getAttrList = async () => {
-  loading.value = true
-  try {
-    queryParams.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
-    const data = await IotOpeationFillApi.getAttrs(queryParams)
-
-    attrList.value = data[0].nonSumList
-    attrList1.value = data[0].sumList
-
-    // 建立累计数据映射,用于后续验证
-    const sumMap = new Map()
-    attrList1.value.forEach((item) => {
-      // 创建匹配规则:移除"填报"字样的差异,保留核心名称
-      const coreName = item.name.replace(/填报/g, '')
-      sumMap.set(coreName, item)
-    })
-
-    // 为非累计数据添加最大值限制
-    attrList.value.forEach(function (item) {
-      if (item.fillContent !== '' && item.fillContent !== null) {
-        const num = Number(item.fillContent)
-        if (!isNaN(num)) {
-          if (item.fillContent.includes('.')) {
-            item.fillContent = Number(num.toFixed(2))
-          } else {
-            item.fillContent = Math.floor(num)
-          }
-        }
-      }
-
-      if (companyName.value === 'rd') {
-        // 添加最大值限制逻辑
-        const coreName = item.name.replace(/填报/g, '')
-        const sumItem = sumMap.get(coreName)
-        if (sumItem) {
-          // 根据字段名称判断使用哪种限制规则
-          if (item.name.includes('公里数填报')) {
-            // 公里数限制:当前累计值 + 3000
-            item.maxAllowedValue = sumItem.totalRunTime + 3000
-            item.currentSumValue = sumItem.totalRunTime // 保存当前累计值用于提示
-            item.limitType = 'km' // 标记为公里数限制
-          } else if (item.name.includes('运转时长填报')) {
-            // 运转时长限制:当前累计值 + 100
-            item.maxAllowedValue = sumItem.totalRunTime + 100
-            item.currentSumValue = sumItem.totalRunTime // 保存当前累计值用于提示
-            item.limitType = 'time' // 标记为时长限制
-          }
-        }
-      }
-
-      item.deviceCode = queryParams.deviceCode
-      item.deptId = queryParams.deptId
-      item.deviceId = queryParams.deviceId
-      item.deviceCategoryId = queryParams.deviceCategoryId
-      item.modelId = item.id
-      console.log(item.fillContent)
-    })
-
-    attrList1.value.forEach(function (item) {
-      item.deviceCode = queryParams.deviceCode
-      item.deptId = queryParams.deptId
-      item.deviceId = queryParams.deviceId
-      item.deviceCategoryId = queryParams.deviceCategoryId
-      item.modelId = item.id
-    })
-  } finally {
-    loading.value = false
-  }
-}
-/** 获取填写信息保存到后台*/
-const getFillInfo = async () => {
-  try {
-    const company = await IotOpeationFillApi.getOrgName(route.params.id.toString().split(',')[0])
-
-    if (devName != '生产日报') {
-      // 检查必填字段
-      const emptyFields = attrList.value.filter((item) => {
-        // 只检查非disabled的字段
-        return (
-          !(item.isCollection === 1 || fillStatus === '1') &&
-          (item.fillContent === undefined || item.fillContent === '')
-        )
-      })
-      if (emptyFields.length > 0) {
-        ElMessage.error(t('operationFillForm.fill'))
-        return
-      }
-    }
-
-    if (company === 'rd') {
-      // 检查是否有超出累计值限制的字段
-      const exceededFields = attrList.value.filter((item) => {
-        if (
-          item.type === 'double' &&
-          item.maxAllowedValue !== undefined &&
-          item.fillContent !== '' &&
-          item.fillContent !== null
-        ) {
-          const value = parseFloat(item.fillContent)
-          return !isNaN(value) && value > item.maxAllowedValue
-        }
-        return false
-      })
-
-      // 如果有超出限制的字段,提示用户确认
-      if (exceededFields.length > 0) {
-        let exceededMessage = ''
-        exceededFields.forEach((field) => {
-          let limitDescription = ''
-          if (field.limitType === 'km') {
-            limitDescription = `(${field.currentSumValue} + 3000)`
-          } else if (field.limitType === 'time') {
-            limitDescription = `(${field.currentSumValue} + 100)`
-          }
-          exceededMessage += `${field.name};\n`
-        })
-
-        // exceededMessage += '\n是否继续保存?'
-
-        const confirmResult = await message.confirm(
-          exceededMessage,
-          '以下填报项超出限制,是否继续保存?',
-          '继续保存',
-          '取消'
-        )
-        if (!confirmResult) {
-          return // 用户取消保存
-        }
-      }
-    }
-
-    attrList2.value = attrList.value.concat(attrList1.value)
-
-    attrList2.value.forEach(function (item) {
-      item.pointName = item.name
-      item.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
-      item.userId = deptId.split(',')[1]
-      item.id = deptId.split(',')[3]
-    })
-    const data = attrList2.value as unknown as IotOpeationFillVO
-    await IotOpeationFillApi.insertLog(data)
-    message.success(t('common.createSuccess'))
-    // 发送操作成功的事件
-    emit('success')
-    cxStatus = false
-    getList()
-  } catch (error) {
-    console.error('保存失败:', error)
-  }
-}
-
-/**清空填写信息*/
-const deleteFillInfo = () => {
-  attrList.value.forEach(function (item) {
-    item.fillContent = ''
-  })
-}
-
-/** 初始化 **/
-onMounted(async () => {
-  const company = await IotOpeationFillApi.getOrgName(route.params.id.toString().split(',')[0])
-  companyName.value = company
-
-  getList()
-  showComponent()
-})
-</script>
-<style scoped>
-.scrollable-form {
-  /* 设置最大高度,超过这个高度会出现滚动条 */
-  max-height: 500px; /* 根据你的需求调整 */
-
-  /* 可选:添加内边距和边框美化 */
-  padding: 16px;
-
-  /* 超出部分显示垂直滚动条 */
-  overflow-y: auto;
-  border: 1px solid #e5e7eb;
-  border-radius: 4px;
-}
-
-/* 可选:美化滚动条 */
-.scrollable-form::-webkit-scrollbar {
-  width: 6px;
-}
-
-.scrollable-form::-webkit-scrollbar-track {
-  background: #f1f1f1;
-  border-radius: 3px;
-}
-
-.scrollable-form::-webkit-scrollbar-thumb {
-  background: #c1c1c1;
-  border-radius: 3px;
-}
-
-.scrollable-form::-webkit-scrollbar-thumb:hover {
-  background: #a8a8a8;
-}
-
-.back-red {
-  /* 红色背景 */
-  background-color: red;
-}
-
-.back-blue {
-  background-color: grey;
-}
-
-.step-container {
-  display: grid;
-  grid-template-columns: 220px 1fr;
-  gap: 10px;
-  height: 100%;
-  min-height: 600px;
-}
-
-.steps-nav {
-  padding-right: 15px;
-  overflow-y: auto;
-}
-
-.form-wrapper {
-  padding: 30px;
-  background: #fff;
-  border-radius: 8px;
-  box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
-}
-
-.navigation-controls {
-  margin-top: 40px;
-  text-align: center;
-}
-
-.custom-label {
-  padding: 0 10px;
-  font-size: 17px;
-  font-weight: 1000;
-  color: forestgreen;
-}
-
-.custom-label1 {
-  padding: 0 10px;
-  font-size: 17px;
-  font-weight: 1000;
-}
-
-.has-border {
-  padding: 3px 8px; /* 适当增加内边距,避免文字太贴近边框 */
-  font-size: 20px;
-  border: 2px solid #333; /* 加粗到2px,使用深灰色#333让边框更清晰 */
-  border-radius: 2px; /* 轻微圆角,可选 */
-}
-
-::v-deep .el-step__icon {
-  color: #fff;
-  background-color: #409eff;
-  border: 0;
-}
-
-.step-title-wrapper {
-  position: relative;
-  display: inline-flex;
-  padding-right: 25px;
-  align-items: center;
-  gap: 8px;
-}
-
-/* 覆盖步骤条默认样式 */
-:deep(.custom-steps) {
-  /* 调整头部位置 */
-  .el-step__head {
-    top: 3px;
-  }
-
-  /* 标题容器定位 */
-  .el-step__title {
-    display: inline-block;
-    padding-right: 0;
-    margin-left: 10px;
-  }
-
-  /* 步骤连接线 */
-  .el-step__line {
-    left: 11px;
-    background-color: #ebeef5;
-  }
-
-  /* 当前步骤样式 */
-  .is-process .title-text {
-    font-weight: 600;
-    color: #409eff;
-  }
-
-  /* 完成状态图标 */
-  .is-finish .tip-icon {
-    color: #67c23a;
-  }
-}
-
-.horizontal-container {
-  display: flex;
-  flex-wrap: wrap; /* 允许换行 */
-  gap: 20px; /* 项目间距 */
-}
-
-.form-item-container {
-  flex: 1; /* 等宽分布 */
-  min-width: 200px; /* 最小宽度防止挤压 */
-}
-
-/* 新增日报填报项的边框样式 */
-.report-border {
-  padding: 2px 4px;
-  border: 2px solid #42b983; /* 使用Vue标志性的绿色边框 */
-  border-radius: 4px;
-}
-</style>

+ 1301 - 0
src/views/pms/iotopeationfill/index1-backend.vue

@@ -0,0 +1,1301 @@
+<template>
+  <ContentWrap>
+    <el-tabs type="border-card" tab-position="left" v-loading="loading" style="height: 84vh">
+      <el-tab-pane
+        style="height: 100%"
+        v-for="(deviceItem, deviceIndex) in list"
+        :key="deviceIndex">
+        <template #label>
+          <span
+            :class="['custom-label', { 'has-border': deviceItem.deviceName === '生产日报' }]"
+            v-if="deviceItem.isFill === 1"
+            @click="
+              openFill(
+                deviceItem.deviceCategoryId,
+                deviceItem.deviceId,
+                deviceItem.deptId,
+                deviceItem.deviceName,
+                deviceItem.deviceCode
+              )
+            ">
+            {{ deviceItem.deviceCode }} ({{ deviceItem.deviceName }})
+          </span>
+          <span
+            :class="['custom-label1', { 'has-border': deviceItem.deviceName === '生产日报' }]"
+            v-else
+            @click="
+              openFill(
+                deviceItem.deviceCategoryId,
+                deviceItem.deviceId,
+                deviceItem.deptId,
+                deviceItem.deviceName,
+                deviceItem.deviceCode
+              )
+            ">
+            {{ deviceItem.deviceCode }} ({{ deviceItem.deviceName }})
+          </span>
+        </template>
+        <div class="form-wrapper h-full">
+          <el-form
+            ref="formRef"
+            size="default"
+            label-width="120px"
+            class="scrollable-form"
+            :model="{ attrList: attrList, reportDetails, taskId }">
+            <div style="margin-left: 24px">
+              <el-form class="demo-form-inline" :inline="true">
+                <el-form-item :label="t('common.createTime')" class="custom-label1">
+                  <span style="text-decoration: underline">
+                    {{ createTime }}
+                  </span>
+                </el-form-item>
+                <el-form-item :label="t('operationFillForm.team')" class="custom-label1">
+                  <span style="text-decoration: underline">
+                    {{ deviceItem.orgName }}
+                  </span>
+                </el-form-item>
+                <el-form-item
+                  v-if="
+                    deviceItem.deviceName === '生产日报' &&
+                    companyName !== 'ry' &&
+                    companyName !== 'rh'
+                  "
+                  label="井号"
+                  class="custom-label1">
+                  <span style="text-decoration: underline">
+                    {{ deviceItem.wellName }}
+                  </span>
+                </el-form-item>
+                <el-form-item
+                  v-else-if="
+                    deviceItem.deviceName === '生产日报' &&
+                    (companyName === 'ry' || companyName === 'rh') &&
+                    taskOptions.length > 0
+                  "
+                  label="井号"
+                  class="custom-label1"
+                  prop="taskId">
+                  <el-select
+                    v-model="taskId"
+                    placeholder="请选择井号"
+                    :options="taskOptions"
+                    class="w-40!" />
+                </el-form-item>
+                <el-form-item
+                  v-else-if="deviceItem.deviceName === '生产日报'"
+                  label="井号"
+                  class="custom-label1">
+                  <span style="text-decoration: underline">
+                    {{ deviceItem.wellName }}
+                  </span>
+                </el-form-item>
+                <el-row :gutter="20">
+                  <el-col
+                    v-for="(summaryItem, summaryIndex) in attrList1"
+                    :key="summaryIndex"
+                    :span="24">
+                    <el-form-item :label="summaryItem.name" class="custom-label1">
+                      <span style="text-decoration: underline">
+                        {{ summaryItem.totalRunTime }}
+                      </span>
+                    </el-form-item>
+                  </el-col>
+                </el-row>
+                <!--                <el-form-item :label="t('operationFillForm.sumTime')" class="custom-label1">
+                  <span style="text-decoration: underline;">
+                  {{totalRunTime1}}h
+                  </span>
+                </el-form-item>-->
+              </el-form>
+            </div>
+
+            <div
+              v-for="(attrItem, attrIndex) in attrList.filter(
+                (item) => !keys.includes(item.description)
+              )"
+              :key="attrIndex"
+              style="margin-left: 24px">
+              <!-- 添加提示文字 -->
+              <div v-if="attrItem.isCollection === 1" class="plc-tip">
+                <el-alert
+                  :title="t('operationFillForm.alert')"
+                  type="warning"
+                  :closable="false"
+                  center
+                  show-icon
+                  style="width: 320px" />
+              </div>
+
+              <el-form-item
+                v-if="companyName === 'ry' ? attrItem.description !== 'productionStatus' : true"
+                :label="attrItem.name"
+                :prop="
+                  'attrList.' +
+                  attrList.findIndex((i) => i.description === attrItem.description) +
+                  '.fillContent'
+                "
+                label-position="top"
+                :rules="rules[attrItem.description]">
+                <div v-if="fillStatus === '1'">
+                  <el-select
+                    disabled
+                    v-model="attrItem.fillContent"
+                    v-if="attrItem.type === 'enum' && attrItem.description !== null"
+                    style="width: 200px">
+                    <el-option
+                      v-for="dict in attrItem.name === '非生产原因'
+                        ? getIntDictOptions(attrItem.description)
+                        : getStrDictOptions(attrItem.description)"
+                      :key="dict.label"
+                      :label="dict.label"
+                      :value="
+                        attrItem.name === '非生产原因' ? Number(dict.value) : dict.value.toString()
+                      " />
+                  </el-select>
+                  <el-input
+                    v-else
+                    v-model="attrItem.fillContent"
+                    clearable
+                    style="width: 200px; margin-right: 10px"
+                    disabled />
+                </div>
+
+                <el-input
+                  v-else-if="attrItem.type === 'textarea'"
+                  v-model="attrItem.fillContent"
+                  type="textarea"
+                  clearable
+                  style="width: 200px" />
+                <el-select
+                  v-model="attrItem.fillContent"
+                  clearable
+                  v-else-if="attrItem.type === 'enum' && attrItem.description !== null"
+                  style="width: 200px"
+                  filterable>
+                  <el-option
+                    v-for="dict in attrItem.name === '非生产原因'
+                      ? getIntDictOptions(attrItem.description)
+                      : getStrDictOptions(attrItem.description)"
+                    :key="dict.label"
+                    :label="dict.label"
+                    :value="
+                      attrItem.name === '非生产原因' ? Number(dict.value) : dict.value.toString()
+                    " />
+                </el-select>
+                <el-input
+                  v-else
+                  v-model="attrItem.fillContent"
+                  clearable
+                  style="width: 200px"
+                  :placeholder="
+                    attrItem.type === 'double'
+                      ? t('operationFillForm.enterNumber')
+                      : t('operationFillForm.enterContent')
+                  "
+                  @input="handleInput(attrItem)"
+                  :maxlength="
+                    attrItem.type === 'double' ? calculateMaxLength(attrItem) : undefined
+                  " />
+              </el-form-item>
+            </div>
+
+            <div
+              v-for="(attrItem, attrIndex) in attrList"
+              :key="attrIndex"
+              style="margin-left: 24px">
+              <el-divider v-if="attrItem.description === 'repairTime'" content-position="left"
+                >非生产时间</el-divider
+              >
+              <el-form-item
+                v-if="
+                  keys.includes(attrItem.description) && attrItem.description !== 'otherNptReason'
+                "
+                label-position="top"
+                :label="attrItem.name"
+                :prop="'attrList.' + attrIndex + '.fillContent'"
+                :rules="rules[attrItem.description]">
+                <el-input-number
+                  class="w-80!"
+                  :min="0"
+                  :max="24"
+                  v-model="attrItem.fillContent"
+                  :controls="false"
+                  align="left"
+                  placeholder="请输入数字"
+                  :disabled="fillStatus === '1'" />
+              </el-form-item>
+
+              <el-form-item
+                v-else-if="attrItem.description === 'otherNptReason'"
+                label-position="top"
+                :label="attrItem.name"
+                :prop="'attrList.' + attrIndex + '.fillContent'"
+                :rules="rules[attrItem.description]">
+                <el-input
+                  class="w-80!"
+                  v-model="attrItem.fillContent"
+                  placeholder="请输入其他非生产原因"
+                  :disabled="fillStatus === '1'" />
+              </el-form-item>
+            </div>
+
+            <div v-if="companyName === 'ry' && deviceItem.deviceName === '生产日报'">
+              <div class="flex items-center justify-between mb-6">
+                <div class="flex items-center gap-2">
+                  <div class="bg-[var(--el-color-primary)] w-1 h-5 rounded-full"></div>
+                  <div class="text-lg font-medium text-[var(--el-text-color-primary)]"
+                    >生产动态</div
+                  >
+                </div>
+                <el-button
+                  :disabled="fillStatus === '1'"
+                  type="primary"
+                  link
+                  :icon="Plus"
+                  @click="addProductionStatusRow">
+                  添加一行
+                </el-button>
+              </div>
+              <el-form-item prop="reportDetails" class="table-form-item">
+                <ZmTable :data="reportDetails" :loading="false" class="mb-4">
+                  <ZmTableColumn label="日期" :width="180" prop="reportDate">
+                    <template #default="{ row, $index }">
+                      <el-form-item
+                        v-if="$index >= 0"
+                        class="mb-0!"
+                        :prop="`reportDetails.${$index}.reportDate`"
+                        :rules="{
+                          required: true,
+                          message: '请选择日期',
+                          trigger: ['change', 'blur'],
+                          type: 'number'
+                        }">
+                        <el-date-picker
+                          v-model="row.reportDate"
+                          placeholder="选择日期"
+                          clearable
+                          class="w-full!"
+                          value-format="x"
+                          :disabled="fillStatus === '1'"
+                          @change="inputCurrentDepth()" />
+                      </el-form-item>
+                    </template>
+                  </ZmTableColumn>
+
+                  <ZmTableColumn :width="130" label="开始时间" prop="startTime">
+                    <template #default="{ row, $index }">
+                      <el-form-item
+                        v-if="$index >= 0"
+                        class="mb-0!"
+                        :prop="`reportDetails.${$index}.startTime`"
+                        :rules="{
+                          required: true,
+                          message: '请选择开始时间',
+                          trigger: ['change', 'blur']
+                        }">
+                        <el-time-picker
+                          v-model="row.startTime"
+                          placeholder="选择开始时间"
+                          clearable
+                          format="HH:mm"
+                          value-format="HH:mm"
+                          class="w-full!"
+                          @change="acalculateDuration(row)"
+                          :disabled="fillStatus === '1'" />
+                      </el-form-item>
+                    </template>
+                  </ZmTableColumn>
+                  <ZmTableColumn :width="130" label="结束时间" prop="endTime">
+                    <template #default="{ row, $index }">
+                      <el-form-item
+                        v-if="$index >= 0"
+                        class="mb-0!"
+                        :prop="`reportDetails.${$index}.endTime`"
+                        :rules="{
+                          required: true,
+                          message: '请选择结束时间',
+                          trigger: ['change', 'blur']
+                        }">
+                        <el-time-picker
+                          v-model="row.endTime"
+                          placeholder="选择结束时间"
+                          clearable
+                          format="HH:mm"
+                          value-format="HH:mm"
+                          class="w-full!"
+                          @change="acalculateDuration(row)"
+                          :disabled="fillStatus === '1'" />
+                      </el-form-item>
+                    </template>
+                  </ZmTableColumn>
+                  <ZmTableColumn :width="80" label="时长(H)" prop="duration" />
+
+                  <ZmTableColumn label="工况" min-width="140">
+                    <template #default="{ row, $index }">
+                      <el-form-item
+                        v-if="$index >= 0"
+                        :prop="`reportDetails.${$index}.currentOperation`"
+                        :rules="{
+                          required: true,
+                          message: '请输入工况',
+                          trigger: ['change', 'blur']
+                        }"
+                        class="mb-0!">
+                        <el-input
+                          v-model="row.currentOperation"
+                          type="textarea"
+                          :autosize="{ minRows: 1 }"
+                          show-word-limit
+                          :maxlength="1000"
+                          placeholder="请输入工况"
+                          :disabled="fillStatus === '1'" />
+                      </el-form-item>
+                    </template>
+                  </ZmTableColumn>
+
+                  <ZmTableColumn v-if="showDepth" label="结束井深(m)" min-width="80">
+                    <template #default="{ row, $index }">
+                      <el-form-item
+                        v-if="$index >= 0"
+                        :prop="`reportDetails.${$index}.currentDepth`"
+                        :rules="[
+                          {
+                            required: true,
+                            message: '请输入结束井深',
+                            trigger: ['blur']
+                          }
+                          // { validator: validateLastCurrentDepth, trigger: ['change', 'blur'] }
+                        ]"
+                        class="mb-0!">
+                        <el-input-number
+                          v-model="row.currentDepth"
+                          :min="0"
+                          :controls="false"
+                          class="!w-full"
+                          align="left"
+                          placeholder="请输入结束井深"
+                          @input="() => inputCurrentDepth()"
+                          :disabled="fillStatus === '1'">
+                          <template #suffix> m </template>
+                        </el-input-number>
+                      </el-form-item>
+                    </template>
+                  </ZmTableColumn>
+
+                  <ZmTableColumn label="详细描述" min-width="200" align="center">
+                    <template #default="{ row, $index }">
+                      <el-form-item
+                        v-if="$index >= 0"
+                        :prop="`reportDetails.${$index}.constructionDetail`"
+                        :rules="{ required: true, message: '请输入详细描述', trigger: 'blur' }"
+                        class="mb-0!">
+                        <el-input
+                          v-model="row.constructionDetail"
+                          type="textarea"
+                          :autosize="{ minRows: 1 }"
+                          show-word-limit
+                          :maxlength="1000"
+                          placeholder="请输入详细描述"
+                          :disabled="fillStatus === '1'" />
+                      </el-form-item>
+                    </template>
+                  </ZmTableColumn>
+
+                  <ZmTableColumn label="操作" width="80" fixed="right" align="center">
+                    <template #default="{ $index }">
+                      <el-button
+                        link
+                        type="danger"
+                        :icon="Delete"
+                        @click="removeProductionStatusRow($index)"
+                        :disabled="fillStatus === '1'">
+                        删除
+                      </el-button>
+                    </template>
+                  </ZmTableColumn>
+                </ZmTable>
+              </el-form-item>
+            </div>
+
+            <el-form-item>
+              <el-button
+                :loading="submitLoading"
+                type="primary"
+                @click="getFillInfo"
+                v-show="showStatus"
+                >{{ t('operationFillForm.confirm') }}</el-button
+              >
+              <el-button type="info" @click="deleteFillInfo" v-show="showStatus">{{
+                t('operationFill.clear')
+              }}</el-button>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { IotOpeationFillApi, IotOpeationFillVO } from '@/api/pms/iotopeationfill'
+import { ElMessage, FormInstance, FormRules } from 'element-plus'
+import moment from 'moment'
+import { getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { useRoute } from 'vue-router'
+import { calculateDuration, formatT } from '@/utils/formatTime'
+import { Delete, Plus } from '@element-plus/icons-vue'
+import { useDebounceFn } from '@vueuse/core'
+import dayjs from 'dayjs'
+
+/** 运行记录填报 列表 */
+defineOptions({ name: 'FillOrderInfo' })
+
+const route = useRoute()
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const loading = ref(true) // 列表的加载中
+const { params } = useRoute() // 查询参数
+const deptId = params.id as string
+const list = ref<IotOpeationFillVO[]>([]) // 列表的数据
+const attrList = ref<IotOpeationFillVO[]>([]) // 非累计属性集合
+const attrList1 = ref<IotOpeationFillVO[]>([]) // 累计属性集合
+const attrList2 = ref<IotOpeationFillVO[]>([]) // 属性集合
+let companyName = ref('')
+
+let fillStatus = deptId.split(',')[4]
+let createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
+let showStatus = true
+const queryParams = reactive<any>({
+  pageNo: 1,
+  pageSize: 10,
+  deviceCode: undefined,
+  deviceName: undefined,
+  fillContent: undefined,
+  deviceType: undefined,
+  deviceComponent: undefined,
+  deptId: undefined,
+  orgName: undefined,
+  proId: undefined,
+  proName: undefined,
+  teamId: undefined,
+  teamName: undefined,
+  dutyName: undefined,
+  creDate: [],
+  createTime: [],
+  deviceCategoryId: 1,
+  deviceId: undefined,
+  threshold: undefined,
+  defaultValue: undefined,
+  isSum: undefined
+})
+
+interface ReportDetail {
+  reportDate: number
+  startTime: string
+  endTime: string
+  duration: number
+  currentDepth: number
+  currentOperation: string
+  constructionDetail: string
+}
+
+const taskId = ref<number | undefined>(undefined)
+const taskOptions = ref<{ label: string; value: number }[]>([])
+
+const reportDetails = ref<ReportDetail[]>([])
+
+const addProductionStatusRow = () => {
+  if (!reportDetails.value) {
+    reportDetails.value = []
+  }
+  reportDetails.value.push({
+    reportDate: createTime ? dayjs(createTime).valueOf() : dayjs().valueOf(),
+    startTime: '',
+    endTime: '',
+    duration: 0,
+    currentDepth: 0,
+    currentOperation: '',
+    constructionDetail: ''
+  })
+}
+
+const removeProductionStatusRow = (index: number) => {
+  if (index === 0) {
+    message.warning('至少填写一条生产动态')
+    return
+  }
+  reportDetails.value.splice(index, 1)
+}
+
+const acalculateDuration = (row: any) => {
+  calculateDuration(row)
+  inputCurrentDepth()
+}
+
+const inputCurrentDepth = useDebounceFn(function inputCurrentDepth() {
+  const details = reportDetails.value
+
+  if (Array.isArray(details) && details.length > 0) {
+    const latestDetail = details.reduce((prev, current) => {
+      const currentFullTime = dayjs(current.reportDate)
+        .hour(parseInt(current.endTime.split(':')[0]))
+        .minute(parseInt(current.endTime.split(':')[1]))
+        .valueOf()
+
+      const prevFullTime = dayjs(prev.reportDate)
+        .hour(parseInt(prev.endTime.split(':')[0]))
+        .minute(parseInt(prev.endTime.split(':')[1]))
+        .valueOf()
+
+      return currentFullTime >= prevFullTime ? current : prev
+    })
+
+    const currentDepth = attrList.value.find((item) => item.description === 'currentDepth')
+    if (currentDepth) currentDepth.fillContent = latestDetail.currentDepth
+  }
+}, 300)
+
+// const inputCurrentDepth = useDebounceFn(function inputCurrentDepth(val: any, index: number) {
+//   if (reportDetails.value && index === reportDetails.value.length - 1) {
+//     const currentDepth = attrList.value.find((item) => item.description === 'currentDepth')
+//     if (currentDepth) currentDepth.fillContent = val
+//   }
+// }, 300)
+
+const showDepth = computed(() => {
+  return attrList.value.some((item) => item.description === 'currentDepth')
+})
+
+const NON_KEYS = [
+  'repairTime',
+  'selfStopTime',
+  'accidentTime',
+  'complexityTime',
+  'rectificationTime',
+  'waitingStopTime',
+  'partyaDesign',
+  'partyaPrepare',
+  'partyaResource',
+  'relocationTime',
+  'winterBreakTime',
+  'otherNptTime'
+]
+
+const keys = [
+  'repairTime',
+  'selfStopTime',
+  'accidentTime',
+  'complexityTime',
+  'rectificationTime',
+  'waitingStopTime',
+  'partyaDesign',
+  'partyaPrepare',
+  'partyaResource',
+  'relocationTime',
+  'winterBreakTime',
+  'otherNptTime',
+  'drillingWorkingTime',
+  'otherProductionTime',
+  'ratedProductionTime',
+  'productionTime',
+  'dailyInjectGasTime',
+  'otherNptReason'
+]
+
+const sumNonProdTimes = () => {
+  let sum = 0
+  NON_KEYS.forEach((field) => {
+    sum += attrList.value.find((item) => item.description === field)?.fillContent || 0
+  })
+  return sum
+}
+
+const rhValidateTotalTime =
+  (isNon: boolean = false) =>
+  (_rule: any, _value: any, callback: any) => {
+    const gasTime =
+      attrList.value.find((item) => item.description === 'dailyInjectGasTime')?.fillContent || 0
+    const nonProdSum = sumNonProdTimes()
+
+    let total = 0
+    let msg = ''
+
+    total = parseFloat((gasTime + nonProdSum).toFixed(2))
+    msg = `运转(${gasTime})+非生产(${nonProdSum})=${total}H,必须为24H`
+
+    if (Math.abs(total - 24) > 0.01) {
+      if (!isNon) callback(new Error(msg))
+      else callback(new Error())
+    } else {
+      callback()
+    }
+  }
+
+// const ryXjValidateTotalTime =
+//   (isNon: boolean = false) =>
+//   (_rule: any, _value: any, callback: any) => {
+//     const rateTime =
+//       attrList.value.find((item) => item.description === 'ratedProductionTime')?.fillContent || 0
+//     const time =
+//       attrList.value.find((item) => item.description === 'productionTime')?.fillContent || 0
+
+//     const nonProdSum = sumNonProdTimes()
+
+//     let total = 0
+//     let msg = ''
+
+//     total = parseFloat((time + nonProdSum).toFixed(2))
+//     msg = `生产(${time})+非生产(${nonProdSum})=${total}H,必须等于额定${rateTime}H`
+
+//     if (Math.abs(total - rateTime) > 0.01) {
+//       if (!isNon) callback(new Error(msg))
+//       else callback(new Error())
+//     } else {
+//       callback()
+//     }
+//   }
+
+const ryValidateTotalTime =
+  (isNon: boolean = false) =>
+  (_rule: any, _value: any, callback: any) => {
+    const drillingTime =
+      attrList.value.find((item) => item.description === 'drillingWorkingTime')?.fillContent || 0
+    const otherTime =
+      attrList.value.find((item) => item.description === 'otherProductionTime')?.fillContent || 0
+
+    const nonProdSum = sumNonProdTimes()
+
+    let total = 0
+    let msg = ''
+
+    total = parseFloat((drillingTime + otherTime + nonProdSum).toFixed(2))
+    msg = `进尺(${drillingTime})+其他(${otherTime})+非生产(${nonProdSum})=${total}H,必须为24H`
+
+    if (Math.abs(total - 24) > 0.01) {
+      if (!isNon) callback(new Error(msg))
+      else callback(new Error())
+    } else {
+      callback()
+    }
+  }
+
+const validateOtherReason = (_rule: any, value: any, callback: any) => {
+  const time = attrList.value.find((item) => item.description === 'otherNptTime')?.fillContent || 0
+  if (time > 0 && !value) {
+    callback(new Error('填写了其他时间,必须说明原因'))
+  } else {
+    callback()
+  }
+}
+
+const rules = reactive<FormRules>({
+  dailyInjectGasTime: [
+    { required: true, message: '请输入当日运转时间' },
+    { validator: rhValidateTotalTime() }
+  ],
+  drillingWorkingTime: [
+    { required: true, message: '请输入进尺工作时间' },
+    { validator: ryValidateTotalTime() }
+  ],
+  nightSupervisors: { required: true, message: '请输入夜班跟班干部' },
+  daySupervisors: { required: true, message: '请输入白班跟班干部' },
+  dailyGasInjection: [
+    {
+      validator: (_rule, value, callback) => {
+        if (companyName.value !== 'rh' || !taskId.value || noGasTasks.value.includes(taskId.value))
+          return callback()
+
+        const dailyInjectGasTime =
+          attrList.value.find((item) => item.description === 'dailyInjectGasTime')?.fillContent || 0
+
+        // if (dailyInjectGasTime > 0 && !(value > 0)) {
+        //   return callback(new Error('当日运转时间大于0,注气量也需要大于0'))
+        // }
+
+        const hasDailyGasInjection = value !== undefined && value !== null && value !== ''
+
+        if (dailyInjectGasTime > 0 && !hasDailyGasInjection) {
+          return callback(new Error('当日运转时间大于0,需要填写当日注气量'))
+        }
+
+        callback()
+      },
+      trigger: ['blur', 'change']
+    }
+  ],
+  otherProductionTime: [{ validator: ryValidateTotalTime() }],
+  // ratedProductionTime: [
+  //   { required: true, message: '请输入额定生产时间', trigger: 'blur' },
+  //   { validator: ryXjValidateTotalTime(), trigger: 'blur' }
+  // ],
+  // productionTime: [
+  //   { required: true, message: '请输入生产时间', trigger: 'blur' },
+  //   { validator: ryXjValidateTotalTime(), trigger: 'blur' }
+  // ],
+  otherNptReason: [{ validator: validateOtherReason }]
+})
+
+const totalValidatorComputed = computed(() => {
+  if (attrList.value.some((item) => item.description === 'dailyInjectGasTime')) {
+    return rhValidateTotalTime
+  }
+  //  else if (attrList.value.some((item) => item.description === 'ratedProductionTime')) {
+  //   return ryXjValidateTotalTime
+  // }
+  else if (attrList.value.some((item) => item.description === 'drillingWorkingTime')) {
+    return ryValidateTotalTime
+  }
+})
+
+nextTick(() => {
+  const validator = totalValidatorComputed.value
+  if (!validator) return
+  NON_KEYS.forEach((field) => {
+    rules[field] = [{ validator: validator(true) }]
+  })
+})
+
+let cxStatus = true
+
+// 计算数字输入的最大长度(根据阈值动态计算)
+const calculateMaxLength = (item: any) => {
+  if (item.type !== 'double' || !item.threshold) return undefined
+
+  const max = parseFloat(item.threshold)
+  if (isNaN(max)) return undefined
+
+  // 整数部分长度 + 可能的小数点 + 两位小数
+  return max.toString().length + (max.toString().includes('.') ? 0 : 3)
+}
+
+// 简单的节流函数,避免提示信息过于频繁
+const throttle = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
+  let lastTime = 0
+
+  return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
+    const now = Date.now()
+    if (now - lastTime >= delay) {
+      fn.apply(this, args)
+      lastTime = now
+    }
+  }
+}
+
+const showComponent = () => {
+  if (JSON.parse(fillStatus) === 1 || JSON.parse(fillStatus) === 3) {
+    showStatus = false
+  }
+}
+
+const noGasTasks = ref<number[]>([])
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    queryParams.deptId = deptId.split(',')[0]
+    queryParams.userId = deptId.split(',')[1]
+    queryParams.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
+    queryParams.orderId = deptId.split(',')[3]
+    const data = await IotOpeationFillApi.getIotOpeationFillPage(queryParams)
+    list.value = data
+    if (cxStatus) {
+      queryParams.deviceCategoryId = list.value[0].deviceCategoryId
+      queryParams.deptId = list.value[0].deptId
+      queryParams.deviceCode = list.value[0].deviceCode
+      queryParams.deviceName = list.value[0].deviceName
+      queryParams.deviceId = list.value[0].deviceId
+    }
+    await getAttrList()
+
+    const daily = list.value.find((item) => item.deviceName === '生产日报') ?? {
+      wellNamePair: {},
+      noGasTasks: []
+    }
+
+    taskOptions.value = Object.keys(daily.wellNamePair ?? {}).map((key) => ({
+      label: daily.wellNamePair[key],
+      value: Number(key)
+    }))
+
+    noGasTasks.value = (daily.noGasTasks ?? []).map(Number)
+
+    taskId.value = taskOptions.value[0]?.value
+
+    IotOpeationFillApi.getReportDetails(deptId.split(',')[3]).then((res) => {
+      reportDetails.value = (res ? (res as any[]) : []).map((item) => ({
+        reportDate: item.reportDate ?? dayjs(createTime).valueOf(),
+        startTime: formatT(item.startTime),
+        endTime: formatT(item.endTime),
+        duration: item.duration,
+        currentDepth: item.currentDepth,
+        currentOperation: item.currentOperation,
+        constructionDetail: item.constructionDetail
+      }))
+
+      if (!reportDetails.value.length) {
+        addProductionStatusRow()
+      }
+    })
+  } finally {
+    loading.value = false
+  }
+}
+function formatTimestamp(timestamp) {
+  // const date = new Date(timestamp * 1000)
+  return moment.unix(timestamp).format('YYYY-MM-DD')
+}
+
+const open = async (_type: string, id?: number) => {
+  alert(id)
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+let devName = ''
+const openFill = (
+  deviceCategoryId?: number,
+  deviceId?: number,
+  deptId?: number,
+  deviceName?: string,
+  deviceCode?: string
+) => {
+  queryParams.deviceCategoryId = deviceCategoryId
+  queryParams.deptId = deptId
+  queryParams.deviceCode = deviceCode
+  queryParams.deviceName = deviceName
+  if (queryParams.deviceName == '生产日报') {
+    devName = '生产日报'
+  }
+  queryParams.deviceId = deviceId
+  getAttrList()
+}
+// 处理输入事件,实时限制输入格式和最大值
+const handleInput = (item: any) => {
+  if (item.type === 'double') {
+    // 保存原始值用于后续比较
+    const originalValue = item.fillContent
+
+    // 1. 格式验证:只允许数字和小数点
+    item.fillContent = item.fillContent.replace(/[^\d.]/g, '')
+    // 确保只有一个小数点
+    item.fillContent = item.fillContent.replace(/\.{2,}/g, '.')
+    // 确保小数点不在开头
+    item.fillContent = item.fillContent.replace(/^\./g, '')
+    // 限制小数位数为两位
+    item.fillContent = item.fillContent.replace(/(\d+)\.(\d{2}).*/, '$1.$2')
+
+    // 2. 最大值验证
+    if (item.threshold) {
+      const value = parseFloat(item.fillContent)
+      const max = parseFloat(item.threshold)
+
+      if (!isNaN(value) && !isNaN(max) && value > max) {
+        // 输入值超过阈值时,恢复到修改前的值
+        item.fillContent = originalValue
+          .replace(/[^\d.]/g, '')
+          .replace(/\.{2,}/g, '.')
+          .replace(/^\./g, '')
+          .replace(/(\d+)\.(\d{2}).*/, '$1.$2')
+
+        if (parseFloat(item.fillContent) > max) {
+          item.fillContent = max.toString()
+        }
+
+        throttle(() => {
+          ElMessage.warning(t('operationFillForm.exceedMax', { max }))
+        }, 1000)()
+      }
+    }
+
+    if (companyName.value === 'rd') {
+      // 3. 累计值限制验证(改为弹窗提示但允许继续)
+      if (item.maxAllowedValue !== undefined) {
+        const value = parseFloat(item.fillContent)
+        if (!isNaN(value) && value > item.maxAllowedValue) {
+          // 不自动修改值,而是显示警告弹窗
+          let limitDescription = ''
+          if (item.limitType === 'km') {
+            limitDescription = `当前累计值${item.currentSumValue} + 3000`
+          } else if (item.limitType === 'time') {
+            limitDescription = `当前累计值${item.currentSumValue} + 100`
+          }
+
+          ElMessage.warning(
+            `填报值 ${value} 超过限制 ${item.maxAllowedValue} (${limitDescription}),请确认是否正确!`
+          )
+        }
+      }
+    }
+  }
+}
+
+const getAttrList = async () => {
+  loading.value = true
+  try {
+    queryParams.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
+    const data = await IotOpeationFillApi.getAttrs(queryParams)
+
+    const timeKeys = keys.filter((k) => k !== 'otherNptReason')
+
+    attrList.value = data[0].nonSumList.map((item) => {
+      if (timeKeys.includes(item.description)) {
+        item.fillContent = item.fillContent ?? 0
+      }
+      return item
+    })
+    attrList1.value = data[0].sumList
+
+    // 建立累计数据映射,用于后续验证
+    const sumMap = new Map()
+    attrList1.value.forEach((item) => {
+      // 创建匹配规则:移除"填报"字样的差异,保留核心名称
+      const coreName = item.name.replace(/填报/g, '')
+      sumMap.set(coreName, item)
+    })
+
+    // 为非累计数据添加最大值限制
+    attrList.value.forEach(function (item) {
+      let strVal = String(item.fillContent || '').trim()
+
+      if (strVal !== '') {
+        const num = Number(strVal)
+
+        if (!isNaN(num)) {
+          if (strVal.includes('.')) {
+            item.fillContent = Number(num.toFixed(2))
+          } else {
+            item.fillContent = num
+          }
+        }
+      }
+
+      // if (item.fillContent !== '' && item.fillContent !== null) {
+      //   const num = Number(item.fillContent)
+      //   if (!isNaN(num)) {
+      //     if (item.fillContent.includes('.')) {
+      //       item.fillContent = Number(num.toFixed(2))
+      //     } else {
+      //       item.fillContent = Math.floor(num)
+      //     }
+      //   }
+      // }
+
+      if (companyName.value === 'rd') {
+        // 添加最大值限制逻辑
+        const coreName = item.name.replace(/填报/g, '')
+        const sumItem = sumMap.get(coreName)
+        if (sumItem) {
+          // 根据字段名称判断使用哪种限制规则
+          if (item.name.includes('公里数填报')) {
+            // 公里数限制:当前累计值 + 3000
+            item.maxAllowedValue = sumItem.totalRunTime + 3000
+            item.currentSumValue = sumItem.totalRunTime // 保存当前累计值用于提示
+            item.limitType = 'km' // 标记为公里数限制
+          } else if (item.name.includes('运转时长填报')) {
+            // 运转时长限制:当前累计值 + 100
+            item.maxAllowedValue = sumItem.totalRunTime + 100
+            item.currentSumValue = sumItem.totalRunTime // 保存当前累计值用于提示
+            item.limitType = 'time' // 标记为时长限制
+          }
+        }
+      }
+
+      item.deviceCode = queryParams.deviceCode
+      item.deptId = queryParams.deptId
+      item.deviceId = queryParams.deviceId
+      item.deviceCategoryId = queryParams.deviceCategoryId
+      item.modelId = item.id
+      console.log(item.fillContent)
+    })
+
+    attrList1.value.forEach(function (item) {
+      item.deviceCode = queryParams.deviceCode
+      item.deptId = queryParams.deptId
+      item.deviceId = queryParams.deviceId
+      item.deviceCategoryId = queryParams.deviceCategoryId
+      item.modelId = item.id
+    })
+  } finally {
+    loading.value = false
+  }
+}
+const formRef = ref<FormInstance[] | null>(null)
+
+const submitLoading = ref(false)
+
+/** 获取填写信息保存到后台*/
+const getFillInfo = async () => {
+  if (!formRef.value) return
+
+  submitLoading.value = true
+
+  try {
+    const validations = formRef.value.map((form) => form.validate())
+    await Promise.all(validations)
+
+    const company = await IotOpeationFillApi.getOrgName(route.params.id.toString().split(',')[0])
+
+    if (devName != '生产日报') {
+      // 检查必填字段
+      const emptyFields = attrList.value.filter((item) => {
+        // 只检查非disabled的字段
+        return (
+          !(item.isCollection === 1 || fillStatus === '1') &&
+          (item.fillContent === undefined || item.fillContent === '')
+        )
+      })
+
+      if (emptyFields.length > 0) {
+        ElMessage.error(t('operationFillForm.fill'))
+        return
+      }
+    }
+
+    if (company === 'rd') {
+      // 检查是否有超出累计值限制的字段
+      const exceededFields = attrList.value.filter((item) => {
+        if (
+          item.type === 'double' &&
+          item.maxAllowedValue !== undefined &&
+          item.fillContent !== '' &&
+          item.fillContent !== null
+        ) {
+          const value = parseFloat(item.fillContent)
+          return !isNaN(value) && value > item.maxAllowedValue
+        }
+        return false
+      })
+
+      // 如果有超出限制的字段,提示用户确认
+      if (exceededFields.length > 0) {
+        let exceededMessage = ''
+        exceededFields.forEach((field) => {
+          let limitDescription = ''
+          if (field.limitType === 'km') {
+            limitDescription = `(${field.currentSumValue} + 3000)`
+          } else if (field.limitType === 'time') {
+            limitDescription = `(${field.currentSumValue} + 100)`
+          }
+          exceededMessage += `${field.name};\n`
+        })
+
+        // exceededMessage += '\n是否继续保存?'
+
+        const confirmResult = await message.confirm(
+          exceededMessage,
+          '以下填报项超出限制,是否继续保存?',
+          '继续保存',
+          '取消'
+        )
+        if (!confirmResult) {
+          return // 用户取消保存
+        }
+      }
+    }
+
+    attrList2.value = attrList.value.concat(attrList1.value)
+
+    attrList2.value.forEach(function (item) {
+      item.pointName = item.name
+      item.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
+      item.userId = deptId.split(',')[1]
+      item.id = deptId.split(',')[3]
+    })
+    const data = attrList2.value as unknown as IotOpeationFillVO
+
+    const reqData = {
+      createReqVO: data,
+      reportDetails: reportDetails.value.map((item) => ({
+        ...item,
+        taskId: taskId.value
+      }))
+    }
+
+    await IotOpeationFillApi.insertLog(reqData)
+    message.success(t('common.createSuccess'))
+    // 发送操作成功的事件
+    emit('success')
+    cxStatus = false
+    getList()
+  } catch (error) {
+    console.error('保存失败:', error)
+  } finally {
+    submitLoading.value = false
+  }
+}
+
+/**清空填写信息*/
+const deleteFillInfo = () => {
+  attrList.value.forEach(function (item) {
+    item.fillContent = ''
+  })
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  const company = await IotOpeationFillApi.getOrgName(route.params.id.toString().split(',')[0])
+  companyName.value = company
+
+  getList()
+  showComponent()
+})
+</script>
+<style scoped>
+.scrollable-form {
+  /* 设置最大高度,超过这个高度会出现滚动条 */
+  max-height: 100%; /* 根据你的需求调整 */
+
+  /* 可选:添加内边距和边框美化 */
+  padding: 16px;
+
+  /* 超出部分显示垂直滚动条 */
+  overflow-y: auto;
+  border: 1px solid #e5e7eb;
+  border-radius: 4px;
+}
+
+/* 可选:美化滚动条 */
+.scrollable-form::-webkit-scrollbar {
+  width: 6px;
+}
+
+.scrollable-form::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 3px;
+}
+
+.scrollable-form::-webkit-scrollbar-thumb {
+  background: #c1c1c1;
+  border-radius: 3px;
+}
+
+.scrollable-form::-webkit-scrollbar-thumb:hover {
+  background: #a8a8a8;
+}
+
+.back-red {
+  /* 红色背景 */
+  background-color: red;
+}
+
+.back-blue {
+  background-color: grey;
+}
+
+.step-container {
+  display: grid;
+  grid-template-columns: 220px 1fr;
+  gap: 10px;
+  height: 100%;
+  min-height: 600px;
+}
+
+.steps-nav {
+  padding-right: 15px;
+  overflow-y: auto;
+}
+
+.form-wrapper {
+  padding: 30px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
+}
+
+.navigation-controls {
+  margin-top: 40px;
+  text-align: center;
+}
+
+.custom-label {
+  padding: 0 10px;
+  font-size: 17px;
+  font-weight: 1000;
+  color: forestgreen;
+}
+
+.custom-label1 {
+  padding: 0 10px;
+  font-size: 17px;
+  font-weight: 1000;
+}
+
+.has-border {
+  padding: 3px 8px; /* 适当增加内边距,避免文字太贴近边框 */
+  font-size: 20px;
+  border: 2px solid #333; /* 加粗到2px,使用深灰色#333让边框更清晰 */
+  border-radius: 2px; /* 轻微圆角,可选 */
+}
+
+::v-deep .el-step__icon {
+  color: #fff;
+  background-color: #409eff;
+  border: 0;
+}
+
+.step-title-wrapper {
+  position: relative;
+  display: inline-flex;
+  padding-right: 25px;
+  align-items: center;
+  gap: 8px;
+}
+
+/* 覆盖步骤条默认样式 */
+:deep(.custom-steps) {
+  /* 调整头部位置 */
+  .el-step__head {
+    top: 3px;
+  }
+
+  /* 标题容器定位 */
+  .el-step__title {
+    display: inline-block;
+    padding-right: 0;
+    margin-left: 10px;
+  }
+
+  /* 步骤连接线 */
+  .el-step__line {
+    left: 11px;
+    background-color: #ebeef5;
+  }
+
+  /* 当前步骤样式 */
+  .is-process .title-text {
+    font-weight: 600;
+    color: #409eff;
+  }
+
+  /* 完成状态图标 */
+  .is-finish .tip-icon {
+    color: #67c23a;
+  }
+}
+
+.horizontal-container {
+  display: flex;
+  flex-wrap: wrap; /* 允许换行 */
+  gap: 20px; /* 项目间距 */
+}
+
+.form-item-container {
+  flex: 1; /* 等宽分布 */
+  min-width: 200px; /* 最小宽度防止挤压 */
+}
+
+/* 新增日报填报项的边框样式 */
+.report-border {
+  padding: 2px 4px;
+  border: 2px solid #42b983; /* 使用Vue标志性的绿色边框 */
+  border-radius: 4px;
+}
+
+:deep(.table-form-item) {
+  .el-form-item__content {
+    margin-left: 0 !important;
+  }
+}
+</style>

+ 1680 - 1148
src/views/pms/iotopeationfill/index1.vue

@@ -1,575 +1,101 @@
-<template>
-  <ContentWrap>
-    <el-tabs type="border-card" tab-position="left" v-loading="loading" style="height: 84vh">
-      <el-tab-pane
-        style="height: 100%"
-        v-for="(deviceItem, deviceIndex) in list"
-        :key="deviceIndex">
-        <template #label>
-          <span
-            :class="['custom-label', { 'has-border': deviceItem.deviceName === '生产日报' }]"
-            v-if="deviceItem.isFill === 1"
-            @click="
-              openFill(
-                deviceItem.deviceCategoryId,
-                deviceItem.deviceId,
-                deviceItem.deptId,
-                deviceItem.deviceName,
-                deviceItem.deviceCode
-              )
-            ">
-            {{ deviceItem.deviceCode }} ({{ deviceItem.deviceName }})
-          </span>
-          <span
-            :class="['custom-label1', { 'has-border': deviceItem.deviceName === '生产日报' }]"
-            v-else
-            @click="
-              openFill(
-                deviceItem.deviceCategoryId,
-                deviceItem.deviceId,
-                deviceItem.deptId,
-                deviceItem.deviceName,
-                deviceItem.deviceCode
-              )
-            ">
-            {{ deviceItem.deviceCode }} ({{ deviceItem.deviceName }})
-          </span>
-        </template>
-        <div class="form-wrapper h-full">
-          <el-form
-            ref="formRef"
-            size="default"
-            label-width="120px"
-            class="scrollable-form"
-            :model="{ attrList: attrList, reportDetails, taskId }">
-            <div style="margin-left: 24px">
-              <el-form class="demo-form-inline" :inline="true">
-                <el-form-item :label="t('common.createTime')" class="custom-label1">
-                  <span style="text-decoration: underline">
-                    {{ createTime }}
-                  </span>
-                </el-form-item>
-                <el-form-item :label="t('operationFillForm.team')" class="custom-label1">
-                  <span style="text-decoration: underline">
-                    {{ deviceItem.orgName }}
-                  </span>
-                </el-form-item>
-                <el-form-item
-                  v-if="
-                    deviceItem.deviceName === '生产日报' &&
-                    companyName !== 'ry' &&
-                    companyName !== 'rh'
-                  "
-                  label="井号"
-                  class="custom-label1">
-                  <span style="text-decoration: underline">
-                    {{ deviceItem.wellName }}
-                  </span>
-                </el-form-item>
-                <el-form-item
-                  v-else-if="
-                    deviceItem.deviceName === '生产日报' &&
-                    (companyName === 'ry' || companyName === 'rh') &&
-                    taskOptions.length > 0
-                  "
-                  label="井号"
-                  class="custom-label1"
-                  prop="taskId">
-                  <el-select
-                    v-model="taskId"
-                    placeholder="请选择井号"
-                    :options="taskOptions"
-                    class="w-40!" />
-                </el-form-item>
-                <el-form-item
-                  v-else-if="deviceItem.deviceName === '生产日报'"
-                  label="井号"
-                  class="custom-label1">
-                  <span style="text-decoration: underline">
-                    {{ deviceItem.wellName }}
-                  </span>
-                </el-form-item>
-                <el-row :gutter="20">
-                  <el-col
-                    v-for="(summaryItem, summaryIndex) in attrList1"
-                    :key="summaryIndex"
-                    :span="24">
-                    <el-form-item :label="summaryItem.name" class="custom-label1">
-                      <span style="text-decoration: underline">
-                        {{ summaryItem.totalRunTime }}
-                      </span>
-                    </el-form-item>
-                  </el-col>
-                </el-row>
-                <!--                <el-form-item :label="t('operationFillForm.sumTime')" class="custom-label1">
-                  <span style="text-decoration: underline;">
-                  {{totalRunTime1}}h
-                  </span>
-                </el-form-item>-->
-              </el-form>
-            </div>
-
-            <div
-              v-for="(attrItem, attrIndex) in attrList.filter(
-                (item) => !keys.includes(item.description)
-              )"
-              :key="attrIndex"
-              style="margin-left: 24px">
-              <!-- 添加提示文字 -->
-              <div v-if="attrItem.isCollection === 1" class="plc-tip">
-                <el-alert
-                  :title="t('operationFillForm.alert')"
-                  type="warning"
-                  :closable="false"
-                  center
-                  show-icon
-                  style="width: 320px" />
-              </div>
-
-              <el-form-item
-                v-if="companyName === 'ry' ? attrItem.description !== 'productionStatus' : true"
-                :label="attrItem.name"
-                :prop="
-                  'attrList.' +
-                  attrList.findIndex((i) => i.description === attrItem.description) +
-                  '.fillContent'
-                "
-                label-position="top"
-                :rules="rules[attrItem.description]">
-                <div v-if="fillStatus === '1'">
-                  <el-select
-                    disabled
-                    v-model="attrItem.fillContent"
-                    v-if="attrItem.type === 'enum' && attrItem.description !== null"
-                    style="width: 200px">
-                    <el-option
-                      v-for="dict in attrItem.name === '非生产原因'
-                        ? getIntDictOptions(attrItem.description)
-                        : getStrDictOptions(attrItem.description)"
-                      :key="dict.label"
-                      :label="dict.label"
-                      :value="
-                        attrItem.name === '非生产原因' ? Number(dict.value) : dict.value.toString()
-                      " />
-                  </el-select>
-                  <el-input
-                    v-else
-                    v-model="attrItem.fillContent"
-                    clearable
-                    style="width: 200px; margin-right: 10px"
-                    disabled />
-                </div>
-
-                <el-input
-                  v-else-if="attrItem.type === 'textarea'"
-                  v-model="attrItem.fillContent"
-                  type="textarea"
-                  clearable
-                  style="width: 200px" />
-                <el-select
-                  v-model="attrItem.fillContent"
-                  clearable
-                  v-else-if="attrItem.type === 'enum' && attrItem.description !== null"
-                  style="width: 200px"
-                  filterable>
-                  <el-option
-                    v-for="dict in attrItem.name === '非生产原因'
-                      ? getIntDictOptions(attrItem.description)
-                      : getStrDictOptions(attrItem.description)"
-                    :key="dict.label"
-                    :label="dict.label"
-                    :value="
-                      attrItem.name === '非生产原因' ? Number(dict.value) : dict.value.toString()
-                    " />
-                </el-select>
-                <el-input
-                  v-else
-                  v-model="attrItem.fillContent"
-                  clearable
-                  style="width: 200px"
-                  :placeholder="
-                    attrItem.type === 'double'
-                      ? t('operationFillForm.enterNumber')
-                      : t('operationFillForm.enterContent')
-                  "
-                  @input="handleInput(attrItem)"
-                  :maxlength="
-                    attrItem.type === 'double' ? calculateMaxLength(attrItem) : undefined
-                  " />
-              </el-form-item>
-            </div>
-
-            <div
-              v-for="(attrItem, attrIndex) in attrList"
-              :key="attrIndex"
-              style="margin-left: 24px">
-              <el-divider v-if="attrItem.description === 'repairTime'" content-position="left"
-                >非生产时间</el-divider
-              >
-              <el-form-item
-                v-if="
-                  keys.includes(attrItem.description) && attrItem.description !== 'otherNptReason'
-                "
-                label-position="top"
-                :label="attrItem.name"
-                :prop="'attrList.' + attrIndex + '.fillContent'"
-                :rules="rules[attrItem.description]">
-                <el-input-number
-                  class="w-80!"
-                  :min="0"
-                  :max="24"
-                  v-model="attrItem.fillContent"
-                  :controls="false"
-                  align="left"
-                  placeholder="请输入数字"
-                  :disabled="fillStatus === '1'" />
-              </el-form-item>
-
-              <el-form-item
-                v-else-if="attrItem.description === 'otherNptReason'"
-                label-position="top"
-                :label="attrItem.name"
-                :prop="'attrList.' + attrIndex + '.fillContent'"
-                :rules="rules[attrItem.description]">
-                <el-input
-                  class="w-80!"
-                  v-model="attrItem.fillContent"
-                  placeholder="请输入其他非生产原因"
-                  :disabled="fillStatus === '1'" />
-              </el-form-item>
-            </div>
-
-            <div v-if="companyName === 'ry' && deviceItem.deviceName === '生产日报'">
-              <div class="flex items-center justify-between mb-6">
-                <div class="flex items-center gap-2">
-                  <div class="bg-[var(--el-color-primary)] w-1 h-5 rounded-full"></div>
-                  <div class="text-lg font-medium text-[var(--el-text-color-primary)]"
-                    >生产动态</div
-                  >
-                </div>
-                <el-button
-                  :disabled="fillStatus === '1'"
-                  type="primary"
-                  link
-                  :icon="Plus"
-                  @click="addProductionStatusRow">
-                  添加一行
-                </el-button>
-              </div>
-              <el-form-item prop="reportDetails" class="table-form-item">
-                <ZmTable :data="reportDetails" :loading="false" class="mb-4">
-                  <ZmTableColumn label="日期" :width="180" prop="reportDate">
-                    <template #default="{ row, $index }">
-                      <el-form-item
-                        v-if="$index >= 0"
-                        class="mb-0!"
-                        :prop="`reportDetails.${$index}.reportDate`"
-                        :rules="{
-                          required: true,
-                          message: '请选择日期',
-                          trigger: ['change', 'blur'],
-                          type: 'number'
-                        }">
-                        <el-date-picker
-                          v-model="row.reportDate"
-                          placeholder="选择日期"
-                          clearable
-                          class="w-full!"
-                          value-format="x"
-                          :disabled="fillStatus === '1'"
-                          @change="inputCurrentDepth()" />
-                      </el-form-item>
-                    </template>
-                  </ZmTableColumn>
-
-                  <ZmTableColumn :width="130" label="开始时间" prop="startTime">
-                    <template #default="{ row, $index }">
-                      <el-form-item
-                        v-if="$index >= 0"
-                        class="mb-0!"
-                        :prop="`reportDetails.${$index}.startTime`"
-                        :rules="{
-                          required: true,
-                          message: '请选择开始时间',
-                          trigger: ['change', 'blur']
-                        }">
-                        <el-time-picker
-                          v-model="row.startTime"
-                          placeholder="选择开始时间"
-                          clearable
-                          format="HH:mm"
-                          value-format="HH:mm"
-                          class="w-full!"
-                          @change="acalculateDuration(row)"
-                          :disabled="fillStatus === '1'" />
-                      </el-form-item>
-                    </template>
-                  </ZmTableColumn>
-                  <ZmTableColumn :width="130" label="结束时间" prop="endTime">
-                    <template #default="{ row, $index }">
-                      <el-form-item
-                        v-if="$index >= 0"
-                        class="mb-0!"
-                        :prop="`reportDetails.${$index}.endTime`"
-                        :rules="{
-                          required: true,
-                          message: '请选择结束时间',
-                          trigger: ['change', 'blur']
-                        }">
-                        <el-time-picker
-                          v-model="row.endTime"
-                          placeholder="选择结束时间"
-                          clearable
-                          format="HH:mm"
-                          value-format="HH:mm"
-                          class="w-full!"
-                          @change="acalculateDuration(row)"
-                          :disabled="fillStatus === '1'" />
-                      </el-form-item>
-                    </template>
-                  </ZmTableColumn>
-                  <ZmTableColumn :width="80" label="时长(H)" prop="duration" />
-
-                  <ZmTableColumn label="工况" min-width="140">
-                    <template #default="{ row, $index }">
-                      <el-form-item
-                        v-if="$index >= 0"
-                        :prop="`reportDetails.${$index}.currentOperation`"
-                        :rules="{
-                          required: true,
-                          message: '请输入工况',
-                          trigger: ['change', 'blur']
-                        }"
-                        class="mb-0!">
-                        <el-input
-                          v-model="row.currentOperation"
-                          type="textarea"
-                          :autosize="{ minRows: 1 }"
-                          show-word-limit
-                          :maxlength="1000"
-                          placeholder="请输入工况"
-                          :disabled="fillStatus === '1'" />
-                      </el-form-item>
-                    </template>
-                  </ZmTableColumn>
-
-                  <ZmTableColumn v-if="showDepth" label="结束井深(m)" min-width="80">
-                    <template #default="{ row, $index }">
-                      <el-form-item
-                        v-if="$index >= 0"
-                        :prop="`reportDetails.${$index}.currentDepth`"
-                        :rules="[
-                          {
-                            required: true,
-                            message: '请输入结束井深',
-                            trigger: ['blur']
-                          }
-                          // { validator: validateLastCurrentDepth, trigger: ['change', 'blur'] }
-                        ]"
-                        class="mb-0!">
-                        <el-input-number
-                          v-model="row.currentDepth"
-                          :min="0"
-                          :controls="false"
-                          class="!w-full"
-                          align="left"
-                          placeholder="请输入结束井深"
-                          @input="() => inputCurrentDepth()"
-                          :disabled="fillStatus === '1'">
-                          <template #suffix> m </template>
-                        </el-input-number>
-                      </el-form-item>
-                    </template>
-                  </ZmTableColumn>
-
-                  <ZmTableColumn label="详细描述" min-width="200" align="center">
-                    <template #default="{ row, $index }">
-                      <el-form-item
-                        v-if="$index >= 0"
-                        :prop="`reportDetails.${$index}.constructionDetail`"
-                        :rules="{ required: true, message: '请输入详细描述', trigger: 'blur' }"
-                        class="mb-0!">
-                        <el-input
-                          v-model="row.constructionDetail"
-                          type="textarea"
-                          :autosize="{ minRows: 1 }"
-                          show-word-limit
-                          :maxlength="1000"
-                          placeholder="请输入详细描述"
-                          :disabled="fillStatus === '1'" />
-                      </el-form-item>
-                    </template>
-                  </ZmTableColumn>
-
-                  <ZmTableColumn label="操作" width="80" fixed="right" align="center">
-                    <template #default="{ $index }">
-                      <el-button
-                        link
-                        type="danger"
-                        :icon="Delete"
-                        @click="removeProductionStatusRow($index)"
-                        :disabled="fillStatus === '1'">
-                        删除
-                      </el-button>
-                    </template>
-                  </ZmTableColumn>
-                </ZmTable>
-              </el-form-item>
-            </div>
-
-            <el-form-item>
-              <el-button
-                :loading="submitLoading"
-                type="primary"
-                @click="getFillInfo"
-                v-show="showStatus"
-                >{{ t('operationFillForm.confirm') }}</el-button
-              >
-              <el-button type="info" @click="deleteFillInfo" v-show="showStatus">{{
-                t('operationFill.clear')
-              }}</el-button>
-            </el-form-item>
-          </el-form>
-        </div>
-      </el-tab-pane>
-    </el-tabs>
-  </ContentWrap>
-</template>
-
-<script setup lang="ts">
-import { IotOpeationFillApi, IotOpeationFillVO } from '@/api/pms/iotopeationfill'
-import { ElMessage, FormInstance, FormRules } from 'element-plus'
-import moment from 'moment'
-import { getIntDictOptions, getStrDictOptions } from '@/utils/dict'
-import { useRoute } from 'vue-router'
+<script lang="ts" setup>
+import { IotOpeationFillApi } from '@/api/pms/iotopeationfill'
 import { calculateDuration, formatT } from '@/utils/formatTime'
-import { Delete, Plus } from '@element-plus/icons-vue'
-import { useDebounceFn } from '@vueuse/core'
+import { getDictOptions } from '@/utils/dict'
+import { ArrowRight, Delete, Monitor, Plus, Warning } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import type { FormInstance, FormItemRule } from 'element-plus'
 import dayjs from 'dayjs'
+import { useRoute } from 'vue-router'
 
-/** 运行记录填报 列表 */
 defineOptions({ name: 'FillOrderInfo' })
 
-const route = useRoute()
-
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const loading = ref(true) // 列表的加载中
-const { params } = useRoute() // 查询参数
-const deptId = params.id as string
-const list = ref<IotOpeationFillVO[]>([]) // 列表的数据
-const attrList = ref<IotOpeationFillVO[]>([]) // 非累计属性集合
-const attrList1 = ref<IotOpeationFillVO[]>([]) // 累计属性集合
-const attrList2 = ref<IotOpeationFillVO[]>([]) // 属性集合
-let companyName = ref('')
-
-let fillStatus = deptId.split(',')[4]
-let createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
-let showStatus = true
-const queryParams = reactive<any>({
-  pageNo: 1,
-  pageSize: 10,
-  deviceCode: undefined,
-  deviceName: undefined,
-  fillContent: undefined,
-  deviceType: undefined,
-  deviceComponent: undefined,
-  deptId: undefined,
-  orgName: undefined,
-  proId: undefined,
-  proName: undefined,
-  teamId: undefined,
-  teamName: undefined,
-  dutyName: undefined,
-  creDate: [],
-  createTime: [],
-  deviceCategoryId: 1,
-  deviceId: undefined,
-  threshold: undefined,
-  defaultValue: undefined,
-  isSum: undefined
-})
-
-interface ReportDetail {
-  reportDate: number
-  startTime: string
-  endTime: string
-  duration: number
-  currentDepth: number
-  currentOperation: string
-  constructionDetail: string
+interface OperationRouteInfo {
+  rawId: string
+  deptId: number
+  userId: number
+  createTime: number
+  orderId: number
+  orderStatus: string
 }
 
-const taskId = ref<number | undefined>(undefined)
-const taskOptions = ref<{ label: string; value: number }[]>([])
-
-const reportDetails = ref<ReportDetail[]>([])
-
-const addProductionStatusRow = () => {
-  if (!reportDetails.value) {
-    reportDetails.value = []
-  }
-  reportDetails.value.push({
-    reportDate: createTime ? dayjs(createTime).valueOf() : dayjs().valueOf(),
-    startTime: '',
-    endTime: '',
-    duration: 0,
-    currentDepth: 0,
-    currentOperation: '',
-    constructionDetail: ''
-  })
+interface DeviceFillInfo {
+  deptId: number
+  userId: number
+  createTime: string | number
+  orderId: number
+  deviceCategoryId: number
+  deviceId: number
+  deviceCode: string
+  deviceName: string
+  isFill: string
+  orgName: string
+  wellName: string
+  wellNamePair: WellNameOption[] | null
+  noGasTasks: number[]
 }
 
-const removeProductionStatusRow = (index: number) => {
-  if (index === 0) {
-    message.warning('至少填写一条生产动态')
-    return
-  }
-  reportDetails.value.splice(index, 1)
+interface WellNameOption {
+  label: string
+  value: number
 }
 
-const acalculateDuration = (row: any) => {
-  calculateDuration(row)
-  inputCurrentDepth()
+interface FillAttribute {
+  defaultValue: string | number | null
+  name: string
+  fillContent: string | number | null
+  type: string
+  description: string
+  totalRunTime?: string | number | null
+  threshold?: string | number | null
+  isCollection?: string | number | null
 }
 
-const inputCurrentDepth = useDebounceFn(function inputCurrentDepth() {
-  const details = reportDetails.value
-
-  if (Array.isArray(details) && details.length > 0) {
-    const latestDetail = details.reduce((prev, current) => {
-      const currentFullTime = dayjs(current.reportDate)
-        .hour(parseInt(current.endTime.split(':')[0]))
-        .minute(parseInt(current.endTime.split(':')[1]))
-        .valueOf()
-
-      const prevFullTime = dayjs(prev.reportDate)
-        .hour(parseInt(prev.endTime.split(':')[0]))
-        .minute(parseInt(prev.endTime.split(':')[1]))
-        .valueOf()
-
-      return currentFullTime >= prevFullTime ? current : prev
-    })
+interface DeviceAttrsResponse {
+  nonSumList: FillAttribute[]
+  sumList: FillAttribute[]
+}
 
-    const currentDepth = attrList.value.find((item) => item.description === 'currentDepth')
-    if (currentDepth) currentDepth.fillContent = latestDetail.currentDepth
-  }
-}, 300)
+interface ReportDetail {
+  reportDate: string | number | null
+  startTime: string
+  endTime: string
+  duration: number
+  currentDepth: number | null
+  currentOperation: string
+  constructionDetail: string
+}
 
-// const inputCurrentDepth = useDebounceFn(function inputCurrentDepth(val: any, index: number) {
-//   if (reportDetails.value && index === reportDetails.value.length - 1) {
-//     const currentDepth = attrList.value.find((item) => item.description === 'currentDepth')
-//     if (currentDepth) currentDepth.fillContent = val
-//   }
-// }, 300)
+const route = useRoute()
 
-const showDepth = computed(() => {
-  return attrList.value.some((item) => item.description === 'currentDepth')
+/** 路由参数格式:deptId,userId,createTime,orderId,orderStatus */
+const routeInfo = reactive<OperationRouteInfo>({
+  rawId: '',
+  deptId: 0,
+  userId: 0,
+  createTime: 0,
+  orderId: 0,
+  orderStatus: ''
 })
 
-const NON_KEYS = [
+const deviceFillList = ref<DeviceFillInfo[]>([])
+const selectedDeviceIndex = ref(-1)
+const companyName = ref('')
+const taskId = ref<number>()
+const nonSumList = ref<FillAttribute[]>([])
+const sumList = ref<FillAttribute[]>([])
+const reportDetails = ref<ReportDetail[]>([])
+const reportDetailsLoading = ref(false)
+const deviceAttrsLoading = ref(false)
+const fillFormRef = ref<FormInstance>()
+const fillFormModel = computed(() => ({
+  nonSumList: nonSumList.value,
+  reportDetails: reportDetails.value
+}))
+const fillStatus = computed(() => routeInfo.orderStatus)
+const isFormDisabled = computed(() => fillStatus.value === '1')
+
+const keys = [
   'repairTime',
   'selfStopTime',
   'accidentTime',
@@ -581,10 +107,16 @@ const NON_KEYS = [
   'partyaResource',
   'relocationTime',
   'winterBreakTime',
-  'otherNptTime'
+  'otherNptTime',
+  'drillingWorkingTime',
+  'otherProductionTime',
+  'ratedProductionTime',
+  'productionTime',
+  'dailyInjectGasTime',
+  'otherNptReason'
 ]
 
-const keys = [
+const NON_KEYS = [
   'repairTime',
   'selfStopTime',
   'accidentTime',
@@ -596,706 +128,1706 @@ const keys = [
   'partyaResource',
   'relocationTime',
   'winterBreakTime',
-  'otherNptTime',
+  'otherNptTime'
+]
+
+const MAX_24_KEYS = [
+  ...NON_KEYS,
   'drillingWorkingTime',
   'otherProductionTime',
   'ratedProductionTime',
   'productionTime',
-  'dailyInjectGasTime',
-  'otherNptReason'
+  'dailyInjectGasTime'
 ]
 
-const sumNonProdTimes = () => {
-  let sum = 0
-  NON_KEYS.forEach((field) => {
-    sum += attrList.value.find((item) => item.description === field)?.fillContent || 0
-  })
-  return sum
-}
+const REQUIRED_KEYS = ['daySupervisors', 'nightSupervisors']
 
-const rhValidateTotalTime =
-  (isNon: boolean = false) =>
-  (_rule: any, _value: any, callback: any) => {
-    const gasTime =
-      attrList.value.find((item) => item.description === 'dailyInjectGasTime')?.fillContent || 0
-    const nonProdSum = sumNonProdTimes()
+const selectedDevice = computed(() => deviceFillList.value[selectedDeviceIndex.value])
+const isDailyReport = computed(() => selectedDevice.value?.deviceName === '生产日报')
+const showProductionDetails = computed(() => companyName.value === 'ry' && isDailyReport.value)
+const showFormActions = computed(() => {
+  return !['1', '3'].includes(fillStatus.value) && selectedDevice.value?.isFill !== '1'
+})
+const showDepth = computed(() =>
+  nonSumList.value.some((attribute) => attribute.description === 'currentDepth')
+)
+const showWellSelector = computed(() => {
+  return Boolean(
+    isDailyReport.value &&
+      ['rh', 'ry'].includes(companyName.value) &&
+      selectedDevice.value?.wellNamePair?.length
+  )
+})
 
-    let total = 0
-    let msg = ''
+const toNumber = (value: string) => {
+  const numberValue = Number(value)
+  return Number.isFinite(numberValue) ? numberValue : 0
+}
 
-    total = parseFloat((gasTime + nonProdSum).toFixed(2))
-    msg = `运转(${gasTime})+非生产(${nonProdSum})=${total}H,必须为24H`
+const formatCreateTime = (value: string | number) => {
+  const stringValue = String(value)
 
-    if (Math.abs(total - 24) > 0.01) {
-      if (!isNon) callback(new Error(msg))
-      else callback(new Error())
-    } else {
-      callback()
-    }
+  if (/^\d+$/.test(stringValue)) {
+    return dayjs.unix(Number(stringValue.slice(0, 10))).format('YYYY-MM-DD')
   }
 
-// const ryXjValidateTotalTime =
-//   (isNon: boolean = false) =>
-//   (_rule: any, _value: any, callback: any) => {
-//     const rateTime =
-//       attrList.value.find((item) => item.description === 'ratedProductionTime')?.fillContent || 0
-//     const time =
-//       attrList.value.find((item) => item.description === 'productionTime')?.fillContent || 0
-
-//     const nonProdSum = sumNonProdTimes()
-
-//     let total = 0
-//     let msg = ''
-
-//     total = parseFloat((time + nonProdSum).toFixed(2))
-//     msg = `生产(${time})+非生产(${nonProdSum})=${total}H,必须等于额定${rateTime}H`
-
-//     if (Math.abs(total - rateTime) > 0.01) {
-//       if (!isNon) callback(new Error(msg))
-//       else callback(new Error())
-//     } else {
-//       callback()
-//     }
-//   }
-
-const ryValidateTotalTime =
-  (isNon: boolean = false) =>
-  (_rule: any, _value: any, callback: any) => {
-    const drillingTime =
-      attrList.value.find((item) => item.description === 'drillingWorkingTime')?.fillContent || 0
-    const otherTime =
-      attrList.value.find((item) => item.description === 'otherProductionTime')?.fillContent || 0
-
-    const nonProdSum = sumNonProdTimes()
-
-    let total = 0
-    let msg = ''
-
-    total = parseFloat((drillingTime + otherTime + nonProdSum).toFixed(2))
-    msg = `进尺(${drillingTime})+其他(${otherTime})+非生产(${nonProdSum})=${total}H,必须为24H`
-
-    if (Math.abs(total - 24) > 0.01) {
-      if (!isNon) callback(new Error(msg))
-      else callback(new Error())
-    } else {
-      callback()
-    }
-  }
+  return dayjs(value).format('YYYY-MM-DD')
+}
+
+const getDevicePrimaryText = (device: DeviceFillInfo) => {
+  return device.deviceCode || device.deviceName || '未命名填报项'
+}
 
-const validateOtherReason = (_rule: any, value: any, callback: any) => {
-  const time = attrList.value.find((item) => item.description === 'otherNptTime')?.fillContent || 0
-  if (time > 0 && !value) {
-    callback(new Error('填写了其他时间,必须说明原因'))
-  } else {
-    callback()
+const getDeviceSecondaryText = (device: DeviceFillInfo) => {
+  if (device.deviceCode) {
+    return device.deviceName || '未命名设备'
   }
+
+  return device.deviceName === '生产日报' ? '填写当日生产记录' : '独立填报项'
 }
 
-const rules = reactive<FormRules>({
-  dailyInjectGasTime: [
-    { required: true, message: '请输入当日运转时间' },
-    { validator: rhValidateTotalTime() }
-  ],
-  drillingWorkingTime: [
-    { required: true, message: '请输入进尺工作时间' },
-    { validator: ryValidateTotalTime() }
-  ],
-  nightSupervisors: { required: true, message: '请输入夜班跟班干部' },
-  daySupervisors: { required: true, message: '请输入白班跟班干部' },
-  dailyGasInjection: [
-    {
-      validator: (_rule, value, callback) => {
-        if (companyName.value !== 'rh' || !taskId.value || noGasTasks.value.includes(taskId.value))
-          return callback()
-
-        const dailyInjectGasTime =
-          attrList.value.find((item) => item.description === 'dailyInjectGasTime')?.fillContent || 0
-
-        // if (dailyInjectGasTime > 0 && !(value > 0)) {
-        //   return callback(new Error('当日运转时间大于0,注气量也需要大于0'))
-        // }
-
-        const hasDailyGasInjection = value !== undefined && value !== null && value !== ''
-
-        if (dailyInjectGasTime > 0 && !hasDailyGasInjection) {
-          return callback(new Error('当日运转时间大于0,需要填写当日注气量'))
-        }
-
-        callback()
-      },
-      trigger: ['blur', 'change']
-    }
-  ],
-  otherProductionTime: [{ validator: ryValidateTotalTime() }],
-  // ratedProductionTime: [
-  //   { required: true, message: '请输入额定生产时间', trigger: 'blur' },
-  //   { validator: ryXjValidateTotalTime(), trigger: 'blur' }
-  // ],
-  // productionTime: [
-  //   { required: true, message: '请输入生产时间', trigger: 'blur' },
-  //   { validator: ryXjValidateTotalTime(), trigger: 'blur' }
-  // ],
-  otherNptReason: [{ validator: validateOtherReason }]
-})
+const hasValue = (value: string | number | null | undefined) => {
+  return value !== null && value !== undefined && value !== ''
+}
 
-const totalValidatorComputed = computed(() => {
-  if (attrList.value.some((item) => item.description === 'dailyInjectGasTime')) {
-    return rhValidateTotalTime
-  }
-  //  else if (attrList.value.some((item) => item.description === 'ratedProductionTime')) {
-  //   return ryXjValidateTotalTime
-  // }
-  else if (attrList.value.some((item) => item.description === 'drillingWorkingTime')) {
-    return ryValidateTotalTime
-  }
-})
+const getSumDisplayValue = (attribute: FillAttribute) => {
+  return hasValue(attribute.totalRunTime) ? attribute.totalRunTime : '暂无数据'
+}
 
-nextTick(() => {
-  const validator = totalValidatorComputed.value
-  if (!validator) return
-  NON_KEYS.forEach((field) => {
-    rules[field] = [{ validator: validator(true) }]
-  })
-})
+const normalizeAttributeName = (name: string) => {
+  return name.replace(/填报/g, '').replace(/\s+/g, '').toUpperCase()
+}
 
-let cxStatus = true
+const getRdAttributeLimit = (attribute: FillAttribute) => {
+  if (companyName.value !== 'rd' || attribute.type !== 'double') return undefined
 
-// 计算数字输入的最大长度(根据阈值动态计算)
-const calculateMaxLength = (item: any) => {
-  if (item.type !== 'double' || !item.threshold) return undefined
+  const normalizedName = normalizeAttributeName(attribute.name)
+  const matchedSumAttribute = sumList.value.find(
+    (sumAttribute) => normalizeAttributeName(sumAttribute.name) === normalizedName
+  )
 
-  const max = parseFloat(item.threshold)
-  if (isNaN(max)) return undefined
+  if (!matchedSumAttribute || !hasValue(matchedSumAttribute.totalRunTime)) return undefined
 
-  // 整数部分长度 + 可能的小数点 + 两位小数
-  return max.toString().length + (max.toString().includes('.') ? 0 : 3)
-}
+  const totalRunTime = Number(matchedSumAttribute.totalRunTime)
+  if (!Number.isFinite(totalRunTime)) return undefined
 
-// 简单的节流函数,避免提示信息过于频繁
-const throttle = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
-  let lastTime = 0
+  const increment = normalizedName.endsWith('KM')
+    ? 3000
+    : normalizedName.endsWith('H')
+      ? 100
+      : undefined
 
-  return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
-    const now = Date.now()
-    if (now - lastTime >= delay) {
-      fn.apply(this, args)
-      lastTime = now
-    }
+  if (increment === undefined) return undefined
+
+  return {
+    totalRunTime,
+    increment,
+    max: totalRunTime + increment
   }
 }
 
-const showComponent = () => {
-  if (JSON.parse(fillStatus) === 1 || JSON.parse(fillStatus) === 3) {
-    showStatus = false
+const getRdLimitExceeded = (attribute: FillAttribute) => {
+  const limit = getRdAttributeLimit(attribute)
+  if (!limit || !hasValue(attribute.fillContent)) return undefined
+
+  const value = Number(attribute.fillContent)
+  if (!Number.isFinite(value) || value <= limit.max) return undefined
+
+  return {
+    attribute,
+    value,
+    ...limit
   }
 }
 
-const noGasTasks = ref<number[]>([])
+const getRdLimitWarning = (attribute: FillAttribute) => {
+  const exceeded = getRdLimitExceeded(attribute)
+  if (!exceeded) return ''
 
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    queryParams.deptId = deptId.split(',')[0]
-    queryParams.userId = deptId.split(',')[1]
-    queryParams.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
-    queryParams.orderId = deptId.split(',')[3]
-    const data = await IotOpeationFillApi.getIotOpeationFillPage(queryParams)
-    list.value = data
-    if (cxStatus) {
-      queryParams.deviceCategoryId = list.value[0].deviceCategoryId
-      queryParams.deptId = list.value[0].deptId
-      queryParams.deviceCode = list.value[0].deviceCode
-      queryParams.deviceName = list.value[0].deviceName
-      queryParams.deviceId = list.value[0].deviceId
-    }
-    await getAttrList()
+  return `超过建议上限:${exceeded.max}(累计值${exceeded.totalRunTime} + ${exceeded.increment})`
+}
 
-    const daily = list.value.find((item) => item.deviceName === '生产日报') ?? {
-      wellNamePair: {},
-      noGasTasks: []
-    }
+const getExceededRdAttributes = () => {
+  return nonSumList.value
+    .map((attribute) => getRdLimitExceeded(attribute))
+    .filter((item): item is NonNullable<typeof item> => Boolean(item))
+}
 
-    taskOptions.value = Object.keys(daily.wellNamePair ?? {}).map((key) => ({
-      label: daily.wellNamePair[key],
-      value: Number(key)
-    }))
+type RdExceededAttribute = ReturnType<typeof getExceededRdAttributes>[number]
+
+const rdConfirmVisible = ref(false)
+const rdExceededAttributes = ref<RdExceededAttribute[]>([])
 
-    noGasTasks.value = (daily.noGasTasks ?? []).map(Number)
+const getAttributeMax = (attribute: FillAttribute) => {
+  if (getRdAttributeLimit(attribute)) return undefined
+  if (MAX_24_KEYS.includes(attribute.description)) return 24
+  if (!hasValue(attribute.threshold)) return undefined
 
-    taskId.value = taskOptions.value[0]?.value
+  const max = Number(attribute.threshold)
+  return Number.isFinite(max) ? max : undefined
+}
 
-    IotOpeationFillApi.getReportDetails(deptId.split(',')[3]).then((res) => {
-      reportDetails.value = (res ? (res as any[]) : []).map((item) => ({
-        reportDate: item.reportDate ?? dayjs(createTime).valueOf(),
-        startTime: formatT(item.startTime),
-        endTime: formatT(item.endTime),
-        duration: item.duration,
-        currentDepth: item.currentDepth,
-        currentOperation: item.currentOperation,
-        constructionDetail: item.constructionDetail
-      }))
+const getNumericValue = (value: string | number | null) => {
+  if (!hasValue(value)) return null
 
-      if (!reportDetails.value.length) {
-        addProductionStatusRow()
-      }
-    })
-  } finally {
-    loading.value = false
-  }
+  const numberValue = Number(value)
+  return Number.isFinite(numberValue) ? numberValue : null
+}
+
+const getDictOptionValue = (value: unknown) => {
+  return typeof value === 'number' ? value : String(value)
+}
+
+const getFillNumber = (description: string) => {
+  const value = nonSumList.value.find(
+    (attribute) => attribute.description === description
+  )?.fillContent
+
+  if (!hasValue(value)) return 0
+
+  const numberValue = Number(value)
+  return Number.isFinite(numberValue) ? numberValue : 0
+}
+
+const getNonProductionTotal = () => {
+  return NON_KEYS.reduce((total, description) => total + getFillNumber(description), 0)
 }
-function formatTimestamp(timestamp) {
-  // const date = new Date(timestamp * 1000)
-  return moment.unix(timestamp).format('YYYY-MM-DD')
-}
-
-const open = async (_type: string, id?: number) => {
-  alert(id)
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-let devName = ''
-const openFill = (
-  deviceCategoryId?: number,
-  deviceId?: number,
-  deptId?: number,
-  deviceName?: string,
-  deviceCode?: string
-) => {
-  queryParams.deviceCategoryId = deviceCategoryId
-  queryParams.deptId = deptId
-  queryParams.deviceCode = deviceCode
-  queryParams.deviceName = deviceName
-  if (queryParams.deviceName == '生产日报') {
-    devName = '生产日报'
+
+const validateRhTotalTime: FormItemRule['validator'] = (_rule, _value, callback) => {
+  const dailyInjectGasTime = getFillNumber('dailyInjectGasTime')
+  const nonProductionTime = getNonProductionTotal()
+  const total = Number((dailyInjectGasTime + nonProductionTime).toFixed(2))
+
+  if (Math.abs(total - 24) > 0.01) {
+    callback(
+      new Error(`运转(${dailyInjectGasTime}) + 非生产(${nonProductionTime}) = ${total}H,必须为24H`)
+    )
+    return
   }
-  queryParams.deviceId = deviceId
-  getAttrList()
-}
-// 处理输入事件,实时限制输入格式和最大值
-const handleInput = (item: any) => {
-  if (item.type === 'double') {
-    // 保存原始值用于后续比较
-    const originalValue = item.fillContent
-
-    // 1. 格式验证:只允许数字和小数点
-    item.fillContent = item.fillContent.replace(/[^\d.]/g, '')
-    // 确保只有一个小数点
-    item.fillContent = item.fillContent.replace(/\.{2,}/g, '.')
-    // 确保小数点不在开头
-    item.fillContent = item.fillContent.replace(/^\./g, '')
-    // 限制小数位数为两位
-    item.fillContent = item.fillContent.replace(/(\d+)\.(\d{2}).*/, '$1.$2')
-
-    // 2. 最大值验证
-    if (item.threshold) {
-      const value = parseFloat(item.fillContent)
-      const max = parseFloat(item.threshold)
-
-      if (!isNaN(value) && !isNaN(max) && value > max) {
-        // 输入值超过阈值时,恢复到修改前的值
-        item.fillContent = originalValue
-          .replace(/[^\d.]/g, '')
-          .replace(/\.{2,}/g, '.')
-          .replace(/^\./g, '')
-          .replace(/(\d+)\.(\d{2}).*/, '$1.$2')
-
-        if (parseFloat(item.fillContent) > max) {
-          item.fillContent = max.toString()
-        }
-
-        throttle(() => {
-          ElMessage.warning(t('operationFillForm.exceedMax', { max }))
-        }, 1000)()
-      }
-    }
 
-    if (companyName.value === 'rd') {
-      // 3. 累计值限制验证(改为弹窗提示但允许继续)
-      if (item.maxAllowedValue !== undefined) {
-        const value = parseFloat(item.fillContent)
-        if (!isNaN(value) && value > item.maxAllowedValue) {
-          // 不自动修改值,而是显示警告弹窗
-          let limitDescription = ''
-          if (item.limitType === 'km') {
-            limitDescription = `当前累计值${item.currentSumValue} + 3000`
-          } else if (item.limitType === 'time') {
-            limitDescription = `当前累计值${item.currentSumValue} + 100`
-          }
-
-          ElMessage.warning(
-            `填报值 ${value} 超过限制 ${item.maxAllowedValue} (${limitDescription}),请确认是否正确!`
-          )
-        }
-      }
-    }
+  callback()
+}
+
+const validateRyTotalTime: FormItemRule['validator'] = (_rule, _value, callback) => {
+  const drillingWorkingTime = getFillNumber('drillingWorkingTime')
+  const otherProductionTime = getFillNumber('otherProductionTime')
+  const nonProductionTime = getNonProductionTotal()
+  const total = Number((drillingWorkingTime + otherProductionTime + nonProductionTime).toFixed(2))
+
+  if (Math.abs(total - 24) > 0.01) {
+    callback(
+      new Error(
+        `进尺(${drillingWorkingTime}) + 其他(${otherProductionTime}) + 非生产(${nonProductionTime}) = ${total}H,必须为24H`
+      )
+    )
+    return
   }
+
+  callback()
 }
 
-const getAttrList = async () => {
-  loading.value = true
-  try {
-    queryParams.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
-    const data = await IotOpeationFillApi.getAttrs(queryParams)
+const validateOtherNptReason: FormItemRule['validator'] = (_rule, value, callback) => {
+  if (getFillNumber('otherNptTime') > 0 && !hasValue(value)) {
+    callback(new Error('填写了其他非生产时间,必须填写其他非生产原因'))
+    return
+  }
 
-    const timeKeys = keys.filter((k) => k !== 'otherNptReason')
+  callback()
+}
 
-    attrList.value = data[0].nonSumList.map((item) => {
-      if (timeKeys.includes(item.description)) {
-        item.fillContent = item.fillContent ?? 0
-      }
-      return item
-    })
-    attrList1.value = data[0].sumList
-
-    // 建立累计数据映射,用于后续验证
-    const sumMap = new Map()
-    attrList1.value.forEach((item) => {
-      // 创建匹配规则:移除"填报"字样的差异,保留核心名称
-      const coreName = item.name.replace(/填报/g, '')
-      sumMap.set(coreName, item)
-    })
+const isDailyGasInjectionRequired = () => {
+  const currentTaskId = taskId.value
 
-    // 为非累计数据添加最大值限制
-    attrList.value.forEach(function (item) {
-      let strVal = String(item.fillContent || '').trim()
-
-      if (strVal !== '') {
-        const num = Number(strVal)
-
-        if (!isNaN(num)) {
-          if (strVal.includes('.')) {
-            item.fillContent = Number(num.toFixed(2))
-          } else {
-            item.fillContent = num
-          }
-        }
-      }
-
-      // if (item.fillContent !== '' && item.fillContent !== null) {
-      //   const num = Number(item.fillContent)
-      //   if (!isNaN(num)) {
-      //     if (item.fillContent.includes('.')) {
-      //       item.fillContent = Number(num.toFixed(2))
-      //     } else {
-      //       item.fillContent = Math.floor(num)
-      //     }
-      //   }
-      // }
-
-      if (companyName.value === 'rd') {
-        // 添加最大值限制逻辑
-        const coreName = item.name.replace(/填报/g, '')
-        const sumItem = sumMap.get(coreName)
-        if (sumItem) {
-          // 根据字段名称判断使用哪种限制规则
-          if (item.name.includes('公里数填报')) {
-            // 公里数限制:当前累计值 + 3000
-            item.maxAllowedValue = sumItem.totalRunTime + 3000
-            item.currentSumValue = sumItem.totalRunTime // 保存当前累计值用于提示
-            item.limitType = 'km' // 标记为公里数限制
-          } else if (item.name.includes('运转时长填报')) {
-            // 运转时长限制:当前累计值 + 100
-            item.maxAllowedValue = sumItem.totalRunTime + 100
-            item.currentSumValue = sumItem.totalRunTime // 保存当前累计值用于提示
-            item.limitType = 'time' // 标记为时长限制
-          }
-        }
-      }
-
-      item.deviceCode = queryParams.deviceCode
-      item.deptId = queryParams.deptId
-      item.deviceId = queryParams.deviceId
-      item.deviceCategoryId = queryParams.deviceCategoryId
-      item.modelId = item.id
-      console.log(item.fillContent)
-    })
+  return Boolean(
+    companyName.value === 'rh' &&
+      hasValue(currentTaskId) &&
+      !selectedDevice.value?.noGasTasks.includes(Number(currentTaskId)) &&
+      getFillNumber('dailyInjectGasTime') > 0
+  )
+}
 
-    attrList1.value.forEach(function (item) {
-      item.deviceCode = queryParams.deviceCode
-      item.deptId = queryParams.deptId
-      item.deviceId = queryParams.deviceId
-      item.deviceCategoryId = queryParams.deviceCategoryId
-      item.modelId = item.id
-    })
-  } finally {
-    loading.value = false
+const validateDailyGasInjection: FormItemRule['validator'] = (_rule, value, callback) => {
+  if (isDailyGasInjectionRequired() && !hasValue(value)) {
+    callback(new Error('当日运转时间大于0,需要填写当日注气量'))
+    return
   }
+
+  callback()
 }
-const formRef = ref<FormInstance[] | null>(null)
 
-const submitLoading = ref(false)
+const isRhTotalTimeField = (description: string) => {
+  return description === 'dailyInjectGasTime' || NON_KEYS.includes(description)
+}
 
-/** 获取填写信息保存到后台*/
-const getFillInfo = async () => {
-  if (!formRef.value) return
+const isRyTotalTimeField = (description: string) => {
+  return (
+    ['drillingWorkingTime', 'otherProductionTime'].includes(description) ||
+    NON_KEYS.includes(description)
+  )
+}
 
-  submitLoading.value = true
+const isAttributeRequired = (attribute: FillAttribute) => {
+  return (
+    REQUIRED_KEYS.includes(attribute.description) ||
+    (!isDailyReport.value && Number(attribute.isCollection) !== 1)
+  )
+}
 
-  try {
-    const validations = formRef.value.map((form) => form.validate())
-    await Promise.all(validations)
-
-    const company = await IotOpeationFillApi.getOrgName(route.params.id.toString().split(',')[0])
-
-    if (devName != '生产日报') {
-      // 检查必填字段
-      const emptyFields = attrList.value.filter((item) => {
-        // 只检查非disabled的字段
-        return (
-          !(item.isCollection === 1 || fillStatus === '1') &&
-          (item.fillContent === undefined || item.fillContent === '')
-        )
-      })
+const getAttributeRules = (attribute: FillAttribute): FormItemRule[] => {
+  const rules: FormItemRule[] = []
+  const isSelect = attribute.type === 'enum'
 
-      if (emptyFields.length > 0) {
-        ElMessage.error(t('operationFillForm.fill'))
-        return
-      }
-    }
+  if (isAttributeRequired(attribute)) {
+    rules.push({
+      required: true,
+      message: `${isSelect ? '请选择' : '请输入'}${attribute.name}`,
+      trigger: isSelect ? 'change' : ['blur', 'change'],
+      type: attribute.type === 'double' ? 'number' : undefined
+    })
+  }
 
-    if (company === 'rd') {
-      // 检查是否有超出累计值限制的字段
-      const exceededFields = attrList.value.filter((item) => {
-        if (
-          item.type === 'double' &&
-          item.maxAllowedValue !== undefined &&
-          item.fillContent !== '' &&
-          item.fillContent !== null
-        ) {
-          const value = parseFloat(item.fillContent)
-          return !isNaN(value) && value > item.maxAllowedValue
-        }
-        return false
-      })
+  if (attribute.description === 'dailyGasInjection') {
+    rules.push({
+      required: isDailyGasInjectionRequired(),
+      validator: validateDailyGasInjection,
+      trigger: ['blur', 'change']
+    })
+  }
 
-      // 如果有超出限制的字段,提示用户确认
-      if (exceededFields.length > 0) {
-        let exceededMessage = ''
-        exceededFields.forEach((field) => {
-          let limitDescription = ''
-          if (field.limitType === 'km') {
-            limitDescription = `(${field.currentSumValue} + 3000)`
-          } else if (field.limitType === 'time') {
-            limitDescription = `(${field.currentSumValue} + 100)`
-          }
-          exceededMessage += `${field.name};\n`
-        })
-
-        // exceededMessage += '\n是否继续保存?'
-
-        const confirmResult = await message.confirm(
-          exceededMessage,
-          '以下填报项超出限制,是否继续保存?',
-          '继续保存',
-          '取消'
-        )
-        if (!confirmResult) {
-          return // 用户取消保存
-        }
-      }
+  if (keys.includes(attribute.description)) {
+    if (companyName.value === 'rh' && isRhTotalTimeField(attribute.description)) {
+      rules.push({
+        validator: validateRhTotalTime,
+        trigger: ['blur', 'change']
+      })
     }
 
-    attrList2.value = attrList.value.concat(attrList1.value)
-
-    attrList2.value.forEach(function (item) {
-      item.pointName = item.name
-      item.createTime = formatTimestamp(JSON.parse(deptId.split(',')[2].substring(0, 10)))
-      item.userId = deptId.split(',')[1]
-      item.id = deptId.split(',')[3]
-    })
-    const data = attrList2.value as unknown as IotOpeationFillVO
-
-    const reqData = {
-      createReqVO: data,
-      reportDetails: reportDetails.value.map((item) => ({
-        ...item,
-        taskId: taskId.value
-      }))
+    if (companyName.value === 'ry' && isRyTotalTimeField(attribute.description)) {
+      rules.push({
+        validator: validateRyTotalTime,
+        trigger: ['blur', 'change']
+      })
     }
 
-    await IotOpeationFillApi.insertLog(reqData)
-    message.success(t('common.createSuccess'))
-    // 发送操作成功的事件
-    emit('success')
-    cxStatus = false
-    getList()
-  } catch (error) {
-    console.error('保存失败:', error)
-  } finally {
-    submitLoading.value = false
+    if (attribute.description === 'otherNptReason') {
+      rules.push({
+        validator: validateOtherNptReason,
+        trigger: ['blur', 'change']
+      })
+    }
   }
-}
 
-/**清空填写信息*/
-const deleteFillInfo = () => {
-  attrList.value.forEach(function (item) {
-    item.fillContent = ''
-  })
+  return rules
 }
 
-/** 初始化 **/
-onMounted(async () => {
-  const company = await IotOpeationFillApi.getOrgName(route.params.id.toString().split(',')[0])
-  companyName.value = company
-
-  getList()
-  showComponent()
-})
-</script>
-<style scoped>
-.scrollable-form {
-  /* 设置最大高度,超过这个高度会出现滚动条 */
-  max-height: 100%; /* 根据你的需求调整 */
+const validateAttributeField = async (description: string) => {
+  const attributeIndex = nonSumList.value.findIndex(
+    (attribute) => attribute.description === description
+  )
 
-  /* 可选:添加内边距和边框美化 */
-  padding: 16px;
+  if (attributeIndex < 0) return
 
-  /* 超出部分显示垂直滚动条 */
-  overflow-y: auto;
-  border: 1px solid #e5e7eb;
-  border-radius: 4px;
+  await nextTick()
+  void fillFormRef.value
+    ?.validateField(`nonSumList.${attributeIndex}.fillContent`)
+    .catch(() => undefined)
 }
 
-/* 可选:美化滚动条 */
-.scrollable-form::-webkit-scrollbar {
-  width: 6px;
+const handleTaskIdChange = () => {
+  void validateAttributeField('dailyGasInjection')
 }
 
-.scrollable-form::-webkit-scrollbar-track {
-  background: #f1f1f1;
-  border-radius: 3px;
+const handleDoubleChange = (attribute: FillAttribute) => {
+  if (attribute.description === 'dailyInjectGasTime') {
+    void validateAttributeField('dailyGasInjection')
+  }
 }
 
-.scrollable-form::-webkit-scrollbar-thumb {
-  background: #c1c1c1;
-  border-radius: 3px;
-}
+const syncTaskId = (device?: DeviceFillInfo) => {
+  const firstWell = device?.wellNamePair?.[0]
+  const canSelectWell =
+    device?.deviceName === '生产日报' &&
+    ['rh', 'ry'].includes(companyName.value) &&
+    Boolean(firstWell)
 
-.scrollable-form::-webkit-scrollbar-thumb:hover {
-  background: #a8a8a8;
+  taskId.value = canSelectWell ? firstWell?.value : undefined
 }
 
-.back-red {
-  /* 红色背景 */
-  background-color: red;
+const getDefaultReportDate = (device?: DeviceFillInfo) => {
+  return dayjs(formatCreateTime(device?.createTime ?? routeInfo.createTime)).valueOf()
 }
 
-.back-blue {
-  background-color: grey;
-}
+const formatReportTime = (value: unknown) => {
+  if (Array.isArray(value) && value.length >= 2) {
+    return formatT([Number(value[0]), Number(value[1])])
+  }
 
-.step-container {
-  display: grid;
-  grid-template-columns: 220px 1fr;
-  gap: 10px;
-  height: 100%;
-  min-height: 600px;
-}
+  if (typeof value === 'string') {
+    const matchedTime = value.match(/^(\d{1,2}):(\d{2})/)
+    return matchedTime ? `${matchedTime[1].padStart(2, '0')}:${matchedTime[2]}` : ''
+  }
 
-.steps-nav {
-  padding-right: 15px;
-  overflow-y: auto;
+  return ''
 }
 
-.form-wrapper {
-  padding: 30px;
-  background: #fff;
-  border-radius: 8px;
-  box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
-}
+const createReportDetail = (device?: DeviceFillInfo): ReportDetail => ({
+  reportDate: getDefaultReportDate(device),
+  startTime: '',
+  endTime: '',
+  duration: 0,
+  currentDepth: 0,
+  currentOperation: '',
+  constructionDetail: ''
+})
 
-.navigation-controls {
-  margin-top: 40px;
-  text-align: center;
+const addProductionStatusRow = () => {
+  reportDetails.value.push(createReportDetail(selectedDevice.value))
 }
 
-.custom-label {
-  padding: 0 10px;
-  font-size: 17px;
-  font-weight: 1000;
-  color: forestgreen;
-}
+const removeProductionStatusRow = (index: number) => {
+  if (reportDetails.value.length <= 1) {
+    ElMessage.warning('至少填写一条生产动态')
+    return
+  }
 
-.custom-label1 {
-  padding: 0 10px;
-  font-size: 17px;
-  font-weight: 1000;
+  reportDetails.value.splice(index, 1)
+  syncCurrentDepth()
 }
 
-.has-border {
-  padding: 3px 8px; /* 适当增加内边距,避免文字太贴近边框 */
-  font-size: 20px;
-  border: 2px solid #333; /* 加粗到2px,使用深灰色#333让边框更清晰 */
-  border-radius: 2px; /* 轻微圆角,可选 */
+const getReportDetailTimestamp = (detail: ReportDetail) => {
+  if (!hasValue(detail.reportDate) || !detail.endTime) return Number.NEGATIVE_INFINITY
+
+  const dateValue = /^\d+$/.test(String(detail.reportDate))
+    ? Number(detail.reportDate)
+    : detail.reportDate
+  const [hour, minute] = detail.endTime.split(':').map(Number)
+
+  if (!Number.isFinite(hour) || !Number.isFinite(minute)) return Number.NEGATIVE_INFINITY
+
+  return dayjs(dateValue).hour(hour).minute(minute).second(0).millisecond(0).valueOf()
 }
 
-::v-deep .el-step__icon {
-  color: #fff;
-  background-color: #409eff;
-  border: 0;
+const syncCurrentDepth = () => {
+  const currentDepthAttribute = nonSumList.value.find(
+    (attribute) => attribute.description === 'currentDepth'
+  )
+
+  if (!currentDepthAttribute || !reportDetails.value.length) return
+
+  const latestDetail = reportDetails.value.reduce((latest, current) => {
+    return getReportDetailTimestamp(current) >= getReportDetailTimestamp(latest) ? current : latest
+  })
+
+  currentDepthAttribute.fillContent = latestDetail.currentDepth
 }
 
-.step-title-wrapper {
-  position: relative;
-  display: inline-flex;
-  padding-right: 25px;
-  align-items: center;
-  gap: 8px;
+const handleReportTimeChange = (detail: ReportDetail) => {
+  calculateDuration(detail)
+  syncCurrentDepth()
 }
 
-/* 覆盖步骤条默认样式 */
-:deep(.custom-steps) {
-  /* 调整头部位置 */
-  .el-step__head {
-    top: 3px;
-  }
+const loadReportDetails = async (device: DeviceFillInfo) => {
+  reportDetails.value = []
 
-  /* 标题容器定位 */
-  .el-step__title {
-    display: inline-block;
-    padding-right: 0;
-    margin-left: 10px;
-  }
+  if (companyName.value !== 'ry' || device.deviceName !== '生产日报') return
+
+  reportDetailsLoading.value = true
 
-  /* 步骤连接线 */
-  .el-step__line {
-    left: 11px;
-    background-color: #ebeef5;
+  try {
+    const response = await IotOpeationFillApi.getReportDetails(device.orderId)
+    const details = Array.isArray(response) ? response : []
+
+    reportDetails.value = details.map((item) => ({
+      reportDate: item.reportDate ?? getDefaultReportDate(device),
+      startTime: formatReportTime(item.startTime),
+      endTime: formatReportTime(item.endTime),
+      duration: Number(item.duration) || 0,
+      currentDepth: getNumericValue(item.currentDepth),
+      currentOperation: String(item.currentOperation ?? ''),
+      constructionDetail: String(item.constructionDetail ?? '')
+    }))
+
+    if (!reportDetails.value.length) {
+      reportDetails.value.push(createReportDetail(device))
+    }
+
+    syncCurrentDepth()
+    console.log('生产动态 reportDetails:', reportDetails.value)
+  } catch (error) {
+    reportDetails.value.push(createReportDetail(device))
+    console.error('获取生产动态失败:', error)
+  } finally {
+    reportDetailsLoading.value = false
   }
+}
 
-  /* 当前步骤样式 */
-  .is-process .title-text {
-    font-weight: 600;
-    color: #409eff;
+const handleConfirm = async () => {
+  if (!fillFormRef.value) return
+
+  try {
+    await fillFormRef.value.validate()
+  } catch (error) {
+    ElMessage.warning('请检查并完善表单内容')
+    console.warn('表单校验未通过:', error)
+    return
   }
 
-  /* 完成状态图标 */
-  .is-finish .tip-icon {
-    color: #67c23a;
+  const exceededAttributes = getExceededRdAttributes()
+
+  if (exceededAttributes.length) {
+    rdExceededAttributes.value = exceededAttributes
+    rdConfirmVisible.value = true
+    return
   }
+
+  previewSubmit()
 }
 
-.horizontal-container {
-  display: flex;
-  flex-wrap: wrap; /* 允许换行 */
-  gap: 20px; /* 项目间距 */
+const previewSubmit = () => {
+  console.log('表单校验及超限确认通过,预览数据:', {
+    nonSumList: nonSumList.value,
+    reportDetails: reportDetails.value,
+    taskId: taskId.value
+  })
+  ElMessage.success('校验已通过,当前为预览效果,未调用提交接口')
 }
 
-.form-item-container {
-  flex: 1; /* 等宽分布 */
-  min-width: 200px; /* 最小宽度防止挤压 */
+const confirmRdExceededSubmit = () => {
+  rdConfirmVisible.value = false
+  previewSubmit()
 }
 
-/* 新增日报填报项的边框样式 */
-.report-border {
-  padding: 2px 4px;
-  border: 2px solid #42b983; /* 使用Vue标志性的绿色边框 */
-  border-radius: 4px;
+const clearFillForm = async () => {
+  nonSumList.value.forEach((attribute) => {
+    attribute.fillContent = null
+  })
+
+  reportDetails.value = showProductionDetails.value
+    ? [createReportDetail(selectedDevice.value)]
+    : []
+
+  await nextTick()
+  fillFormRef.value?.clearValidate()
 }
 
-:deep(.table-form-item) {
-  .el-form-item__content {
-    margin-left: 0 !important;
+const getDeviceAttrs = async (device: DeviceFillInfo) => {
+  const deviceText = getDevicePrimaryText(device)
+
+  deviceAttrsLoading.value = true
+  nonSumList.value = []
+  sumList.value = []
+
+  try {
+    const attrsParams = {
+      deptId: device.deptId,
+      userId: device.userId,
+      createTime: formatCreateTime(device.createTime),
+      orderId: device.orderId,
+      deviceCategoryId: device.deviceCategoryId,
+      deviceId: device.deviceId,
+      deviceCode: device.deviceCode,
+      deviceName: device.deviceName
+    }
+
+    console.log(`${deviceText} 的属性请求参数:`, attrsParams)
+    const attrs = await IotOpeationFillApi.getAttrs(attrsParams)
+    const attrsData = (Array.isArray(attrs) ? attrs[0] : attrs) as DeviceAttrsResponse | undefined
+
+    nonSumList.value = (attrsData?.nonSumList ?? []).map((attribute) => ({
+      ...attribute,
+      fillContent:
+        attribute.type === 'double'
+          ? getNumericValue(attribute.fillContent)
+          : (attribute.fillContent ?? null)
+    }))
+    sumList.value = attrsData?.sumList ?? []
+    await loadReportDetails(device)
+
+    console.log(`${deviceText} 的属性和已有填报值:`, attrs)
+  } catch (error) {
+    console.error(`获取 ${deviceText} 的属性和已有填报值失败:`, error)
+  } finally {
+    deviceAttrsLoading.value = false
+  }
+}
+
+const getInitialData = async () => {
+  try {
+    const company = await IotOpeationFillApi.getOrgName(routeInfo.deptId)
+    companyName.value = String(company ?? '')
+    console.log('公司标识:', company)
+
+    const fillList = await IotOpeationFillApi.getIotOpeationFillPage({
+      pageNo: 1,
+      pageSize: 10,
+      deviceCategoryId: 1,
+      deptId: routeInfo.deptId,
+      userId: routeInfo.userId,
+      createTime: formatCreateTime(routeInfo.createTime),
+      orderId: routeInfo.orderId
+    })
+
+    console.log('object :>> ', fillList)
+
+    deviceFillList.value = (fillList ?? []).map((item) => ({
+      deptId: item.deptId,
+      userId: routeInfo.userId,
+      createTime: item.createTime,
+      orderId: item.orderId,
+      deviceCategoryId: item.deviceCategoryId,
+      deviceId: item.deviceId,
+      deviceCode: item.deviceCode,
+      deviceName: item.deviceName,
+      isFill: String(item.isFill ?? ''),
+      orgName: item.orgName ?? '',
+      wellName: item.wellName ?? '',
+      wellNamePair: item.wellNamePair
+        ? Object.entries(item.wellNamePair).map(([key, value]) => ({
+            label: String(value),
+            value: Number(key)
+          }))
+        : null,
+      noGasTasks: Array.isArray(item.noGasTasks) ? item.noGasTasks.map(Number) : []
+    }))
+
+    selectedDeviceIndex.value = deviceFillList.value.length > 0 ? 0 : -1
+
+    console.log('整理后的设备和填报列表:', deviceFillList.value)
+
+    const firstDevice = deviceFillList.value[0]
+    if (firstDevice) {
+      syncTaskId(firstDevice)
+      await getDeviceAttrs(firstDevice)
+    }
+  } catch (error) {
+    console.error('获取初始化数据失败:', error)
+  }
+}
+
+const selectDevice = async (index: number) => {
+  selectedDeviceIndex.value = index
+
+  const device = deviceFillList.value[index]
+  if (device) {
+    syncTaskId(device)
+    await getDeviceAttrs(device)
+  }
+}
+
+const storeRouteInfo = (id: string | string[] | undefined) => {
+  const rawId = Array.isArray(id) ? (id[0] ?? '') : (id ?? '')
+  const [deptId = '', userId = '', createTime = '', orderId = '', orderStatus = ''] =
+    rawId.split(',')
+
+  Object.assign(routeInfo, {
+    rawId,
+    deptId: toNumber(deptId),
+    userId: toNumber(userId),
+    createTime: toNumber(createTime),
+    orderId: toNumber(orderId),
+    orderStatus: orderStatus
+  })
+
+  console.log('运行记录填报路由信息:', { ...routeInfo })
+  getInitialData()
+}
+
+watch(() => route.params.id, storeRouteInfo, { immediate: true })
+</script>
+
+<template>
+  <ContentWrap class="fill-content-wrap">
+    <div class="fill-layout">
+      <aside class="device-panel">
+        <div class="device-panel__header">
+          <div class="device-panel__title">
+            <span class="device-panel__icon">
+              <el-icon><Monitor /></el-icon>
+            </span>
+            <div>
+              <h2>填报列表</h2>
+              <p>选择并填写运行记录</p>
+            </div>
+          </div>
+          <span class="device-count">{{ deviceFillList.length }} 项</span>
+        </div>
+
+        <el-scrollbar class="device-scrollbar">
+          <div v-if="deviceFillList.length" class="device-list">
+            <button
+              v-for="(device, index) in deviceFillList"
+              :key="`${device.deviceId ?? 'report'}-${device.deviceCode || device.deviceName}-${index}`"
+              type="button"
+              class="device-item"
+              :class="{
+                'is-active': selectedDeviceIndex === index,
+                'is-filled': device.isFill === '1'
+              }"
+              @click="selectDevice(index)">
+              <span class="device-item__marker"></span>
+              <span class="device-item__content">
+                <span class="device-item__code">{{ getDevicePrimaryText(device) }}</span>
+                <span class="device-item__name">{{ getDeviceSecondaryText(device) }}</span>
+              </span>
+              <span class="device-item__arrow">
+                <el-icon><ArrowRight /></el-icon>
+              </span>
+            </button>
+          </div>
+
+          <el-empty v-else description="暂无设备数据" :image-size="90" />
+        </el-scrollbar>
+      </aside>
+
+      <main class="form-panel">
+        <template v-if="selectedDevice">
+          <div class="form-panel__header">
+            <div class="form-panel__identity">
+              <div class="form-panel__eyebrow">
+                {{ selectedDevice.deviceCode ? '设备填报' : '日报填报' }}
+              </div>
+              <h1>{{ selectedDevice.deviceName || '未命名填报项' }}</h1>
+            </div>
+            <div class="basic-info__grid">
+              <div class="basic-info__item">
+                <span class="basic-info__label">创建时间</span>
+                <span class="basic-info__value">
+                  {{ formatCreateTime(selectedDevice.createTime) }}
+                </span>
+              </div>
+
+              <div class="basic-info__item">
+                <span class="basic-info__label">所属队伍</span>
+                <span class="basic-info__value">
+                  {{ selectedDevice.orgName || '暂无队伍信息' }}
+                </span>
+              </div>
+
+              <div v-if="isDailyReport" class="basic-info__item basic-info__item--well">
+                <span class="basic-info__label">井号</span>
+                <el-select
+                  v-if="showWellSelector"
+                  v-model="taskId"
+                  :disabled="isFormDisabled"
+                  size="default"
+                  placeholder="请选择井号"
+                  class="basic-info__select"
+                  @change="handleTaskIdChange">
+                  <el-option
+                    v-for="option in selectedDevice.wellNamePair ?? []"
+                    :key="option.value"
+                    :label="option.label"
+                    :value="option.value" />
+                </el-select>
+                <span v-else class="basic-info__value">
+                  {{ selectedDevice.wellName || '暂无井号信息' }}
+                </span>
+              </div>
+            </div>
+            <span v-if="selectedDevice.deviceCode" class="selected-device-code">
+              {{ selectedDevice.deviceCode }}
+            </span>
+          </div>
+
+          <div v-loading="deviceAttrsLoading" class="form-panel__body">
+            <section v-if="sumList.length" class="sum-section">
+              <h3 class="sum-section__title">累计信息</h3>
+              <div class="sum-list">
+                <div
+                  v-for="(attribute, index) in sumList"
+                  :key="attribute.description || `${attribute.name}-${index}`"
+                  class="sum-list__item">
+                  <span class="sum-list__label">{{ attribute.name }}</span>
+                  <span class="sum-list__value">{{ getSumDisplayValue(attribute) }}</span>
+                </div>
+              </div>
+            </section>
+
+            <section v-if="nonSumList.length" class="fill-section">
+              <h3 class="fill-section__title">填写信息</h3>
+              <el-form
+                ref="fillFormRef"
+                :model="fillFormModel"
+                :disabled="isFormDisabled"
+                label-position="top"
+                size="default"
+                scroll-to-error
+                class="fill-form">
+                <div class="fill-form__grid">
+                  <el-form-item
+                    v-for="(attribute, index) in nonSumList"
+                    :key="attribute.description || `${attribute.name}-${index}`"
+                    :prop="`nonSumList.${index}.fillContent`"
+                    :rules="getAttributeRules(attribute)"
+                    :class="{ 'fill-form__item--wide': attribute.type === 'textarea' }">
+                    <template #label>
+                      <span class="fill-form__label">
+                        {{ attribute.name }}
+                        <el-tooltip
+                          v-if="Number(attribute.isCollection) === 1"
+                          content="以下数值取自PLC,如有不符请修改"
+                          placement="top">
+                          <el-icon class="collection-warning-icon">
+                            <Warning />
+                          </el-icon>
+                        </el-tooltip>
+                      </span>
+                    </template>
+
+                    <el-input
+                      v-if="attribute.type === 'textarea'"
+                      v-model="attribute.fillContent"
+                      type="textarea"
+                      :autosize="{ minRows: 3, maxRows: 6 }"
+                      clearable
+                      placeholder="请输入内容" />
+
+                    <el-input-number
+                      v-else-if="attribute.type === 'double'"
+                      v-model="attribute.fillContent"
+                      :min="0"
+                      :max="getAttributeMax(attribute)"
+                      :precision="2"
+                      :step="0.01"
+                      :controls="false"
+                      class="fill-form__control"
+                      placeholder="请输入数字"
+                      @change="handleDoubleChange(attribute)" />
+
+                    <el-select
+                      v-else-if="attribute.type === 'enum'"
+                      v-model="attribute.fillContent"
+                      class="fill-form__control"
+                      clearable
+                      filterable
+                      placeholder="请选择">
+                      <el-option
+                        v-for="option in getDictOptions(attribute.description)"
+                        :key="String(option.value)"
+                        :label="option.label"
+                        :value="getDictOptionValue(option.value)" />
+                    </el-select>
+
+                    <el-input
+                      v-else
+                      v-model="attribute.fillContent"
+                      clearable
+                      placeholder="请输入内容" />
+
+                    <span v-if="getRdLimitWarning(attribute)" class="rd-limit-warning">
+                      {{ getRdLimitWarning(attribute) }}
+                    </span>
+                  </el-form-item>
+                </div>
+
+                <section v-if="showProductionDetails" class="production-section">
+                  <div class="production-section__header">
+                    <h3 class="production-section__title">生产动态</h3>
+                    <el-button
+                      type="primary"
+                      link
+                      :icon="Plus"
+                      :disabled="isFormDisabled"
+                      @click="addProductionStatusRow">
+                      添加一行
+                    </el-button>
+                  </div>
+
+                  <el-table
+                    v-loading="reportDetailsLoading"
+                    :data="reportDetails"
+                    border
+                    class="production-table">
+                    <el-table-column label="日期" width="170">
+                      <template #default="{ row, $index }">
+                        <el-form-item
+                          :prop="`reportDetails.${$index}.reportDate`"
+                          :rules="{
+                            required: true,
+                            message: '请选择日期',
+                            trigger: ['change', 'blur']
+                          }"
+                          class="production-table__form-item">
+                          <el-date-picker
+                            v-model="row.reportDate"
+                            type="date"
+                            value-format="x"
+                            placeholder="选择日期"
+                            clearable
+                            class="production-table__control"
+                            @change="syncCurrentDepth" />
+                        </el-form-item>
+                      </template>
+                    </el-table-column>
+
+                    <el-table-column label="开始时间" width="140">
+                      <template #default="{ row, $index }">
+                        <el-form-item
+                          :prop="`reportDetails.${$index}.startTime`"
+                          :rules="{
+                            required: true,
+                            message: '请选择开始时间',
+                            trigger: ['change', 'blur']
+                          }"
+                          class="production-table__form-item">
+                          <el-time-picker
+                            v-model="row.startTime"
+                            format="HH:mm"
+                            value-format="HH:mm"
+                            placeholder="开始时间"
+                            clearable
+                            class="production-table__control"
+                            @change="handleReportTimeChange(row)" />
+                        </el-form-item>
+                      </template>
+                    </el-table-column>
+
+                    <el-table-column label="结束时间" width="140">
+                      <template #default="{ row, $index }">
+                        <el-form-item
+                          :prop="`reportDetails.${$index}.endTime`"
+                          :rules="{
+                            required: true,
+                            message: '请选择结束时间',
+                            trigger: ['change', 'blur']
+                          }"
+                          class="production-table__form-item">
+                          <el-time-picker
+                            v-model="row.endTime"
+                            format="HH:mm"
+                            value-format="HH:mm"
+                            placeholder="结束时间"
+                            clearable
+                            class="production-table__control"
+                            @change="handleReportTimeChange(row)" />
+                        </el-form-item>
+                      </template>
+                    </el-table-column>
+
+                    <el-table-column label="时长(H)" width="90" align="center">
+                      <template #default="{ row }">
+                        <span class="production-table__duration">
+                          {{ Number(row.duration || 0).toFixed(2) }}
+                        </span>
+                      </template>
+                    </el-table-column>
+
+                    <el-table-column label="工况" min-width="180">
+                      <template #default="{ row, $index }">
+                        <el-form-item
+                          :prop="`reportDetails.${$index}.currentOperation`"
+                          :rules="{
+                            required: true,
+                            message: '请输入工况',
+                            trigger: ['change', 'blur']
+                          }"
+                          class="production-table__form-item">
+                          <el-input
+                            v-model="row.currentOperation"
+                            type="textarea"
+                            :autosize="{ minRows: 1, maxRows: 3 }"
+                            maxlength="1000"
+                            show-word-limit
+                            placeholder="请输入工况" />
+                        </el-form-item>
+                      </template>
+                    </el-table-column>
+
+                    <el-table-column v-if="showDepth" label="结束井深(m)" width="160">
+                      <template #default="{ row, $index }">
+                        <el-form-item
+                          :prop="`reportDetails.${$index}.currentDepth`"
+                          :rules="{
+                            required: true,
+                            message: '请输入结束井深',
+                            trigger: ['change', 'blur']
+                          }"
+                          class="production-table__form-item">
+                          <el-input-number
+                            v-model="row.currentDepth"
+                            :min="0"
+                            :precision="2"
+                            :controls="false"
+                            placeholder="结束井深"
+                            class="production-table__control"
+                            @change="syncCurrentDepth" />
+                        </el-form-item>
+                      </template>
+                    </el-table-column>
+
+                    <el-table-column label="详细描述" min-width="220">
+                      <template #default="{ row, $index }">
+                        <el-form-item
+                          :prop="`reportDetails.${$index}.constructionDetail`"
+                          :rules="{
+                            required: true,
+                            message: '请输入详细描述',
+                            trigger: ['change', 'blur']
+                          }"
+                          class="production-table__form-item">
+                          <el-input
+                            v-model="row.constructionDetail"
+                            type="textarea"
+                            :autosize="{ minRows: 1, maxRows: 3 }"
+                            maxlength="1000"
+                            show-word-limit
+                            placeholder="请输入详细描述" />
+                        </el-form-item>
+                      </template>
+                    </el-table-column>
+
+                    <el-table-column label="操作" width="80" fixed="right" align="center">
+                      <template #default="{ $index }">
+                        <el-button
+                          type="danger"
+                          link
+                          :icon="Delete"
+                          :disabled="isFormDisabled"
+                          @click="removeProductionStatusRow($index)">
+                          删除
+                        </el-button>
+                      </template>
+                    </el-table-column>
+                  </el-table>
+                </section>
+              </el-form>
+            </section>
+          </div>
+
+          <div v-if="showFormActions" class="form-actions">
+            <el-button size="default" @click="clearFillForm">清空</el-button>
+            <el-button size="default" type="primary" @click="handleConfirm">确定</el-button>
+          </div>
+        </template>
+
+        <el-empty v-else class="form-panel__empty" description="请先选择设备" />
+      </main>
+    </div>
+
+    <el-dialog
+      v-model="rdConfirmVisible"
+      title="确认提交超限数据"
+      width="540px"
+      append-to-body
+      destroy-on-close
+      class="rd-limit-dialog">
+      <p class="rd-confirm-description">以下数据超过建议上限,请确认是否继续提交。</p>
+
+      <div class="rd-confirm-list">
+        <div
+          v-for="item in rdExceededAttributes"
+          :key="item.attribute.description || item.attribute.name"
+          class="rd-confirm-item">
+          <div class="rd-confirm-item__header">
+            <span class="rd-confirm-item__name">{{ item.attribute.name }}</span>
+            <span class="rd-confirm-item__tag">超限</span>
+          </div>
+
+          <div class="rd-confirm-item__values">
+            <div class="rd-confirm-item__value">
+              <span>填写值</span>
+              <strong>{{ item.value }}</strong>
+            </div>
+            <div class="rd-confirm-item__value">
+              <span>建议上限</span>
+              <strong>{{ item.max }}</strong>
+            </div>
+          </div>
+
+          <div class="rd-confirm-item__formula">
+            计算方式:累计值 {{ item.totalRunTime }} + {{ item.increment }}
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <el-button size="default" @click="rdConfirmVisible = false">返回修改</el-button>
+        <el-button size="default" type="primary" @click="confirmRdExceededSubmit">
+          确认提交
+        </el-button>
+      </template>
+    </el-dialog>
+  </ContentWrap>
+</template>
+
+<style scoped>
+.fill-content-wrap {
+  --fill-primary: var(--el-color-primary);
+  --fill-primary-light: var(--el-color-primary-light-9);
+
+  height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
+  margin-bottom: 0 !important;
+  box-sizing: border-box;
+}
+
+.fill-content-wrap :deep(.el-card__body) {
+  height: 100%;
+  box-sizing: border-box;
+}
+
+.fill-layout {
+  display: grid;
+  height: 100%;
+  grid-template-columns: 286px minmax(0, 1fr);
+  min-height: 0;
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 16px;
+  box-shadow: 0 10px 32px rgb(31 45 61 / 8%);
+}
+
+.device-panel {
+  display: flex;
+  min-width: 0;
+  min-height: 0;
+  background: var(--el-fill-color-extra-light);
+  border-right: 1px solid var(--el-border-color-light);
+  flex-direction: column;
+}
+
+.device-panel__header {
+  display: flex;
+  height: 88px;
+  min-height: 88px;
+  padding: 16px 18px;
+  align-items: center;
+  justify-content: space-between;
+  border-bottom: 1px solid var(--el-border-color-light);
+  box-sizing: border-box;
+}
+
+.device-panel__title {
+  display: flex;
+  min-width: 0;
+  align-items: center;
+  gap: 10px;
+}
+
+.device-panel__icon {
+  display: inline-flex;
+  width: 38px;
+  height: 38px;
+  font-size: 18px;
+  color: var(--fill-primary);
+  background: var(--fill-primary-light);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 10px;
+  align-items: center;
+  justify-content: center;
+}
+
+.device-panel__title h2,
+.form-panel__header h1 {
+  padding: 0;
+  margin: 0;
+}
+
+.device-panel__title h2 {
+  font-size: 16px;
+  font-weight: 650;
+  color: var(--el-text-color-primary);
+}
+
+.device-panel__title p {
+  margin: 3px 0 0;
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+}
+
+.device-count {
+  min-width: 40px;
+  padding: 4px 9px;
+  font-size: 12px;
+  font-weight: 500;
+  color: var(--el-text-color-secondary);
+  text-align: center;
+  background: var(--el-fill-color);
+  border-radius: 999px;
+}
+
+.device-scrollbar {
+  min-height: 0;
+  padding: 12px;
+  flex: 1;
+}
+
+.device-list {
+  display: flex;
+  padding: 5px;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-extra-light);
+  border-radius: 12px;
+  box-shadow: 0 2px 8px rgb(31 45 61 / 4%);
+  flex-direction: column;
+  gap: 2px;
+}
+
+.device-item {
+  position: relative;
+  display: flex;
+  width: 100%;
+  padding: 12px 10px;
+  overflow: hidden;
+  font-family: inherit;
+  text-align: left;
+  cursor: pointer;
+  background: transparent;
+  border: 0;
+  border-radius: 8px;
+  transition:
+    color 0.18s ease,
+    background-color 0.18s ease,
+    box-shadow 0.18s ease;
+  align-items: center;
+  gap: 10px;
+}
+
+.device-item:hover {
+  background: var(--el-fill-color-light);
+}
+
+.device-item.is-active {
+  background: var(--fill-primary-light);
+  box-shadow: inset 3px 0 0 var(--fill-primary);
+}
+
+.device-item.is-active.is-filled {
+  background: var(--el-color-success-light-9);
+  box-shadow: inset 3px 0 0 var(--el-color-success);
+}
+
+.device-item__marker {
+  width: 8px;
+  height: 8px;
+  background: var(--el-border-color-darker);
+  border-radius: 999px;
+  transition: background 0.2s ease;
+  flex: 0 0 auto;
+}
+
+.device-item.is-active .device-item__marker {
+  background: var(--fill-primary);
+  box-shadow: 0 0 0 4px var(--el-color-primary-light-8);
+}
+
+.device-item.is-filled .device-item__marker {
+  background: var(--el-color-success);
+  box-shadow: 0 0 0 4px var(--el-color-success-light-8);
+}
+
+.device-item__content {
+  display: flex;
+  min-width: 0;
+  flex: 1;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.device-item__code {
+  overflow: hidden;
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.device-item.is-active .device-item__code {
+  color: var(--fill-primary);
+}
+
+.device-item.is-filled .device-item__code {
+  color: var(--el-color-success);
+}
+
+.device-item__name {
+  overflow: hidden;
+  font-size: 12px;
+  font-weight: 500;
+  color: var(--el-text-color-regular);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.device-item__arrow {
+  display: inline-flex;
+  width: 26px;
+  height: 26px;
+  font-size: 13px;
+  color: var(--el-text-color-placeholder);
+  border-radius: 7px;
+  transition:
+    color 0.2s ease,
+    background-color 0.2s ease,
+    transform 0.2s ease;
+  align-items: center;
+  justify-content: center;
+}
+
+.device-item.is-active .device-item__arrow {
+  color: var(--fill-primary);
+  background: var(--el-bg-color);
+  transform: translateX(2px);
+}
+
+.device-item.is-active.is-filled .device-item__arrow {
+  color: var(--el-color-success);
+}
+
+.form-panel {
+  display: flex;
+  min-width: 0;
+  min-height: 0;
+  overflow: hidden;
+  background: var(--el-bg-color);
+  flex-direction: column;
+}
+
+.form-panel__header {
+  display: flex;
+  height: 88px;
+  min-height: 88px;
+  padding: 16px 28px;
+  align-items: center;
+  gap: 24px;
+  border-bottom: 1px solid var(--el-border-color-light);
+  box-sizing: border-box;
+}
+
+.form-panel__identity {
+  display: flex;
+  min-width: 140px;
+  justify-content: center;
+  flex-direction: column;
+}
+
+.form-panel__eyebrow {
+  margin-bottom: 5px;
+  font-size: 12px;
+  font-weight: 600;
+  letter-spacing: 0.12em;
+  color: var(--fill-primary);
+}
+
+.form-panel__header h1 {
+  overflow: hidden;
+  font-size: 21px;
+  font-weight: 650;
+  color: var(--el-text-color-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.selected-device-code {
+  max-width: 22%;
+  padding: 7px 13px;
+  overflow: hidden;
+  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--fill-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  background: var(--fill-primary-light);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 8px;
+}
+
+.form-panel__body {
+  min-height: 0;
+  padding: 24px 28px 28px;
+  overflow-y: auto;
+  overscroll-behavior: contain;
+  scrollbar-gutter: stable;
+  flex: 1;
+}
+
+.basic-info__grid {
+  display: flex;
+  min-width: 0;
+  flex: 1;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 28px;
+}
+
+.basic-info__item {
+  display: flex;
+  min-width: 110px;
+  justify-content: center;
+  flex-direction: column;
+}
+
+.basic-info__item--well {
+  width: 210px;
+}
+
+.basic-info__label {
+  margin-bottom: 5px;
+  font-size: 12px;
+  font-weight: 500;
+  line-height: 18px;
+  color: var(--el-text-color-primary);
+}
+
+.basic-info__value {
+  display: flex;
+  height: 32px;
+  overflow: hidden;
+  font-size: 14px;
+  font-weight: 500;
+  line-height: 20px;
+  color: var(--el-text-color-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  align-items: center;
+}
+
+.basic-info__select {
+  width: 210px;
+}
+
+.sum-section {
+  display: flex;
+  padding: 2px 0 16px;
+  margin-bottom: 18px;
+  align-items: center;
+  gap: 24px;
+  border-bottom: 1px solid var(--el-border-color-extra-light);
+}
+
+.sum-section__title {
+  position: relative;
+  padding-left: 9px;
+  margin: 0;
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  flex: 0 0 auto;
+}
+
+.sum-list {
+  display: flex;
+  max-width: 100%;
+  overflow-x: auto;
+  align-items: center;
+}
+
+.sum-list__item {
+  display: flex;
+  padding: 0 22px;
+  align-items: baseline;
+  gap: 10px;
+}
+
+.sum-list__item:first-child {
+  padding-left: 0;
+}
+
+.sum-list__item + .sum-list__item {
+  border-left: 1px solid var(--el-border-color-lighter);
+}
+
+.sum-list__label {
+  font-size: 13px;
+  font-weight: 500;
+  color: var(--el-text-color-primary);
+  white-space: nowrap;
+}
+
+.sum-list__value {
+  font-size: 17px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+  white-space: nowrap;
+}
+
+.fill-section__title {
+  position: relative;
+  padding-left: 9px;
+  margin: 0 0 16px;
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.sum-section__title::before,
+.fill-section__title::before,
+.production-section__title::before {
+  position: absolute;
+  top: 50%;
+  left: 0;
+  width: 3px;
+  height: 20px;
+  background: var(--fill-primary);
+  border-radius: 999px;
+  content: '';
+  transform: translateY(-50%);
+}
+
+.fill-form__grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+  column-gap: 22px;
+}
+
+.fill-form :deep(.el-form-item) {
+  margin-bottom: 18px;
+}
+
+.fill-form :deep(.el-form-item__label) {
+  font-weight: 500;
+  color: var(--el-text-color-primary);
+}
+
+.fill-form__label {
+  display: inline-flex;
+  min-width: 0;
+  align-items: center;
+  gap: 5px;
+}
+
+.collection-warning-icon {
+  font-size: 15px;
+  color: var(--el-color-warning);
+  cursor: help;
+  flex: 0 0 auto;
+}
+
+.rd-limit-warning {
+  display: block;
+  width: 100%;
+  margin-top: 5px;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-color-warning-dark-2);
+}
+
+:global(.rd-limit-dialog) {
+  max-width: calc(100vw - 32px);
+  overflow: hidden;
+  border-radius: 12px;
+}
+
+:global(.rd-limit-dialog .el-dialog__header) {
+  padding: 16px 16px 10px;
+  margin-right: 0;
+}
+
+:global(.rd-limit-dialog .el-dialog__title) {
+  font-size: 17px;
+  font-weight: 600;
+}
+
+:global(.rd-limit-dialog .el-dialog__body) {
+  padding: 0 16px;
+}
+
+:global(.rd-confirm-description) {
+  margin: 0 0 12px;
+  font-size: 13px;
+  line-height: 20px;
+  color: var(--el-text-color-secondary);
+}
+
+:global(.rd-confirm-list) {
+  max-height: min(430px, calc(100vh - 190px));
+  overflow-y: auto;
+}
+
+:global(.rd-confirm-item) {
+  padding: 11px 12px;
+  margin-bottom: 8px;
+  background: var(--el-color-warning-light-9);
+  border: 1px solid var(--el-color-warning-light-7);
+  border-radius: 9px;
+}
+
+:global(.rd-confirm-item:last-child) {
+  margin-bottom: 0;
+}
+
+:global(.rd-confirm-item__header) {
+  display: flex;
+  margin-bottom: 9px;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+}
+
+:global(.rd-confirm-item__name) {
+  overflow: hidden;
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+:global(.rd-confirm-item__tag) {
+  padding: 1px 7px;
+  font-size: 11px;
+  line-height: 18px;
+  color: var(--el-color-warning-dark-2);
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-color-warning-light-5);
+  border-radius: 999px;
+  flex: 0 0 auto;
+}
+
+:global(.rd-confirm-item__values) {
+  display: grid;
+  margin-bottom: 7px;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 10px;
+}
+
+:global(.rd-confirm-item__value) {
+  display: flex;
+  padding: 8px 10px;
+  background: var(--el-bg-color);
+  border-radius: 6px;
+  flex-direction: column;
+  gap: 3px;
+}
+
+:global(.rd-confirm-item__value span) {
+  font-size: 11px;
+  color: var(--el-text-color-secondary);
+}
+
+:global(.rd-confirm-item__value strong) {
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+:global(.rd-confirm-item__formula) {
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-regular);
+}
+
+:global(.rd-limit-dialog .el-dialog__footer) {
+  display: flex;
+  padding: 12px 16px 14px;
+  justify-content: flex-end;
+  gap: 12px;
+}
+
+:global(.rd-limit-dialog .el-dialog__footer .el-button + .el-button) {
+  margin-left: 0;
+}
+
+.fill-form__item--wide {
+  grid-column: 1 / -1;
+}
+
+.fill-form__control,
+.fill-form :deep(.el-input),
+.fill-form :deep(.el-textarea) {
+  width: 100%;
+}
+
+.fill-form :deep(.el-input-number .el-input__inner) {
+  text-align: left;
+}
+
+.production-section {
+  padding-top: 20px;
+  margin-top: 8px;
+  border-top: 1px solid var(--el-border-color-lighter);
+}
+
+.production-section__header {
+  display: flex;
+  margin-bottom: 14px;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.production-section__title {
+  position: relative;
+  padding-left: 9px;
+  margin: 0;
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.production-table {
+  width: 100%;
+  border-radius: 8px;
+}
+
+.production-table :deep(.el-table__cell) {
+  vertical-align: middle;
+}
+
+.production-table :deep(.el-table__header-wrapper th) {
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  background: var(--el-fill-color-light);
+}
+
+.production-table__form-item {
+  margin-bottom: 0 !important;
+}
+
+.production-table__control {
+  width: 100% !important;
+}
+
+.production-table__duration {
+  display: inline-flex;
+  min-height: 32px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  align-items: center;
+}
+
+.form-actions {
+  display: flex;
+  min-height: 56px;
+  padding: 10px 24px;
+  background: var(--el-bg-color);
+  border-top: 1px solid var(--el-border-color-lighter);
+  box-shadow: 0 -4px 14px rgb(31 45 61 / 4%);
+  box-sizing: border-box;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 12px;
+  flex: 0 0 auto;
+}
+
+.form-actions :deep(.el-button) {
+  min-width: 76px;
+}
+
+.form-panel__empty {
+  flex: 1;
+  justify-content: center;
+}
+
+@media (width <= 900px) {
+  .fill-layout {
+    grid-template-columns: 1fr;
+  }
+
+  .device-panel {
+    max-height: 340px;
+    border-right: 0;
+    border-bottom: 1px solid var(--el-border-color-lighter);
+  }
+
+  .form-panel__header,
+  .form-panel__body {
+    padding-right: 20px;
+    padding-left: 20px;
   }
 }
 </style>