shiyanyu 1 месяц назад
Родитель
Сommit
18e6aa8532

+ 183 - 1
src/views/EntDeclaration/auditTaskManagement/components/DynamicTableDialog.vue

@@ -189,6 +189,7 @@
           :is-view-mode="!isEditMode"
           :fixed-fields="fixedFields"
           :fixed-fieldids="fixedFieldids"
+          :columns-meta="columnsMeta"
           :audited-unit-id="
             auditedUnitId || (surveyData && surveyData.auditedUnitId) || ''
           "
@@ -231,6 +232,7 @@
     getSingleRecordSurveyList,
     getSurveyDetail,
   } from '@/api/audit/survey'
+  import { getListBySurveyTemplateIdAndVersion } from '@/api/costSurveyTemplateHeaders'
 
   export default {
     name: 'DynamicTableDialog',
@@ -335,6 +337,7 @@
         // 固定字段配置
         fixedFields: '',
         fixedFieldids: '',
+        columnsMeta: [],
       }
     },
     computed: {
@@ -849,6 +852,122 @@
         this.detailSavedData = null
         this.loadSingleRecordSurveyList()
       },
+      // 将表头字段配置映射为列元数据(用于渲染与校验)
+      mapHeaderFieldToColumnMeta(item) {
+        if (!item || typeof item !== 'object') return null
+        const pick = (keys) => {
+          for (const k of keys) {
+            if (item[k] !== undefined && item[k] !== null && item[k] !== '') {
+              return item[k]
+            }
+          }
+          return undefined
+        }
+        const toBool = (v) => {
+          if (v === undefined || v === null) return false
+          if (typeof v === 'boolean') return v
+          if (typeof v === 'number') return v === 1
+          const s = String(v).trim().toLowerCase()
+          return ['1', 'true', 'y', 'yes', '是'].includes(s)
+        }
+        const toNum = (v) => {
+          if (v === undefined || v === null || v === '') return undefined
+          const n = Number(v)
+          return Number.isNaN(n) ? undefined : n
+        }
+        const fieldId =
+          pick([
+            'fieldId',
+            'field_id',
+            'fieldCode',
+            'field_code',
+            'columnName',
+            'column_name',
+            'fieldEname',
+            'field_ename',
+          ]) || ''
+        const label =
+          pick([
+            'columnComment',
+            'column_comment',
+            'fieldCname',
+            'field_cname',
+            'label',
+            'fieldName',
+            'field_name',
+          ]) ||
+          fieldId ||
+          ''
+        const rawFieldType = String(
+          pick(['fieldType', 'field_type']) || ''
+        ).toLowerCase()
+        const columnType = String(
+          pick(['columnType', 'column_type']) || rawFieldType
+        ).toLowerCase()
+        const totalLength = toNum(
+          pick([
+            'fieldTypeLen',
+            'fieldTypelen',
+            'field_typelen',
+            'length',
+            'fieldLength',
+          ])
+        )
+        const decimalLength = toNum(
+          pick([
+            'fieldTypeNointLen',
+            'fieldTypenointlen',
+            'field_typenointlen',
+            'scale',
+          ])
+        )
+        const required = toBool(pick(['isRequired', 'is_required', 'required']))
+        const optionsRaw = pick(['options'])
+        let options = []
+        if (Array.isArray(optionsRaw)) options = optionsRaw
+        else if (typeof optionsRaw === 'string' && optionsRaw.trim() !== '') {
+          options = optionsRaw
+            .split(',')
+            .map((v) => ({ label: v.trim(), value: v.trim() }))
+        }
+        let type = pick(['componentType', 'type'])
+        const tLower = (type || columnType || '').toLowerCase()
+        if (!type) {
+          if (tLower.includes('datetime') || tLower.includes('timestamp'))
+            type = 'datetime'
+          else if (tLower.includes('date')) type = 'date'
+          else if (tLower.includes('year')) type = 'year'
+          else if (
+            tLower.includes('int') ||
+            tLower.includes('number') ||
+            tLower.includes('decimal') ||
+            tLower.includes('float') ||
+            tLower.includes('double')
+          )
+            type = 'number'
+          else if (rawFieldType.includes('bool')) type = 'boolean'
+          else type = 'input'
+        }
+        // 整数类型强制小数位为0
+        let decLenOut = decimalLength
+        const rawTypeAll = rawFieldType || tLower
+        if (
+          String(type).toLowerCase() === 'number' &&
+          (rawTypeAll.includes('int') || rawTypeAll === 'integer')
+        ) {
+          decLenOut = 0
+        }
+        return {
+          fieldId: String(fieldId),
+          label: String(label),
+          type: String(type),
+          fieldType: rawFieldType || columnType,
+          required: !!required,
+          totalLength: totalLength,
+          decimalLength: decLenOut,
+          options: options,
+        }
+      },
       async loadSingleRecordSurveyList() {
         if (!this.currentRow) return
 
@@ -871,6 +990,13 @@
           this.currentRow.surveyTemplateId ||
           sd.surveyTemplateId ||
           ''
+        const versionId =
+          (this.currentRow &&
+            (this.currentRow.versionId ||
+              this.currentRow.version ||
+              this.currentRow.templateVersionId)) ||
+          (sd && (sd.versionId || sd.version || sd.templateVersionId)) ||
+          ''
         const periodRecordId =
           this.currentRow.id || this.currentRow.periodRecordId || ''
 
@@ -917,6 +1043,37 @@
             )
           }
 
