Browse Source

fix: 成本调查表单记录校验修改完善

shiyanyu 1 tháng trước cách đây
mục cha
commit
42d1100ff9

+ 358 - 28
src/views/EntDeclaration/auditTaskManagement/components/CostSurveyTab.vue

@@ -232,6 +232,7 @@
     getSurveyDetail,
     getDynamicTableData,
   } from '@/api/audit/survey'
+  import { getListBySurveyTemplateIdAndVersion } from '@/api/costSurveyTemplateHeaders'
 
   export default {
     name: 'CostSurveyTab',
@@ -458,7 +459,7 @@
       },
       // 初始化表单字段配置
       async initFormFields() {
-        // 如果是单记录类型,调用接口获取表单字段配置
+        // 如果是单记录类型,调用接口获取表单字段配置(包含校验规则)
         if (
           this.currentSurveyRow &&
           this.currentSurveyRow.tableType === '单记录' &&
@@ -468,35 +469,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 +540,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 [

+ 99 - 55
src/views/EntDeclaration/auditTaskManagement/components/SurveyFormDialog.vue

@@ -17,11 +17,7 @@
           :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"
@@ -238,9 +234,24 @@
         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,
@@ -298,45 +309,52 @@
         if (!templateId) {
           return
         }
-        this.internalFormFields = []
-        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') {
-              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())
+        // 只有在没有外部字段配置时才获取内部字段
+        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)
-                mapped = labels.map((label, index) => ({
-                  prop: ids[index] || `field_${index}`,
-                  label,
-                  type: 'input',
-                  colSpan: 12,
-                  placeholder: `请输入${label}`,
-                  required: false,
-                }))
+              } 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)
               }
             }
-            if (!hasExternalFields && mapped.length > 0) {
-              this.internalFormFields = mapped
-            }
+          } catch (error) {
+            console.error('获取调查表字段失败:', error)
           }
-        } catch (error) {
-          console.error('获取调查表字段失败:', error)
         }
       },
       mapApiFieldToFormField(item, index = 0) {
@@ -669,56 +687,82 @@
         }
         return this.dictData[dictType] || []
       },
-      // 初始化表单
       initForm() {
+        const fields = this.effectiveFormFields
+        if (!fields || fields.length === 0) {
+          return
+        }
+
         const form = {}
         const rules = {}
 
-        const fields = this.effectiveFormFields
-
         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 ? [] : ''
           }
 
-          // 初始化验证规则
+          // 初始化验证规则 - 这是关键!必须正确设置 rules
+          // 优先使用字段配置中的 rules(从 mapApiFieldToFormField 返回的完整规则)
           if (
             field.rules &&
             Array.isArray(field.rules) &&
             field.rules.length > 0
           ) {
-            rules[field.prop] = field.rules
+            // 使用完整的规则数组
+            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() {