Преглед на файлове

Merge remote-tracking branch 'origin/master'

zzw преди 1 месец
родител
ревизия
3c331f6519

+ 374 - 29
src/views/EntDeclaration/auditTaskManagement/components/CostSurveyTab.vue

@@ -23,7 +23,7 @@
     <!-- 固定表填报弹窗 -->
     <fixed-table-dialog
       :visible.sync="fixedTableDialogVisible"
-      :survey-data="currentSurveyRow"
+      :survey-data="{ ...currentSurveyRow, fixedHeaders }"
       :table-items="tableItems"
       :audit-periods="auditPeriods"
       :is-view-mode="isViewMode"
@@ -232,6 +232,7 @@
     getSurveyDetail,
     getDynamicTableData,
   } from '@/api/audit/survey'
+  import { getListBySurveyTemplateIdAndVersion } from '@/api/costSurveyTemplateHeaders'
 
   export default {
     name: 'CostSurveyTab',
@@ -297,6 +298,7 @@
         dynamicTableData: [],
         dynamicDialogKey: 0,
         dynamicTableLoading: false,
+        fixedHeaders: null,
       }
     },
     mounted() {
@@ -458,7 +460,7 @@
       },
       // 初始化表单字段配置
       async initFormFields() {
-        // 如果是单记录类型,调用接口获取表单字段配置
+        // 如果是单记录类型,调用接口获取表单字段配置(包含校验规则)
         if (
           this.currentSurveyRow &&
           this.currentSurveyRow.tableType === '单记录' &&
@@ -468,35 +470,53 @@
             const params = {
               surveyTemplateId: this.currentSurveyRow.surveyTemplateId,
             }
-            const res = await getSingleRecordSurveyList(params)
-            console.log('单记录表单字段配置', res)
-            if (res && res.code === 200 && res.value) {
-              // 将接口返回的数据转换为表单字段配置格式
-              const { fixedFields, fixedFieldids } = res.value
-
-              if (fixedFields && fixedFieldids) {
-                // 将 fixedFields 和 fixedFieldids 按逗号分割
-                const labels = fixedFields.split(',').map((item) => item.trim())
-                const ids = fixedFieldids.split(',').map((item) => item.trim())
-                console.log('labels', labels)
-                console.log('ids', ids)
+            // 调用 getListBySurveyTemplateIdAndVersion 获取完整的字段配置(包含校验规则)
+            const res = await getListBySurveyTemplateIdAndVersion(params)
+            console.log('单记录表单字段配置(含校验规则)', res)
+            if (res && res.code === 200) {
+              let mapped = []
+              if (Array.isArray(res.value)) {
+                // 数组格式:直接映射每个字段(包含完整的校验规则)
+                mapped = res.value
+                  .map((item, index) =>
+                    this.mapApiFieldToFormField(item, index)
+                  )
+                  .filter(Boolean)
+              } else if (res.value && typeof res.value === 'object') {
+                // 对象格式:从 fixedFields 和 fixedFieldids 解析(兼容旧格式)
+                const { fixedFields, fixedFieldids } = res.value
+                if (fixedFields && fixedFieldids) {
+                  const labels = fixedFields
+                    .split(',')
+                    .map((item) => item.trim())
+                  const ids = fixedFieldids
+                    .split(',')
+                    .map((item) => item.trim())
+                  mapped = labels.map((label, index) => ({
+                    prop: ids[index] || `field_${index}`,
+                    label: label,
+                    type: 'input',
+                    colSpan: 12,
+                    placeholder: `请输入${label}`,
+                    rules: [],
+                    defaultValue: '',
+                    disabled: false,
+                    clearable: true,
+                    multiple: false,
+                    required: false,
+                  }))
+                }
+              }
 
-                // 组合成表单字段配置数组
-                this.formFields = labels.map((label, index) => ({
-                  prop: ids[index] || `field_${index}`, // 使用 fixedFieldids 作为 prop
-                  label: label, // 使用 fixedFields 作为 label
-                  type: 'input', // 默认类型为 input
-                  colSpan: 12, // 默认占一半宽度
-                  placeholder: `请输入${label}`,
-                  rules: [],
-                  defaultValue: '',
-                  disabled: false,
-                  clearable: true,
-                  multiple: false,
-                  required: false,
-                }))
+              if (mapped.length > 0) {
+                // 使用包含完整校验规则的字段配置
+                this.formFields = mapped
+                console.log(
+                  '转换后的表单字段配置(含校验规则)',
+                  this.formFields
+                )
               } else {
-                // 如果没有 fixedFields 和 fixedFieldids,使用默认配置
+                // 如果没有数据,使用默认配置
                 this.formFields = this.getMockFormFields()
               }
             } else {
@@ -521,6 +541,317 @@
           this.surveyFormDialogVisible = true
         }
       },
+      // 将 API 返回的字段数据映射为表单字段配置(包含校验规则)
+      mapApiFieldToFormField(item, index = 0) {
+        if (!item) return null
+        const getVal = (keys, fallback) => {
+          for (const key of keys) {
+            if (
+              key &&
+              item[key] !== undefined &&
+              item[key] !== null &&
+              item[key] !== ''
+            ) {
+              return item[key]
+            }
+          }
+          return fallback
+        }
+        const toBool = (value) => {
+          if (value === undefined || value === null) return false
+          if (typeof value === 'boolean') return value
+          if (typeof value === 'number') return value === 1
+          const str = String(value).trim().toLowerCase()
+          return ['1', 'true', 'y', 'yes', '是'].includes(str)
+        }
+        const toNumber = (value) => {
+          if (value === undefined || value === null || value === '')
+            return undefined
+          const num = Number(value)
+          return Number.isNaN(num) ? undefined : num
+        }
+        const prop =
+          getVal(
+            [
+              'fieldName',
+              'field_name',
+              'columnName',
+              'column_name',
+              'fieldCode',
+            ],
+            undefined
+          ) || `field_${index}`
+        const label =
+          getVal(
+            [
+              'columnComment',
+              'column_comment',
+              'fieldCname',
+              'field_cname',
+              'fieldLabel',
+              'field_label',
+            ],
+            prop
+          ) || prop
+        const columnType =
+          (getVal(
+            ['columnType', 'column_type', 'fieldType', 'field_type'],
+            ''
+          ) || '') + ''
+        const columnTypeLower = columnType.toLowerCase()
+        const totalLength = toNumber(
+          getVal(
+            ['fieldTypeLen', 'field_typelen', 'length', 'fieldLength'],
+            undefined
+          )
+        )
+        const decimalLength = toNumber(
+          getVal(
+            ['fieldTypeNointLen', 'field_typenointlen', 'scale'],
+            undefined
+          )
+        )
+        const isAuditPeriod = toBool(
+          getVal(['isAuditPeriod', 'is_audit_period'], false)
+        )
+        const dictCode =
+          getVal(
+            [
+              'dictCode',
+              'dict_code',
+              'dictId',
+              'dictid',
+              'dictType',
+              'dict_type',
+            ],
+            ''
+          ) || ''
+        const optionsRaw = getVal(['options'], [])
+        let options = []
+        if (Array.isArray(optionsRaw)) {
+          options = optionsRaw
+        } else if (typeof optionsRaw === 'string' && optionsRaw.trim() !== '') {
+          options = optionsRaw.split(',').map((value) => ({
+            label: value.trim(),
+            value: value.trim(),
+          }))
+        }
+        let type = getVal(['componentType', 'type'], '')
+        if (!type) {
+          if (dictCode || options.length > 0) {
+            type = 'select'
+          } else if (
+            columnTypeLower.includes('datetime') ||
+            columnTypeLower.includes('timestamp') ||
+            columnTypeLower.includes('date time')
+          ) {
+            type = 'datetime'
+          } else if (columnTypeLower.includes('date')) {
+            type = 'date'
+          } else if (columnTypeLower.includes('year')) {
+            type = 'year'
+          } else if (
+            columnTypeLower.includes('int') ||
+            columnTypeLower.includes('number') ||
+            columnTypeLower.includes('decimal') ||
+            columnTypeLower.includes('float') ||
+            columnTypeLower.includes('double')
+          ) {
+            type = 'number'
+          } else {
+            type = 'input'
+          }
+        }
+        const required = toBool(
+          getVal(['isRequired', 'is_required', 'required'], false)
+        )
+        const multiple = toBool(
+          getVal(['isMultiple', 'is_multiple', 'multiple'], false)
+        )
+        const colSpan =
+          toNumber(
+            getVal(['colSpan', 'colspan', 'columnSpan', 'column_span'], 12)
+          ) || 12
+        const placeholder =
+          getVal(
+            ['placeholder', 'columnComment', 'column_comment'],
+            undefined
+          ) || (type === 'select' ? `请选择${label}` : `请输入${label}`)
+        const defaultValue = getVal(
+          ['defaultValue', 'default_value', 'defaultVal', 'default_val'],
+          undefined
+        )
+        const precision = toNumber(
+          getVal(
+            ['fieldTypeNointLen', 'field_typenointlen', 'precision'],
+            undefined
+          )
+        )
+        const min = toNumber(getVal(['min'], undefined))
+        const max = toNumber(getVal(['max'], undefined))
+        const format = getVal(['format'], undefined)
+        const valueFormat =
+          getVal(['valueFormat', 'value_format'], undefined) ||
+          (type === 'datetime'
+            ? 'yyyy-MM-dd HH:mm:ss'
+            : type === 'date'
+            ? 'yyyy-MM-dd'
+            : type === 'year'
+            ? 'yyyy'
+            : undefined)
+
+        const formatLength = this.extractLengthFromFormat(format)
+        const rules = this.buildFieldRules({
+          type,
+          label,
+          required,
+          totalLength,
+          decimalLength,
+          formatLength,
+          format,
+          isAuditPeriod,
+        })
+
+        return {
+          prop,
+          label,
+          type,
+          colSpan,
+          placeholder,
+          dictCode,
+          dictType: dictCode,
+          options,
+          required,
+          defaultValue,
+          multiple,
+          precision,
+          min,
+          max,
+          format,
+          valueFormat,
+          totalLength,
+          decimalLength,
+          formatLength,
+          rules, // 包含完整的校验规则
+        }
+      },
+      // 从 format 中提取长度
+      extractLengthFromFormat(format) {
+        if (!format) return undefined
+        const str = String(format).trim()
+        if (!str) return undefined
+        const match = str.match(/\d+/)
+        if (match && match[0]) {
+          const len = Number(match[0])
+          return Number.isNaN(len) ? undefined : len
+        }
+        return undefined
+      },
+      // 构建字段校验规则
+      buildFieldRules(meta) {
+        const {
+          type,
+          label,
+          required,
+          totalLength,
+          decimalLength,
+          formatLength,
+          format,
+          isAuditPeriod,
+        } = meta || {}
+        const rules = []
+        const trigger = type === 'select' ? 'change' : 'blur'
+        if (required) {
+          rules.push({
+            required: true,
+            message: `${type === 'select' ? '请选择' : '请输入'}${label}`,
+            trigger,
+          })
+        }
+        const inputMaxLength = formatLength || totalLength
+        if (type === 'input' && inputMaxLength) {
+          rules.push({
+            validator: (_, value, callback) => {
+              if (value === undefined || value === null || value === '') {
+                callback()
+                return
+              }
+              const str = String(value)
+              if (str.length > inputMaxLength) {
+                callback(
+                  new Error(`${label}长度不能超过${inputMaxLength}个字符`)
+                )
+              } else {
+                callback()
+              }
+            },
+            trigger: 'blur',
+          })
+        }
+        const numberTotal = totalLength || formatLength
+        if (type === 'number') {
+          rules.push({
+            validator: (_, value, callback) => {
+              if (value === undefined || value === null || value === '') {
+                callback()
+                return
+              }
+              if (Number.isNaN(Number(value))) {
+                callback(new Error(`${label}必须为数字`))
+                return
+              }
+              const pure = String(value).replace('-', '')
+              if (numberTotal && pure.replace('.', '').length > numberTotal) {
+                callback(new Error(`${label}总位数不能超过${numberTotal}`))
+                return
+              }
+              if (decimalLength !== undefined && decimalLength !== null) {
+                const decimals = pure.split('.')[1] || ''
+                if (decimals.length > decimalLength) {
+                  callback(
+                    new Error(`${label}小数位不能超过${decimalLength}位`)
+                  )
+                  return
+                }
+              }
+              callback()
+            },
+            trigger: 'blur',
+          })
+        }
+        if (type === 'datetime' || type === 'date') {
+          if (format) {
+            rules.push({
+              validator: (_, value, callback) => {
+                if (value === undefined || value === null || value === '') {
+                  callback()
+                  return
+                }
+                callback()
+              },
+              trigger: 'change',
+            })
+          }
+        }
+        if (type === 'year' || isAuditPeriod) {
+          rules.push({
+            validator: (_, value, callback) => {
+              if (value === undefined || value === null || value === '') {
+                callback()
+                return
+              }
+              const pattern = /^\d{4}$/
+              if (!pattern.test(String(value))) {
+                callback(new Error(`${label}必须是四位年份`))
+              } else {
+                callback()
+              }
+            },
+            trigger: 'change',
+          })
+        }
+        return rules
+      },
       // 获取假数据表单字段配置(用于测试)
       getMockFormFields() {
         return [
@@ -697,6 +1028,20 @@
           ]
         }
 
+        // 额外拉取表头/规则信息,并透传给弹窗
+        try {
+          const headerRes = await getListBySurveyTemplateIdAndVersion({
+            surveyTemplateId: this.currentSurveyRow.surveyTemplateId,
+          })
+          if (headerRes && headerRes.code === 200) {
+            this.fixedHeaders = headerRes.value || null
+          } else {
+            this.fixedHeaders = null
+          }
+        } catch (e) {
+          this.fixedHeaders = null
+        }
+
         // 打开弹窗
         this.fixedTableDialogVisible = true
       },

+ 91 - 0
src/views/EntDeclaration/auditTaskManagement/components/FixedTableDialog.vue

@@ -171,6 +171,8 @@
         tableData: [],
         yearColumns: [],
         validationErrors: [],
+        cellCodeIndex: {}, // cellCode -> row
+        dependentsMap: {}, // cellCode -> Set(rows depending on it)
       }
     },
     watch: {
@@ -369,6 +371,10 @@
         })
 
         this.tableData = flatData