+          // 1.1 获取模板字段定义与校验规则
+          try {
+            if (surveyTemplateId) {
+              const headerParams = {
+                surveyTemplateId,
+                versionId,
+                type: this.requestType,
+              }
+              const headerRes = await getListBySurveyTemplateIdAndVersion(
+                headerParams
+              )
+              if (headerRes && headerRes.code === 200) {
+                const fieldsArr = Array.isArray(headerRes.value)
+                  ? headerRes.value
+                  : headerRes.value && Array.isArray(headerRes.value.items)
+                  ? headerRes.value.items
+                  : []
+                const meta = fieldsArr
+                  .map((it) => this.mapHeaderFieldToColumnMeta(it))
+                  .filter(Boolean)
+                this.columnsMeta = meta
+              } else {
+                this.columnsMeta = []
+              }
+            } else {
+              this.columnsMeta = []
+            }
+          } catch (e) {
+            this.columnsMeta = []
+          }
+
           // 2. 获取填报数据
           const detailRes =
             periodRecordId || uploadId
@@ -939,10 +1096,14 @@
               this.localTableItems = tableItems
             }
 
+            const detailArr = this.extractArrayFromPayload(detailPayload)
             if (savedData) {
               this.detailSavedData = savedData
             } else if (tableItems && tableItems.length > 0) {
               this.detailSavedData = this.cloneDeep(tableItems)
+            } else if (Array.isArray(detailArr) && detailArr.length === 0) {
+              // 详情为空数组时,使用配置接口返回的 itemlist 渲染
+              this.detailSavedData = this.cloneDeep(this.localTableItems)
             } else {
               this.detailSavedData =
                 (this.currentRow && this.currentRow.data) || null
@@ -1130,13 +1291,18 @@
           if (!Array.isArray(item.children)) {
             item.children = []
           }
+          // 以多键索引,确保 child.parentid 可匹配到父项
           map.set(id, item)
+          const aliasRowId = item.rowid || item.rowId
+          if (aliasRowId && aliasRowId !== id) {
+            map.set(aliasRowId, item)
+          }
         })
 
         const roots = []
         cloned.forEach((item) => {
           if (!item || typeof item !== 'object') return
-          const parentId =
+          let parentId =
             item.parentId ??
             item.parentid ??
             item.parentID ??
@@ -1144,6 +1310,8 @@
             item.parent_code ??
             item.parentRowId ??
             item.parentrowid
+          // 兼容 parentid 为字符串 '-1' 视为根
+          if (String(parentId) === '-1') parentId = undefined
           if (parentId && map.has(parentId)) {
             const parent = map.get(parentId)
             if (!Array.isArray(parent.children)) {
@@ -1156,6 +1324,20 @@
           }
         })
 
+        // 对 roots 和各级 children 按 seq(或行内 index)排序,确保子项紧跟父项
+        const num = (v) => {
+          if (v === undefined || v === null || v === '') return Infinity
+          const n = Number(v)
+          return isNaN(n) ? Infinity : n
+        }
+        const sortTree = (nodes) => {
+          nodes.sort((a, b) => num(a.seq) - num(b.seq))
+          nodes.forEach(
+            (n) => Array.isArray(n.children) && sortTree(n.children)
+          )
+        }
+        sortTree(roots)
+
         return roots.length > 0 ? roots : cloned
       },
       // 保存详情

