Просмотр исходного кода

fix: 成本调查表加整数校验,关闭后再次打开校验生效

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

+ 242 - 45
src/views/EntDeclaration/auditTaskManagement/components/SurveyFormDialog.vue

@@ -9,7 +9,13 @@
     :modal="false"
     @close="handleClose"
   >
-    <el-form ref="surveyForm" :model="form" :rules="rules" label-width="120px">
+    <el-form
+      :key="formKey"
+      ref="surveyForm"
+      :model="form"
+      :rules="rules"
+      label-width="120px"
+    >
       <el-row :gutter="20">
         <!-- 动态生成表单字段 -->
         <el-col
@@ -17,7 +23,11 @@
           :key="`${field.prop || field.id || 'field'}-${index}`"
           :span="field.colSpan || 12"
         >
-          <el-form-item :label="field.label" :prop="field.prop">
+          <el-form-item
+            :label="field.label"
+            :prop="field.prop"
+            :required="isFieldMarkedRequired(field)"
+          >
             <!-- 文本输入框 -->
             <el-input
               v-if="field.type === 'input' || !field.type"
@@ -216,6 +226,7 @@
         rules: {},
         dictData: {}, // 初始化字典数据对象
         internalFormFields: [],
+        formKey: 0,
       }
     },
     computed: {
@@ -252,15 +263,31 @@
           this.dialogVisible = newVal
           if (newVal) {
             // 弹窗打开时,强制重新获取字段配置并初始化表单
-            // 先清空旧数据,确保重新加载
-            this.internalFormFields = []
+            // 先清空旧表单值与规则,保留字段配置,避免二次打开字段顺序或禁用状态异常
             this.form = {}
             this.rules = {}
+            // 强制表单组件重新渲染,避免上次校验/禁用等内部状态残留
+            this.formKey += 1
             // 等待字段配置加载完成
-            await this.ensureTemplateFields()
+            await this.ensureTemplateFields(true)
             // 确保 DOM 更新后再初始化表单
             await this.$nextTick()
-            this.initForm()
+            this.initForm({ clear: false })
+            // 若使用外部字段(ensureTemplateFields 不会触发校验),此处主动校验一次
+            await this.$nextTick()
+            if (
+              Array.isArray(this.formFields) &&
+              this.formFields.length > 0 &&
+              this.$refs.surveyForm
+            ) {
+              this.$refs.surveyForm.validate(() => {})
+              const props = (this.effectiveFormFields || [])
+                .map((f) => f && f.prop)
+                .filter(Boolean)
+              if (props.length > 0) {
+                this.$refs.surveyForm.validateField(props, () => {})
+              }
+            }
           } else {
             // 关闭弹窗时,清理表单数据但保留字段配置(可选)
             // this.internalFormFields = []
@@ -315,7 +342,37 @@
       this.initDictData()
     },
     methods: {
-      async ensureTemplateFields() {
+      // 用于控制红色必填星标:对必填或包含数值规则的字段都加红色标记
+      isFieldMarkedRequired(field) {
+        if (!field) return false
+        if (field.required) return true
+        const kind = this._detectNumericKind(field)
+        return !!kind
+      },
+      _detectNumericKind(field) {
+        const ft = String(
+          field.fieldType || field.field_type || ''
+        ).toLowerCase()
+        const ct = String(
+          field.columnType || field.column_type || ''
+        ).toLowerCase()
+        const decimalLen =
+          field.decimalLength ||
+          field.fieldTypeNointLen ||
+          field.field_typenointlen
+        if (decimalLen === 0) return 'integer'
+        if (decimalLen > 0) return 'decimal'
+        if (ft === 'integer' || ct.includes('int')) return 'integer'
+        if (
+          ft === 'double' ||
+          ct.includes('decimal') ||
+          ct.includes('float') ||
+          ct.includes('double')
+        )
+          return 'decimal'
+        return undefined
+      },
+      async ensureTemplateFields(force = false) {
         const hasExternalFields =
           Array.isArray(this.formFields) && this.formFields.length > 0
         const templateId =
@@ -326,8 +383,8 @@
         if (!templateId) {
           return
         }
-        // 只有在没有外部字段配置时才获取内部字段
-        if (!hasExternalFields) {
+        // 无外部字段或强制刷新时,获取服务端字段配置
+        if (force || !hasExternalFields) {
           try {
             const params = {
               surveyTemplateId: templateId,
@@ -368,6 +425,21 @@
               // 使用 Vue.set 确保响应式更新
               if (mapped.length > 0) {
                 this.$set(this, 'internalFormFields', mapped)
+                // 接口返回字段后,若弹窗已打开,立即重建表单并触发校验
+                if (this.dialogVisible) {
+                  await this.$nextTick()
+                  this.initForm()
+                  await this.$nextTick()
+                  if (this.$refs.surveyForm) {
+                    this.$refs.surveyForm.validate(() => {})
+                    const props = (this.effectiveFormFields || [])
+                      .map((f) => f && f.prop)
+                      .filter(Boolean)
+                    if (props.length > 0) {
+                      this.$refs.surveyForm.validateField(props, () => {})
+                    }
+                  }
+                }
               }
             }
           } catch (error) {
@@ -437,13 +509,24 @@
         const ftLower = explicitFieldType.toLowerCase()
         const totalLength = toNumber(
           getVal(
-            ['fieldTypeLen', 'field_typelen', 'length', 'fieldLength'],
+            [
+              'fieldTypeLen',
+              'fieldTypelen',
+              'field_typelen',
+              'length',
+              'fieldLength',
+            ],
             undefined
           )
         )
         const decimalLength = toNumber(
           getVal(
-            ['fieldTypeNointLen', 'field_typenointlen', 'scale'],
+            [
+              'fieldTypeNointLen',
+              'fieldTypenointlen',
+              'field_typenointlen',
+              'scale',
+            ],
             undefined
           )
         )
@@ -511,6 +594,9 @@
         const required = toBool(
           getVal(['isRequired', 'is_required', 'required'], false)
         )
+        const disabled = toBool(
+          getVal(['disabled', 'is_disabled', 'isDisabled'], false)
+        )
         const multiple = toBool(
           getVal(['isMultiple', 'is_multiple', 'multiple'], false)
         )
@@ -562,6 +648,16 @@
           formatLength,
           format,
           isAuditPeriod,
+          // 数字类型提示:用于 input 类型时也能按数值规则校验
+          numericKind:
+            ftLower === 'integer'
+              ? 'integer'
+              : ftLower === 'double' ||
+                columnTypeLower.includes('decimal') ||
+                columnTypeLower.includes('float') ||
+                columnTypeLower.includes('double')
+              ? 'decimal'
+              : undefined,
         })
 
         return {
@@ -574,6 +670,7 @@
           dictType: dictCode,
           options,
           required,
+          disabled,
           defaultValue,
           multiple,
           precision,
@@ -609,6 +706,7 @@
           formatLength,
           format,
           isAuditPeriod,
+          numericKind,
         } = meta || {}
         const rules = []
         const trigger = type === 'select' ? 'change' : 'blur'
@@ -639,6 +737,51 @@
             trigger: 'blur',
           })
         }
+        // 针对输入框的数值校验(根据后端字段类型提示)
+        if (type === 'input' && numericKind) {
+          rules.push({
+            validator: (_, value, callback) => {
+              if (value === undefined || value === null || value === '') {
+                callback()
+                return
+              }
+              const str = String(value).trim()
+              if (numericKind === 'integer') {
+                // 仅整数,且位数不得超过 fieldTypelen
+                const intPattern = /^-?\d+$/
+                if (!intPattern.test(str)) {
+                  callback(new Error(`${label}必须为整数`))
+                  return
+                }
+                if (totalLength) {
+                  const digits = str.replace('-', '')
+                  if (digits.length > totalLength) {
+                    callback(new Error(`${label}长度不能超过${totalLength}位`))
+                    return
+                  }
+                }
+              } else if (numericKind === 'decimal') {
+                // 数字,且小数位不得超过 fieldTypenointlen
+                const num = Number(str)
+                if (Number.isNaN(num)) {
+                  callback(new Error(`${label}必须为数字`))
+                  return
+                }
+                if (decimalLength !== undefined && decimalLength !== null) {
+                  const decimals = str.split('.')[1] || ''
+                  if (decimals.length > decimalLength) {
+                    callback(
+                      new Error(`${label}小数位不能超过${decimalLength}位`)
+                    )
+                    return
+                  }
+                }
+              }
+              callback()
+            },
+            trigger: ['blur', 'change'],
+          })
+        }
         const numberTotal = totalLength || formatLength
         if (type === 'number') {
           rules.push({
@@ -698,7 +841,7 @@
                 callback()
               }
             },
-            trigger: 'change',
+            trigger: ['blur', 'change'],
           })
         }
         return rules