+
+        // 构建公式索引并计算一次
+        this.buildFormulaEngine()
+        this.recomputeAllFormulas()
       },
       // 创建行数据
       createRowData(item, isChild, rowid) {
@@ -471,6 +477,9 @@
 
               // 强制更新视图
               this.$forceUpdate()
+
+              // 回显后根据公式重算
+              this.recomputeAllFormulas()
             }
           } catch (err) {
             console.error('获取固定表详情失败', err)
@@ -562,6 +571,8 @@
       handleCellInput(row, year) {
         // 实时验证勾稽关系
         this.validateLinkage(row, year)
+        // 实时联动计算
+        this.recomputeDependentsForRow(row, year)
       },
       // 单元格失焦事件
       handleCellBlur(row, year) {
@@ -569,6 +580,8 @@
         this.validateCell(row, year)
         // 验证勾稽关系
         this.validateLinkage(row, year)
+        // 失焦后再次联动计算,确保取整/格式等影响后结果一致
+        this.recomputeDependentsForRow(row, year)
       },
       // 验证单元格(非空和格式验证)
       validateCell(row, year) {
@@ -855,6 +868,84 @@
           Message.error(err.message || '保存失败')
         }
       },
+      // 构建 cellCode 索引与依赖图
+      buildFormulaEngine() {
+        const index = {}
+        const deps = {}
+        this.tableData.forEach((row) => {
+          if (row && row.cellCode) {
+            index[row.cellCode] = row
+          }
+        })
+        // 构建依赖映射:A1 -> [依赖 A1 的行...]
+        this.tableData.forEach((row) => {
+          if (!row || !row.calculationFormula) return
+          const codes =
+            row.calculationFormula.toString().match(/[A-Z]+\d+/g) || []
+          codes.forEach((code) => {
+            if (!deps[code]) deps[code] = new Set()
+            deps[code].add(row)
+          })
+        })
+        this.cellCodeIndex = index
+        this.dependentsMap = deps
+      },
+      // 重新计算一个公式行在某年的值
+      recomputeRowForYear(row, year) {
+        if (!row || !row.calculationFormula) return
+        const expr = row.calculationFormula.toString()
+        // 替换变量为对应年的数值
+        const replaced = expr.replace(/[A-Z]+\d+/g, (code) => {
+          const refRow = this.cellCodeIndex[code]
+          const v = refRow ? Number(refRow[`year_${year}`]) : 0
+          return isNaN(v) ? '0' : String(v)
+        })
+        // 仅允许数字与运算符
+        const safe = this.safeEvalExpression(replaced)
+        if (safe !== null && !isNaN(safe)) {
+          row[`year_${year}`] = String(safe)
+        }
+      },
+      // 在某年针对一个输入行,联动所有依赖它的行
+      recomputeDependentsForRow(row, year) {
+        if (!row) return
+        const code = row.cellCode
+        if (!code) return
+        const dependents = this.dependentsMap[code]
+        if (!dependents || dependents.size === 0) return
+        dependents.forEach((depRow) => this.recomputeRowForYear(depRow, year))
+        // 可能存在链式依赖,递归触发
+        dependents.forEach((depRow) =>
+          this.recomputeDependentsForRow(depRow, year)
+        )
+      },
+      // 对所有含公式的行、所有年重算
+      recomputeAllFormulas() {
+        if (!this.yearColumns || this.yearColumns.length === 0) return
+        const formulaRows = this.tableData.filter(
+          (r) => r && r.calculationFormula
+        )
+        this.yearColumns.forEach((year) => {
+          formulaRows.forEach((r) => this.recomputeRowForYear(r, year))
+        })
+        this.$forceUpdate()
+      },
+      // 简单安全表达式求值:仅支持数字、+ - * / 和括号
+      safeEvalExpression(expr) {
+        const cleaned = expr.replace(/\s+/g, '')
+        if (!/^[-+*/().\d]+$/.test(cleaned)) {
+          // 存在不允许的字符,放弃计算
+          return null
+        }
+        try {
+          // eslint-disable-next-line no-new-func
+          const fn = new Function(`return (${cleaned})`)
+          const res = fn()
+          return typeof res === 'number' && isFinite(res) ? res : null
+        } catch (e) {
+          return null
+        }
+      },
     },
   }
 </script>