+ 398 - 77
src/views/EntDeclaration/auditTaskManagement/components/FixedAssetsTable.vue

@@ -19,7 +19,7 @@
 
       <!-- 动态表头列 -->
       <el-table-column
-        v-for="column in dynamicColumns"
+        v-for="column in renderedColumns"
         :key="column.prop"
         :prop="column.prop"
         :label="column.label"
@@ -27,22 +27,8 @@
         :align="column.align || 'center'"
       >
         <template slot-scope="scope">
-          <!-- parentid 为 -1 的行只读显示 -->
-          <span
-            v-if="
-              scope.row.parentid === '-1' ||
-              scope.row.parentid === -1 ||
-              scope.row.parentId === '-1' ||
-              scope.row.parentId === -1 ||
-              scope.row.isCategory
-            "
-            class="category-value"
-          >
-            {{ scope.row[column.prop] || '-' }}
-          </span>
-          <!-- 子项可编辑 -->
           <el-date-picker
-            v-else-if="column.type === 'date'"
+            v-if="column.type === 'date'"
             v-model="scope.row[column.prop]"
             type="date"
             placeholder="选择日期"
@@ -52,6 +38,81 @@
             style="width: 100%"
             :disabled="isViewMode"
           />
+          <el-date-picker
+            v-else-if="column.type === 'datetime'"
+            v-model="scope.row[column.prop]"
+            type="datetime"
+            placeholder="选择日期时间"
+            size="mini"
+            format="yyyy-MM-dd HH:mm:ss"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            style="width: 100%"
+            :disabled="isViewMode"
+          />
+          <el-date-picker
+            v-else-if="column.type === 'year'"
+            v-model="scope.row[column.prop]"
+            type="year"
+            placeholder="选择年份"
+            size="mini"
+            format="yyyy"
+            value-format="yyyy"
+            style="width: 100%"
+            :disabled="isViewMode"
+          />
+          <el-select
+            v-else-if="
+              column.type === 'select' && Array.isArray(column.options)
+            "
+            v-model="scope.row[column.prop]"
+            :placeholder="column.placeholder || '请选择' + column.label"
+            size="mini"
+            style="width: 100%"
+            :disabled="isViewMode"
+            clearable
+          >
+            <el-option
+              v-for="opt in column.options"
+              :key="
+                opt &&
+                (opt.value != null ? String(opt.value) : String(opt.label))
+              "
+              :label="opt && (opt.label != null ? opt.label : opt.value)"
+              :value="
+                opt &&
+                (opt.value != null ? String(opt.value) : String(opt.label))
+              "
+            />
+          </el-select>
+          <el-switch
+            v-else-if="column.type === 'boolean'"
+            v-model="scope.row[column.prop]"
+            :disabled="isViewMode"
+            active-value="1"
+            inactive-value="0"
+            active-text="是"
+            inactive-text="否"
+          />
+          <el-input
+            v-else-if="column.type === 'number'"
+            v-model="scope.row[column.prop]"
+            :placeholder="column.placeholder || '请输入' + column.label"
+            size="mini"
+            :disabled="isViewMode"
+            :maxlength="computeNumberMaxlength(column)"
+            @input="sanitizeNumberInput(scope.row, column)"
+            @keypress.native="
+              onNumberKeypressNumeric($event, scope.row, column)
+            "
+            @keydown.native="onNumberKeydownNumeric($event, scope.row, column)"
+            @paste.native.prevent="
+              onNumberPasteNumeric($event, scope.row, column)
+            "
+            @compositionend.native="
+              onNumberCompositionEndNumeric(scope.row, column)
+            "
+            @blur="handleCellBlur(scope.row, column.prop)"
+          />
           <el-input
             v-else
             v-model="scope.row[column.prop]"