@@ -725,7 +868,8 @@
         }
         return this.dictData[dictType] || []
       },
-      initForm() {
+      initForm(opts = {}) {
+        const shouldClear = opts.clear !== false
         const fields = this.effectiveFormFields
         if (!fields || fields.length === 0) {
           return
@@ -753,38 +897,84 @@
             form[field.prop] = field.multiple ? [] : ''
           }
 
-          // 初始化验证规则 - 这是关键!必须正确设置 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: message,
-                trigger:
-                  field.type === 'select' ||
-                  field.type === 'date' ||
-                  field.type === 'datetime' ||
-                  field.type === 'year'
-                    ? 'change'
-                    : 'blur',
-              },
-            ]
+          } else {
+            const ft = String(
+              field.fieldType || field.field_type || ''
+            ).toLowerCase()
+            const ct = String(
+              field.columnType || field.column_type || ''
+            ).toLowerCase()
+            const numericKind =
+              field.decimalLength === 0 ||
+              field.fieldTypeNointLen === 0 ||
+              field.field_typenointlen === 0
+                ? 'integer'
+                : field.decimalLength > 0 ||
+                  field.fieldTypeNointLen > 0 ||
+                  field.field_typenointlen > 0
+                ? 'decimal'
+                : ft === 'integer' || ct.includes('int')
+                ? 'integer'
+                : ft === 'double' ||
+                  ct.includes('decimal') ||
+                  ct.includes('float') ||
+                  ct.includes('double')
+                ? 'decimal'
+                : undefined
+            const totalLength =
+              field.totalLength ||
+              field.fieldTypeLen ||
+              field.fieldTypelen ||
+              field.field_typelen
+            const decimalLength =
+              field.decimalLength ||
+              field.fieldTypeNointLen ||
+              field.fieldTypenointlen ||
+              field.field_typenointlen
+            const formatLength = this.extractLengthFromFormat(field.format)
+            const autoRules = this.buildFieldRules({
+              type: field.type || 'input',
+              label: field.label,
+              required: !!field.required,
+              totalLength,
+              decimalLength,
+              formatLength,
+              format: field.format,
+              isAuditPeriod: !!field.isAuditPeriod,
+              numericKind,
+            })
+            if (autoRules && autoRules.length > 0) {
+              rules[field.prop] = autoRules
+            } else if (field.required) {
+              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: message,
+                  trigger:
+                    field.type === 'select' ||
+                    field.type === 'date' ||
+                    field.type === 'datetime' ||
+                    field.type === 'year'
+                      ? 'change'
+                      : 'blur',
+                },
+              ]
+            }
           }
         })
 