+ 469 - 37
src/views/EntDeclaration/auditTaskManagement/components/SurveyFormDialog.vue

@@ -13,15 +13,11 @@
       <el-row :gutter="20">
         <!-- 动态生成表单字段 -->
         <el-col
-          v-for="(field, index) in formFields"
+          v-for="(field, index) in effectiveFormFields"
           :key="field.prop || field.id || `field-${index}`"
           :span="field.colSpan || 12"
         >
-          <el-form-item
-            :label="field.label"
-            :prop="field.prop"
-            :rules="field.rules"
-          >
+          <el-form-item :label="field.label" :prop="field.prop">
             <!-- 文本输入框 -->
             <el-input
               v-if="field.type === 'input' || !field.type"
@@ -44,7 +40,9 @@
 
             <!-- 下拉选择框(字典类型) -->
             <el-select
-              v-else-if="field.type === 'select' && field.dictType"
+              v-else-if="
+                field.type === 'select' && (field.dictCode || field.dictType)
+              "
               v-model="form[field.prop]"
               :placeholder="field.placeholder || `请选择${field.label}`"
               :disabled="field.disabled || isViewMode"
@@ -53,7 +51,7 @@
               :multiple="field.multiple"
             >
               <el-option
-                v-for="item in getDictOptions(field.dictType)"
+                v-for="item in getDictOptions(field.dictCode || field.dictType)"
                 :key="item.key || item.value"
                 :label="item.name || item.label"
                 :value="item.key || item.value"