@@ -170,6 +231,11 @@
         type: [String, Number],
         default: '',
       },
+      // 字段元数据(从模板接口返回,包含校验信息)
+      columnsMeta: {
+        type: Array,
+        default: () => [],
+      },
     },
     data() {
       return {
@@ -224,6 +290,65 @@
           }
         })
       },
+      // 根据后端字段的 fieldType/规则渲染具体控件类型
+      renderedColumns() {
+        const cols = Array.isArray(this.dynamicColumns)
+          ? this.dynamicColumns.map((c) => ({ ...c }))
+          : []
+        const metas = Array.isArray(this.columnsMeta) ? this.columnsMeta : []
+        const byLabel = new Map()
+        const byId = new Map()
+        metas.forEach((m) => {
+          if (m && m.label) byLabel.set(String(m.label), m)
+          if (m && m.fieldId) byId.set(String(m.fieldId), m)
+        })
+        const normalizeType = (meta, fallback) => {
+          const t = (
+            meta && (meta.type || meta.fieldType)
+              ? String(meta.type || meta.fieldType)
+              : ''
+          ).toLowerCase()
+          if (!t) return fallback
+          if (t.includes('datetime') || t.includes('timestamp'))
+            return 'datetime'
+          if (t.includes('date') && !t.includes('datetime')) return 'date'
+          if (t.includes('year')) return 'year'
+          if (
+            t.includes('int') ||
+            t.includes('number') ||
+            t.includes('decimal') ||
+            t.includes('float') ||
+            t.includes('double')
+          )
+            return 'number'
+          if (t.includes('bool')) return 'boolean'
+          if (t.includes('select')) return 'select'
+          return fallback
+        }
+        return cols.map((col) => {
+          const meta =
+            (col.label && byLabel.get(String(col.label))) ||
+            (col.fieldId && byId.get(String(col.fieldId))) ||
+            null
+          if (meta) {
+            col.type = normalizeType(meta, col.type || 'input')
+            if (Array.isArray(meta.options) && meta.options.length) {
+              col.options = meta.options
+            }
+            col.totalLength = meta.totalLength
+            // 若为整数类型,强制小数位为0
+            const rawType = String(
+              meta.type || meta.fieldType || ''
+            ).toLowerCase()
+            if (rawType.includes('int') || rawType === 'integer') {
+              col.decimalLength = 0
+            } else {
+              col.decimalLength = meta.decimalLength
+            }
+          }
+          return col
+        })
+      },
     },
     watch: {
       tableItems: {
@@ -294,6 +419,136 @@
       },
     },
     methods: {
+      sanitizeNumberInput(row, column) {
+        const prop = column && column.prop
+        if (!prop) return
+        let v = row[prop]
+        if (v === null || v === undefined) {
+          row[prop] = ''
+          return
+        }
+        v = String(v)
+        // 仅保留数字、-、.,并处理多个符号
+        v = v.replace(/[^0-9+\-.]/g, '')
+        // 只允许一个正负号且在最前
+        v = v.replace(/(?!^)[+\-]/g, '')
+        // 只允许一个小数点
+        const parts = v.split('.')
+        if (parts.length > 2) {
+          v = parts[0] + '.' + parts.slice(1).join('')
+        }
+        // 限制整数/小数位
+        const intLimit = Number(column.totalLength)
+        const decLimit = Number(column.decimalLength)
+        const sign = v.startsWith('-') || v.startsWith('+') ? v[0] : ''
+        const unsigned = sign ? v.slice(1) : v
+        const [iRaw, dRaw = ''] = unsigned.split('.')
+        let i = iRaw.replace(/^0+(?=\d)/, '')
+        let d = dRaw
+        if (!isNaN(intLimit) && intLimit > 0 && i.length > intLimit) {
+          i = i.slice(0, intLimit)
+        }
+        if (!isNaN(decLimit) && decLimit >= 0 && d.length > decLimit) {
+          d = d.slice(0, decLimit)
+        }
+        if (!isNaN(decLimit) && decLimit === 0) {
+          // 整数:不保留小数点
+          v = sign + i
+        } else {
+          v = sign + (dRaw !== undefined ? (i || '0') + '.' + d : i)
+        }
+        // 边界:只有 - 或 +
+        if (v === '-' || v === '+') {
+          row[prop] = ''
+          return
+        }
+        row[prop] = v
+      },
+      onNumberKeypressNumeric(e, row, column) {
+        const decLimit = Number(column.decimalLength)
+        const ch = e.key
+        if (ch.length !== 1) return
+        // 允许数字
+        if (/[0-9]/.test(ch)) return
+        // 允许一个小数点且仅当 decLimit>0
+        if (ch === '.') {
+          if (isNaN(decLimit) || decLimit <= 0) {
+            e.preventDefault()
+            return
+          }
+          const val = String(row[column.prop] || '')
+          if (val.includes('.')) {
+            e.preventDefault()
+            return
+          }
+          return
+        }
+        // 允许首位正负号
+        if (ch === '-' || ch === '+') {
+          const val = String(row[column.prop] || '')
+          if (val.length > 0) {
+            e.preventDefault()
+            return
+          }
+          return
+        }
+        // 其它全部拦截
+        e.preventDefault()
+      },
+      onNumberPasteNumeric(e, row, column) {
+        const text = (e.clipboardData && e.clipboardData.getData('text')) || ''
+        if (!text) return
+        let v = text.replace(/[^0-9+\-.]/g, '')
+        const decLimit = Number(column.decimalLength)
+        // 一个正负号,且在最前
+        v = v.replace(/(?!^)[+\-]/g, '')
+        // 一个小数点;若整数限定,去掉小数点
+        if (!isNaN(decLimit) && decLimit === 0) {
+          v = v.replace(/[.]/g, '')
+        } else {
+          const parts = v.split('.')
+          if (parts.length > 2) v = parts[0] + '.' + parts.slice(1).join('')
+        }
+        // 应用 input 过滤
+        const prop = column && column.prop
+        if (!prop) return
+        const curr = String(row[prop] == null ? '' : row[prop])
+        const selection =
+          window.getSelection && window.getSelection().toString()
+        // 简化:直接赋值后再走 onNumberInput 进行长度裁剪
+        row[prop] = v
+        this.sanitizeNumberInput(row, column)
+      },
+      onNumberKeydownNumeric(e, row, column) {
+        // 允许的控制键:Backspace, Delete, Tab, Arrow keys, Home, End
+        const code = e.key
+        const allowedControl = [
+          'Backspace',
+          'Delete',
+          'Tab',
+          'ArrowLeft',
+          'ArrowRight',
+          'Home',
+          'End',
+        ]
+        if (allowedControl.includes(code)) return
+        // 其它按键交给 keypress 处理
+      },
+      onNumberCompositionEndNumeric(row, column) {
+        // 输入法结束后再次清洗
+        this.sanitizeNumberInput(row, column)
+      },
+      computeNumberMaxlength(column) {
+        const intLimit = Number(column.totalLength)
+        const decLimit = Number(column.decimalLength)
+        if (!isNaN(intLimit) && intLimit > 0) {
+          // + 整数位 + 小数点 + 小数位
+          const extra = !isNaN(decLimit) && decLimit > 0 ? 1 + decLimit : 0
+          // 预留一个符号位
+          return intLimit + extra + 1
+        }
+        return undefined
+      },
       // 将传入的子项标记为只读(不可编辑/删除),新增的行不受影响
       markInitialChildrenReadonly() {
         const data = this.fixedAssetsData || []
@@ -1290,74 +1545,140 @@
           return false
         }
       },