@@ -795,12 +985,14 @@
         // 初始化字典数据
         this.initDictData()
 
-        // 确保表单组件能识别新的规则
-        this.$nextTick(() => {
-          if (this.$refs.surveyForm) {
-            this.$refs.surveyForm.clearValidate()
-          }
-        })
+        // 根据需要清除上次校验状态(默认清除;在自动校验场景下可跳过)
+        if (shouldClear) {
+          this.$nextTick(() => {
+            if (this.$refs.surveyForm) {
+              this.$refs.surveyForm.clearValidate()
+            }
+          })
+        }
       },
       // 获取默认表单字段配置(兼容旧版本)
       getDefaultFormFields() {
@@ -892,6 +1084,11 @@
       handleClose() {
         this.dialogVisible = false
         this.$emit('update:visible', false)
+        if (this.$refs.surveyForm) {
+          // 重置为初始状态,避免下次打开继承内部状态
+          this.$refs.surveyForm.resetFields()
+          this.$refs.surveyForm.clearValidate()
+        }
       },
       handleCancel() {
         this.handleClose()

+ 17 - 16
src/views/costAudit/auditInfo/auditManage/costAudit.vue

@@ -179,12 +179,15 @@
   import {
     getlistBySurveyTemplateId,
     getVerifyTemplateDetail,
-    importExcel,
-    exportExcel,
   } from '@/api/costVerifyManage'
   import { getDetail } from '@/api/auditInitiation'
   import { catalogMixin } from '@/mixins/useDict'
-  import { saveSingleRecordSurvey, getSurveyDetail } from '@/api/audit/survey'
+  import {
+    saveSingleRecordSurvey,
+    getSurveyDetail,
+    downloadTemplate,
+    importData,
+  } from '@/api/audit/survey'
 
   export default {
     name: 'CostAudit',
@@ -520,7 +523,6 @@
       },
       // 单元格输入联动:当账面值或审核调整值变化时,自动计算核定值
       handleCellInput(row, item) {
-        console.log(111)
         if (!row) return
         // 若未传入列信息,则对该行全部年份重算
         if (!item || !item.prop) {
@@ -1140,16 +1142,15 @@
             const formData = new FormData()
             formData.append('file', file)
 
-            // 其他参数作为query参数传递
-            const queryParams = {
-              surveyTemplateId: this.auditForm.surveyTemplateId,
-              taskId: this.selectedProject.taskId,
-              materialId: '', // 根据API文档,此参数为必填,但当前没有值,保留空字符串
-              periodRecordId: '', // 非必填参数
-            }
+            // 其他参数作为表单字段传递
+            formData.append('surveyTemplateId', this.auditForm.surveyTemplateId)
+            formData.append('taskId', this.selectedProject.taskId)
+            formData.append('materialId', '')
+            formData.append('periodRecordId', '')
+            formData.append('type', 3)
 
-            // 先调用上传API,将参数作为query传递
-            const uploadRes = await importExcel(formData, queryParams)
+            // 调用新的导入接口
+            const uploadRes = await importData(formData)
 
             // 第四步:检查上传结果
             if (!uploadRes.state) {
@@ -1182,11 +1183,11 @@
           background: 'rgba(0, 0, 0, 0.7)',
         })
 
-        // 调用导出API
-        exportExcel({
-          // surveyTemplateId: '1985224388517109760', // 测试用  1989165590455066624
+        // 调用新的导出API
+        downloadTemplate({
           surveyTemplateId: this.auditForm.surveyTemplateId,
           versionId: this.tableHeadersRes[0].versionId || '',
+          type: 3,
         })
           .then((res) => {
             // 从响应头获取文件名(如果有)

+ 15 - 10
src/views/costAudit/auditInfo/auditManage/workDraft.vue

@@ -53,7 +53,7 @@
       <el-table-column label="附件" width="120">
         <template slot-scope="scope">
           <el-button
-            v-if="scope.row.attachments && scope.row.attachments.length > 0"
+            v-if="scope.row.attachmentUrl"
             type="text"
             size="small"
             @click="handlePreviewWorkingPaperAttachment(scope.row)"
@@ -666,18 +666,23 @@
       },
 
       handlePreviewWorkingPaperAttachment(row) {
-        const attachments =
-          row.attachments && Array.isArray(row.attachments)
-            ? row.attachments
-            : []
-        if (attachments.length === 0) {
+        const raw = row && row.attachmentUrl
+        if (!raw) {
           this.$message.info('暂无附件')
           return
         }
-        this.$message({
-          type: 'info',
-          message: `预览附件:${attachments.join(', ')}`,
-        })
+        // 支持数组或逗号分隔的字符串
+        let firstUrl = ''
+        if (Array.isArray(raw)) {
+          firstUrl = raw[0]
+        } else if (typeof raw === 'string') {
+          firstUrl = raw.split(',')[0].trim()
+        }
+        if (!firstUrl) {
+          this.$message.info('暂无附件')
+          return
+        }
+        window.open(firstUrl, '_blank')
       },
 
       // 文件上传前验证