|
|
@@ -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()
|