@@ -139,6 +137,7 @@
   import { Message } from 'element-ui'
   import { dictMixin } from '@/mixins/useDict'
   import { saveSingleRecordSurvey } from '@/api/audit/survey'
+  import { getListBySurveyTemplateIdAndVersion } from '@/api/costSurveyTemplateHeaders'
 
   export default {
     name: 'SurveyFormDialog',
@@ -205,27 +204,54 @@
         form: {},
         rules: {},
         dictData: {}, // 初始化字典数据对象
+        internalFormFields: [],
       }
     },
     computed: {
       // 计算需要获取的字典类型
       dictTypes() {
         const types = new Set()
-        this.formFields.forEach((field) => {
-          if (field.type === 'select' && field.dictType) {
-            types.add(field.dictType)
+        this.effectiveFormFields.forEach((field) => {
+          const dictKey = field.dictCode || field.dictType
+          if (field.type === 'select' && dictKey) {
+            types.add(dictKey)
           }
         })
         return Array.from(types)
       },
+      effectiveFormFields() {
+        if (this.internalFormFields && this.internalFormFields.length > 0) {
+          return this.internalFormFields
+        }
+        if (Array.isArray(this.formFields) && this.formFields.length > 0) {
+          return this.formFields
+        }
+        return this.getDefaultFormFields()
+      },
     },
     watch: {
       visible: {
-        handler(newVal) {
+        async handler(newVal) {
           this.dialogVisible = newVal
           if (newVal) {
-            // 弹窗打开时初始化表单
+            // 弹窗打开时,强制重新获取字段配置并初始化表单
+            // 先清空旧数据,确保重新加载
+            this.internalFormFields = []
+            this.form = {}
+            this.rules = {}
+            // 等待字段配置加载完成
+            await this.ensureTemplateFields()
+            // 确保 DOM 更新后再初始化表单
+            await this.$nextTick()
             this.initForm()
+          } else {
+            // 关闭弹窗时,清理表单数据但保留字段配置(可选)
+            // this.internalFormFields = []
+            this.form = {}
+            this.rules = {}
+            if (this.$refs.surveyForm) {
+              this.$refs.surveyForm.clearValidate()
+            }
           }
         },
         immediate: true,
@@ -236,7 +262,10 @@
         }
       },
       formFields: {
-        handler() {
+        handler(newVal) {
+          if (Array.isArray(newVal) && newVal.length > 0) {
+            this.internalFormFields = []
+          }
           // 字段配置变化时重新初始化
           if (this.dialogVisible) {
             this.initForm()
@@ -245,7 +274,8 @@
         deep: true,
       },
       surveyData: {
-        handler() {
+        async handler() {
+          await this.ensureTemplateFields()
           // 详情数据变化时重新初始化表单(用于回显数据)
           if (this.dialogVisible) {
             this.initForm()
@@ -253,12 +283,388 @@
         },
         deep: true,
       },
+      surveyTemplateId: {
+        async handler() {
+          await this.ensureTemplateFields()
+          if (this.dialogVisible) {
+            this.initForm()
+          }
+        },
+      },
     },
-    created() {
+    async created() {
+      await this.ensureTemplateFields()
       // 初始化字典数据
       this.initDictData()
     },
     methods: {
+      async ensureTemplateFields() {
+        const hasExternalFields =
+          Array.isArray(this.formFields) && this.formFields.length > 0
+        const templateId =
+          this.surveyTemplateId ||
+          this.surveyData.surveyTemplateId ||
+          this.surveyData.surveyId ||
+          ''
+        if (!templateId) {
+          return
+        }
+        // 只有在没有外部字段配置时才获取内部字段
+        if (!hasExternalFields) {
+          try {
+            const params = {
+              surveyTemplateId: templateId,
+            }
+            const res = await getListBySurveyTemplateIdAndVersion(params)
+            if (res && res.code === 200) {
+              let mapped = []
+              if (Array.isArray(res.value)) {
+                // 数组格式:直接映射每个字段
+                mapped = res.value
+                  .map((item, index) =>
+                    this.mapApiFieldToFormField(item, index)
+                  )
+                  .filter(Boolean)
+              } else if (res.value && typeof res.value === 'object') {
+                // 对象格式:从 fixedFields 和 fixedFieldids 解析
+                const { fixedFields, fixedFieldids } = res.value
+                if (fixedFields && fixedFieldids) {
+                  const labels = String(fixedFields)
+                    .split(',')
+                    .map((item) => item.trim())
+                    .filter(Boolean)
+                  const ids = String(fixedFieldids)
+                    .split(',')
+                    .map((item) => item.trim())
+                    .filter(Boolean)
+                  mapped = labels.map((label, index) => ({
+                    prop: ids[index] || `field_${index}`,
+                    label,
+                    type: 'input',
+                    colSpan: 12,
+                    placeholder: `请输入${label}`,
+                    required: false,
+                  }))
+                }
+              }
+              // 使用 Vue.set 确保响应式更新
+              if (mapped.length > 0) {
+                this.$set(this, 'internalFormFields', mapped)
+              }
+            }
+          } catch (error) {
+            console.error('获取调查表字段失败:', error)
+          }
+        }
+      },
+      mapApiFieldToFormField(item, index = 0) {
+        if (!item) return null
+        const getVal = (keys, fallback) => {
+          for (const key of keys) {
+            if (
+              key &&
+              item[key] !== undefined &&
+              item[key] !== null &&
+              item[key] !== ''
+            ) {
+              return item[key]
+            }
+          }
+          return fallback
+        }
+        const toBool = (value) => {
+          if (value === undefined || value === null) return false
+          if (typeof value === 'boolean') return value
+          if (typeof value === 'number') return value === 1
+          const str = String(value).trim().toLowerCase()
+          return ['1', 'true', 'y', 'yes', '是'].includes(str)
+        }
+        const toNumber = (value) => {
+          if (value === undefined || value === null || value === '')
+            return undefined
+          const num = Number(value)
+          return Number.isNaN(num) ? undefined : num
+        }
+        const prop =
+          getVal(
+            [
+              'fieldName',
+              'field_name',
+              'columnName',
+              'column_name',
+              'fieldCode',
+            ],
+            undefined
+          ) || `field_${index}`
+        const label =
+          getVal(
+            [
+              'columnComment',
+              'column_comment',
+              'fieldCname',
+              'field_cname',
+              'fieldLabel',
+              'field_label',
+            ],
+            prop
+          ) || prop
+        const columnType =
+          (getVal(
+            ['columnType', 'column_type', 'fieldType', 'field_type'],
+            ''
+          ) || '') + ''
+        const columnTypeLower = columnType.toLowerCase()
+        const totalLength = toNumber(
+          getVal(
+            ['fieldTypeLen', 'field_typelen', 'length', 'fieldLength'],
+            undefined
+          )
+        )
+        const decimalLength = toNumber(
+          getVal(
+            ['fieldTypeNointLen', 'field_typenointlen', 'scale'],
+            undefined
+          )
+        )
+        const isAuditPeriod = toBool(
+          getVal(['isAuditPeriod', 'is_audit_period'], false)
+        )
+        const dictCode =
+          getVal(
+            [
+              'dictCode',
+              'dict_code',
+              'dictId',
+              'dictid',
+              'dictType',
+              'dict_type',
+            ],
+            ''
+          ) || ''
+        const optionsRaw = getVal(['options'], [])
+        let options = []
+        if (Array.isArray(optionsRaw)) {
+          options = optionsRaw
+        } else if (typeof optionsRaw === 'string' && optionsRaw.trim() !== '') {
+          options = optionsRaw.split(',').map((value) => ({
+            label: value.trim(),
+            value: value.trim(),
+          }))
+        }
+        let type = getVal(['componentType', 'type'], '')
+        if (!type) {
+          if (dictCode || options.length > 0) {
+            type = 'select'
+          } else if (
+            columnTypeLower.includes('datetime') ||
+            columnTypeLower.includes('timestamp') ||
+            columnTypeLower.includes('date time')
+          ) {
+            type = 'datetime'
+          } else if (columnTypeLower.includes('date')) {
+            type = 'date'
+          } else if (columnTypeLower.includes('year')) {
+            type = 'year'
+          } else if (
+            columnTypeLower.includes('int') ||
+            columnTypeLower.includes('number') ||
+            columnTypeLower.includes('decimal') ||
+            columnTypeLower.includes('float') ||
+            columnTypeLower.includes('double')
+          ) {
+            type = 'number'
+          } else {
+            type = 'input'
+          }
+        }
+        const required = toBool(
+          getVal(['isRequired', 'is_required', 'required'], false)
+        )
+        const multiple = toBool(
+          getVal(['isMultiple', 'is_multiple', 'multiple'], false)
+        )
+        const colSpan =
+          toNumber(
+            getVal(['colSpan', 'colspan', 'columnSpan', 'column_span'], 12)
+          ) || 12
+        const placeholder =
+          getVal(
+            ['placeholder', 'columnComment', 'column_comment'],
+            undefined
+          ) || (type === 'select' ? `请选择${label}` : `请输入${label}`)
+        const defaultValue = getVal(
+          ['defaultValue', 'default_value', 'defaultVal', 'default_val'],
+          undefined
+        )
+        const precision = toNumber(
+          getVal(
+            ['fieldTypeNointLen', 'field_typenointlen', 'precision'],
+            undefined
+          )
+        )
+        const min = toNumber(getVal(['min'], undefined))
+        const max = toNumber(getVal(['max'], undefined))
+        const format = getVal(['format'], undefined)
+        const valueFormat =
+          getVal(['valueFormat', 'value_format'], undefined) ||
+          (type === 'datetime'
+            ? 'yyyy-MM-dd HH:mm:ss'
+            : type === 'date'
+            ? 'yyyy-MM-dd'
+            : type === 'year'
+            ? 'yyyy'
+            : undefined)
+
+        const formatLength = this.extractLengthFromFormat(format)
+        const rules = this.buildFieldRules({
+          type,
+          label,
+          required,
+          totalLength,
+          decimalLength,
+          formatLength,
+          format,
+          isAuditPeriod,
+        })
+
+        return {
+          prop,
+          label,
+          type,
+          colSpan,
+          placeholder,
+          dictCode,
+          dictType: dictCode,
+          options,
+          required,
+          defaultValue,
+          multiple,
+          precision,
+          min,
+          max,
+          format,
+          valueFormat,
+          totalLength,
+          decimalLength,
+          formatLength,
+          rules,
+        }
+      },
+      extractLengthFromFormat(format) {
+        if (!format) return undefined
+        const str = String(format).trim()
+        if (!str) return undefined
+        const match = str.match(/\d+/)
+        if (match && match[0]) {
+          const len = Number(match[0])
+          return Number.isNaN(len) ? undefined : len
+        }
+        return undefined
+      },
+      buildFieldRules(meta) {
+        const {
+          type,
+          label,
+          required,
+          totalLength,
+          decimalLength,
+          formatLength,
+          format,
+          isAuditPeriod,
+        } = meta || {}
+        const rules = []
+        const trigger = type === 'select' ? 'change' : 'blur'
+        if (required) {
+          rules.push({
+            required: true,
+            message: `${type === 'select' ? '请选择' : '请输入'}${label}`,
+            trigger,
+          })
+        }
+        const inputMaxLength = formatLength || totalLength
+        if (type === 'input' && inputMaxLength) {
+          rules.push({
+            validator: (_, value, callback) => {
+              if (value === undefined || value === null || value === '') {
+                callback()
+                return
+              }
+              const str = String(value)
+              if (str.length > inputMaxLength) {
+                callback(
+                  new Error(`${label}长度不能超过${inputMaxLength}个字符`)
+                )
+              } else {
+                callback()
+              }
+            },
+            trigger: 'blur',
+          })
+        }
+        const numberTotal = totalLength || formatLength
+        if (type === 'number') {
+          rules.push({
+            validator: (_, value, callback) => {
+              if (value === undefined || value === null || value === '') {
+                callback()
+                return
+              }
+              if (Number.isNaN(Number(value))) {
+                callback(new Error(`${label}必须为数字`))
+                return
+              }
+              const pure = String(value).replace('-', '')
+              if (numberTotal && pure.replace('.', '').length > numberTotal) {
+                callback(new Error(`${label}总位数不能超过${numberTotal}`))
+                return
+              }
+              if (decimalLength !== undefined && decimalLength !== null) {
+                const decimals = pure.split('.')[1] || ''
+                if (decimals.length > decimalLength) {
+                  callback(
+                    new Error(`${label}小数位不能超过${decimalLength}位`)
+                  )
+                  return
+                }
+              }
+              callback()
+            },
+            trigger: 'blur',
+          })
+        }
+        if (type === 'datetime' || type === 'date') {
+          if (format) {
+            rules.push({
+              validator: (_, value, callback) => {
+                if (value === undefined || value === null || value === '') {
+                  callback()
+                  return
+                }
+                callback()
+              },
+              trigger: 'change',
+            })
+          }
+        }
+        if (type === 'year' || isAuditPeriod) {
+          rules.push({
+            validator: (_, value, callback) => {
+              if (value === undefined || value === null || value === '') {
+                callback()
+                return
+              }
+              const pattern = /^\d{4}$/
+              if (!pattern.test(String(value))) {
+                callback(new Error(`${label}必须是四位年份`))
+              } else {
+                callback()
+              }
+            },
+            trigger: 'change',
+          })
+        }
+        return rules
+      },
       // 初始化字典数据
       initDictData() {
         if (this.dictTypes.length > 0) {
@@ -276,61 +682,87 @@
       },
       // 获取字典选项
       getDictOptions(dictType) {
-        if (!this.dictData || !this.dictData[dictType]) {
+        if (!dictType || !this.dictData || !this.dictData[dictType]) {
           return []
         }
         return this.dictData[dictType] || []
       },
-      // 初始化表单
       initForm() {
+        const fields = this.effectiveFormFields
+        if (!fields || fields.length === 0) {
+          return
+        }
+
         const form = {}
         const rules = {}
 
-        // 如果没有传入字段配置,使用默认配置
-        const fields =
-          this.formFields && this.formFields.length > 0
-            ? this.formFields
-            : this.getDefaultFormFields()
-
         fields.forEach((field) => {
+          if (!field || !field.prop) {
+            return
+          }
+
           // 初始化表单值
-          // 优先使用传入的详情数据(surveyData 中可能包含从 getSurveyDetail 接口返回的数据)
           if (
             this.surveyData &&
             this.surveyData[field.prop] !== undefined &&
             this.surveyData[field.prop] !== null &&
             this.surveyData[field.prop] !== ''
           ) {
-            // 优先使用传入的数据
             form[field.prop] = this.surveyData[field.prop]
           } else if (field.defaultValue !== undefined) {
-            // 其次使用默认值
             form[field.prop] = field.defaultValue
           } else {
-            // 最后使用空值
             form[field.prop] = field.multiple ? [] : ''
           }
 
-          // 初始化验证规则
-          if (field.rules && Array.isArray(field.rules)) {
-            rules[field.prop] = field.rules
+          // 初始化验证规则 - 这是关键!必须正确设置 rules
+          // 优先使用字段配置中的 rules(从 mapApiFieldToFormField 返回的完整规则)
+          if (
+            field.rules &&
+            Array.isArray(field.rules) &&
+            field.rules.length > 0
+          ) {
+            // 使用完整的规则数组
+            rules[field.prop] = [...field.rules]
           } else if (field.required) {
-            // 如果字段标记为必填,自动添加必填验证
+            // 如果字段是必填的但没有 rules,添加必填验证
+            const message =
+              field.type === 'select'
+                ? `请选择${field.label}`
+                : field.type === 'date' ||
+                  field.type === 'datetime' ||
+                  field.type === 'year'
+                ? `请选择${field.label}`
+                : `请输入${field.label}`
             rules[field.prop] = [
               {
                 required: true,
-                message: `请输入${field.label}`,
-                trigger: field.type === 'select' ? 'change' : 'blur',
+                message: message,
+                trigger:
+                  field.type === 'select' ||
+                  field.type === 'date' ||
+                  field.type === 'datetime' ||
+                  field.type === 'year'
+                    ? 'change'
+                    : 'blur',
               },
             ]
           }
         })
 
-        this.form = form
-        this.rules = rules
+        // 使用 Vue.set 确保响应式更新
+        this.$set(this, 'form', form)
+        this.$set(this, 'rules', rules)
 
         // 初始化字典数据
         this.initDictData()
+
+        // 确保表单组件能识别新的规则
+        this.$nextTick(() => {
+          if (this.$refs.surveyForm) {
+            this.$refs.surveyForm.clearValidate()
+          }
+        })
       },
       // 获取默认表单字段配置(兼容旧版本)
       getDefaultFormFields() {
@@ -438,7 +870,7 @@
               )
 
               // 将表单数据转换为接口需要的格式
-              const saveData = this.formFields.map((field) => {
+              const saveData = this.effectiveFormFields.map((field) => {
                 const dataItem = {
                   auditedUnitId:
                     this.auditedUnitId || this.surveyData.auditedUnitId || '',