-      // 保存前验证
+      // 保存前验证(基于 columnsMeta 动态字段规则)
       validateBeforeSave() {
-        const errors = []
-        const categories = Array.isArray(this.fixedAssetsData)
-          ? this.fixedAssetsData
-          : []
+        // 如果没有字段元数据,使用旧的兜底校验(保持兼容)
+        const metaArr = Array.isArray(this.columnsMeta) ? this.columnsMeta : []
+        if (!metaArr.length) {
+          const fallbackErrors = []
+          this.validationErrors = fallbackErrors
+          return fallbackErrors
+        }
 
-        categories.forEach((category) => {
-          const items = Array.isArray(category.items) ? category.items : []
-          items.forEach((item, index) => {
-            const rowNum = index + 1
-            // 必填校验
-            if (!item.name) {
-              errors.push(
-                `${category.category} 第${rowNum}行:项目名称不能为空`
-              )
-            }
-            if (!item.unit) {
-              errors.push(
-                `${category.category} 第${rowNum}行:计量单位不能为空`
-              )
-            }
-            if (
-              item.originalValue === '' ||
-              item.originalValue === undefined ||
-              item.originalValue === null
-            ) {
-              errors.push(
-                `${category.category} 第${rowNum}行:固定资产原值不能为空`
-              )
-            } else if (isNaN(Number(item.originalValue))) {
-              errors.push(
-                `${category.category} 第${rowNum}行:固定资产原值必须为数字`
-              )
-            }
-            if (!item.entryDate) {
-              errors.push(
-                `${category.category} 第${rowNum}行:入帐或竣工验收日期不能为空`
-              )
-            }
-            if (
-              item.depreciationPeriod === '' ||
-              item.depreciationPeriod === undefined ||
-              item.depreciationPeriod === null
-            ) {
-              errors.push(
-                `${category.category} 第${rowNum}行:折旧年限不能为空`
-              )
-            } else if (isNaN(Number(item.depreciationPeriod))) {
-              errors.push(
-                `${category.category} 第${rowNum}行:折旧年限必须为数字`
-              )
+        const metaByLabel = new Map()
+        const metaByFieldId = new Map()
+        metaArr.forEach((m) => {
+          if (!m) return
+          if (m.label) metaByLabel.set(String(m.label), m)
+          if (m.fieldId) metaByFieldId.set(String(m.fieldId), m)
+        })
+
+        const getMetaForProp = (prop, column) => {
+          // column.label 优先,其次 fieldId
+          if (column && column.label && metaByLabel.has(String(column.label))) {
+            return metaByLabel.get(String(column.label))
+          }
+          if (
+            column &&
+            column.fieldId &&
+            metaByFieldId.has(String(column.fieldId))
+          ) {
+            return metaByFieldId.get(String(column.fieldId))
+          }
+          // 最后尝试用 prop 当作 label 匹配
+          if (prop && metaByLabel.has(String(prop)))
+            return metaByLabel.get(String(prop))
+          return null
+        }
+
+        const isEmpty = (v) => v === undefined || v === null || v === ''
+        const isValidDate = (v) =>
+          typeof v === 'string' &&
+          /^\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2}(:\d{2})?)?$/.test(v)
+        const isIntegerWithLen = (v, len) => {
+          if (typeof v !== 'string') v = v == null ? '' : String(v)
+          if (!/^[-+]?\d+$/.test(v)) return false
+          const digits = v.replace(/^[-+]?/, '')
+          return len === undefined ? true : digits.length <= len
+        }
+        const isDecimalWithLen = (v, intLen, decLen) => {
+          if (typeof v !== 'string') v = v == null ? '' : String(v)
+          if (!/^[-+]?\d*(?:\.\d+)?$/.test(v)) return false
+          const [intPart, decPart = ''] = v.replace(/^[-+]?/, '').split('.')
+          const intOk = intLen === undefined ? true : intPart.length <= intLen
+          const decOk = decLen === undefined ? true : decPart.length <= decLen
+          // 至少有整数或小数位
+          return intOk && decOk && (intPart.length > 0 || decPart.length > 0)
+        }
+
+        const errors = []
+        const flat = Array.isArray(this.flattenedData) ? this.flattenedData : []
+        flat.forEach((row, rowIndex) => {
+          // 跳过分类行
+          if (
+            row.parentid === '-1' ||
+            row.parentid === -1 ||
+            row.parentId === '-1' ||
+            row.parentId === -1 ||
+            row.isCategory
+          ) {
+            return
+          }
+          this.dynamicColumns.forEach((col) => {
+            const prop = col.prop
+            const value = row[prop]
+            const meta = getMetaForProp(prop, col)
+            if (!meta) return
+
+            // 必填
+            if (meta.required && isEmpty(value)) {
+              errors.push(`第${rowIndex + 1}行【${col.label}】不能为空`)
+              return
             }
+            if (isEmpty(value)) return
+
+            const t = String(meta.type || meta.fieldType || '').toLowerCase()
+            const intLen = meta.totalLength
+            const decLen = meta.decimalLength
             if (
-              item.depreciationExpense === '' ||
-              item.depreciationExpense === undefined ||
-              item.depreciationExpense === null
+              t === 'number' ||
+              t === 'int' ||
+              t === 'integer' ||
+              t === 'decimal' ||
+              t === 'float' ||
+              t === 'double'
             ) {
-              errors.push(`${category.category} 第${rowNum}行:折旧费不能为空`)
-            } else if (isNaN(Number(item.depreciationExpense))) {
-              errors.push(
-                `${category.category} 第${rowNum}行:折旧费必须为数字`
-              )
+              if (!decLen || decLen === 0) {
+                if (!isIntegerWithLen(value, intLen)) {
+                  errors.push(
+                    `第${rowIndex + 1}行【${col.label}】必须为整数且不超过${
+                      intLen || '限定'
+                    }位`
+                  )
+                }
+              } else {
+                if (!isDecimalWithLen(value, intLen, decLen)) {
+                  errors.push(
+                    `第${rowIndex + 1}行【${col.label}】必须为数字,整数不超过${
+                      intLen || '限定'
+                    }位,小数不超过${decLen}位`
+                  )
+                }
+              }
+            } else if (t === 'date' || t === 'datetime' || t === 'year') {
+              if (!isValidDate(value)) {
+                errors.push(
+                  `第${rowIndex + 1}行【${
+                    col.label
+                  }】日期格式不正确,应为YYYY-MM-DD或YYYY-MM-DD HH:mm:ss`
+                )
+              }
             }
-            if (!item.fundSource) {
-              errors.push(
-                `${category.category} 第${rowNum}行:资金来源不能为空`
+
+            // 字典/枚举
+            if (Array.isArray(meta.options) && meta.options.length > 0) {
+              const allowed = new Set(
+                meta.options.map(
+                  (o) =>
+                    o && (o.value != null ? String(o.value) : String(o.label))
+                )
               )
+              const valStr = String(value)
+              if (!allowed.has(valStr)) {
+                errors.push(
+                  `第${rowIndex + 1}行【${col.label}】不在允许的取值范围内`
+                )
+              }
             }
           })
         })