|
|
@@ -83,10 +83,12 @@
|
|
|
<el-input
|
|
|
v-if="!scope.row.isCategory"
|
|
|
v-model="scope.row[`year_${year}`]"
|
|
|
+ inputmode="decimal"
|
|
|
:placeholder="`请输入${year}年数据`"
|
|
|
:disabled="isViewMode"
|
|
|
+ style="width: 100%"
|
|
|
+ @input="handleNumericInput(scope.row, year, $event)"
|
|
|
@blur="handleCellBlur(scope.row, year)"
|
|
|
- @input="handleCellInput(scope.row, year)"
|
|
|
/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
@@ -651,6 +653,46 @@
|
|
|
// 实时联动计算
|
|
|
this.recomputeDependentsForRow(row, year)
|
|
|
},
|
|
|
+ // 仅数字输入:按行规则限制小数位;整数不允许小数
|
|
|
+ handleNumericInput(row, year, val) {
|
|
|
+ if (!row) return
|
|
|
+ const rules = (row && row.validateRules) || {}
|
|
|
+ const field = `year_${year}`
|
|
|
+ let s = String(val == null ? '' : val)
|
|
|
+ // 允许负号和小数点,其余去除
|
|
|
+ s = s.replace(/[^0-9+\-\.]/g, '')
|
|
|
+ // 只保留第一个负号在最前
|
|
|
+ s = s.replace(/(?!^)-/g, '')
|
|
|
+ if (s.startsWith('+')) s = s.slice(1)
|
|
|
+ // 多个点只保留第一个
|
|
|
+ const firstDot = s.indexOf('.')
|
|
|
+ if (firstDot >= 0) {
|
|
|
+ s =
|
|
|
+ s.slice(0, firstDot + 1) + s.slice(firstDot + 1).replace(/\./g, '')
|
|
|
+ }
|
|
|
+ const t = String(rules.type || '').toLowerCase()
|
|
|
+ const isInteger =
|
|
|
+ t === 'integer' ||
|
|
|
+ (t === 'number' &&
|
|
|
+ (rules.fieldTypenointlen === 0 || rules.decimalLength === 0))
|
|
|
+ if (isInteger && s.includes('.')) {
|
|
|
+ s = s.split('.')[0]
|
|
|
+ }
|
|
|
+ if (!isInteger) {
|
|
|
+ const dlen =
|
|
|
+ rules.fieldTypenointlen ||
|
|
|
+ rules.fieldTypeNointLen ||
|
|
|
+ rules.decimalLength
|
|
|
+ if (typeof dlen === 'number' && dlen >= 0 && firstDot >= 0) {
|
|
|
+ const parts = s.split('.')
|
|
|
+ parts[1] = (parts[1] || '').slice(0, dlen)
|
|
|
+ s = parts.join('.')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 写回且触发联动
|
|
|
+ this.$set(row, field, s)
|
|
|
+ this.handleCellInput(row, year)
|
|
|
+ },
|
|
|
// 单元格失焦事件
|
|
|
handleCellBlur(row, year) {
|
|
|
// 验证格式和非空
|
|
|
@@ -675,37 +717,133 @@
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
- // 格式验证
|
|
|
- if (value && row.validateRules) {
|
|
|
- if (row.validateRules.type === 'number') {
|
|
|
- const numValue = Number(value)
|
|
|
- if (isNaN(numValue)) {
|
|
|
+ // 类型/规则验证:支持 integer/decimal/number/boolean/date/dict
|
|
|
+ if (
|
|
|
+ value !== undefined &&
|
|
|
+ value !== null &&
|
|
|
+ value !== '' &&
|
|
|
+ row.validateRules
|
|
|
+ ) {
|
|
|
+ const r = row.validateRules || {}
|
|
|
+ const rawStr = String(value).trim()
|
|
|
+ const type = String(r.type || '').toLowerCase()
|
|
|
+
|
|
|
+ // 数值范围通用检查
|
|
|
+ const checkRange = (numVal) => {
|
|
|
+ if (r.min !== undefined && numVal < r.min) {
|
|
|
this.showFieldError(
|
|
|
row,
|
|
|
year,
|
|
|
- `${row.itemName}的${year}年数据必须是数字`
|
|
|
+ `${row.itemName}的${year}年数据不能小于${r.min}`
|
|
|
)
|
|
|
return false
|
|
|
}
|
|
|
- if (
|
|
|
- row.validateRules.min !== undefined &&
|
|
|
- numValue < row.validateRules.min
|
|
|
- ) {
|
|
|
+ if (r.max !== undefined && numVal > r.max) {
|
|
|
this.showFieldError(
|
|
|
row,
|
|
|
year,
|
|
|
- `${row.itemName}的${year}年数据不能小于${row.validateRules.min}`
|
|
|
+ `${row.itemName}的${year}年数据不能大于${r.max}`
|
|
|
)
|
|
|
return false
|
|
|
}
|
|
|
- if (
|
|
|
- row.validateRules.max !== undefined &&
|
|
|
- numValue > row.validateRules.max
|
|
|
- ) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ type === 'integer' ||
|
|
|
+ (type === 'number' &&
|
|
|
+ (r.fieldTypenointlen === 0 || r.decimalLength === 0))
|
|
|
+ ) {
|
|
|
+ // 仅整数;可选长度限制 fieldTypelen/fieldTypelen 别名
|
|
|
+ const intPattern = /^-?\d+$/
|
|
|
+ if (!intPattern.test(rawStr)) {
|
|
|
+ this.showFieldError(
|
|
|
+ row,
|
|
|
+ year,
|
|
|
+ `${row.itemName}的${year}年数据必须为整数`
|
|
|
+ )
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ const limit = r.fieldTypelen || r.fieldTypeLen || r.totalLength
|
|
|
+ if (limit && rawStr.replace('-', '').length > Number(limit)) {
|
|
|
+ this.showFieldError(
|
|
|
+ row,
|
|
|
+ year,
|
|
|
+ `${row.itemName}的${year}年整数长度不能超过${limit}位`
|
|
|
+ )
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ const numVal = Number(rawStr)
|
|
|
+ if (!checkRange(numVal)) return false
|
|
|
+ } else if (
|
|
|
+ type === 'decimal' ||
|
|
|
+ type === 'double' ||
|
|
|
+ type === 'float' ||
|
|
|
+ type === 'number'
|
|
|
+ ) {
|
|
|
+ const numVal = Number(rawStr)
|
|
|
+ if (Number.isNaN(numVal)) {
|
|
|
+ this.showFieldError(
|
|
|
+ row,
|
|
|
+ year,
|
|
|
+ `${row.itemName}的${year}年数据必须为数字`
|
|
|
+ )
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ const dlen =
|
|
|
+ r.fieldTypenointlen || r.fieldTypeNointLen || r.decimalLength
|
|
|
+ if (dlen !== undefined && dlen !== null) {
|
|
|
+ const decimals = rawStr.split('.')[1] || ''
|
|
|
+ if (decimals.length > Number(dlen)) {
|
|
|
+ this.showFieldError(
|
|
|
+ row,
|
|
|
+ year,
|
|
|
+ `${row.itemName}的${year}年数据小数位不能超过${dlen}位`
|
|
|
+ )
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!checkRange(numVal)) return false
|
|
|
+ } else if (type === 'boolean' || type === 'bool') {
|
|
|
+ const ok = [
|
|
|
+ 'true',
|
|
|
+ 'false',
|
|
|
+ '1',
|
|
|
+ '0',
|
|
|
+ '是',
|
|
|
+ '否',
|
|
|
+ 'Y',
|
|
|
+ 'N',
|
|
|
+ 'y',
|
|
|
+ 'n',
|
|
|
+ ].includes(rawStr)
|
|
|
+ if (!ok) {
|
|
|
+ this.showFieldError(
|
|
|
+ row,
|
|
|
+ year,
|
|
|
+ `${row.itemName}的${year}年数据必须为布尔值(是/否/true/false/1/0)`
|
|
|
+ )
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ } else if (type === 'date' || type === 'datetime' || r.dateFormat) {
|
|
|
+ const fmt =
|
|
|
+ r.dateFormat ||
|
|
|
+ (type === 'datetime' ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd')
|
|
|
+ if (!this.validateDateByFormat(rawStr, fmt)) {
|
|
|
+ this.showFieldError(
|
|
|
+ row,
|
|
|
+ year,
|
|
|
+ `${row.itemName}的${year}年数据不符合日期格式 ${fmt}`
|
|
|
+ )
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ } else if (type === 'dict' || r.allowedValues) {
|
|
|
+ const arr = Array.isArray(r.allowedValues) ? r.allowedValues : []
|
|
|
+ if (arr.length > 0 && !arr.includes(rawStr)) {
|
|
|
this.showFieldError(
|
|
|
row,
|
|
|
year,
|
|
|
- `${row.itemName}的${year}年数据不能大于${row.validateRules.max}`
|
|
|
+ `${row.itemName}的${year}年数据必须为字典项之一`
|
|
|
)
|
|
|
return false
|
|
|
}
|
|
|
@@ -810,6 +948,77 @@
|
|
|
})
|
|
|
})
|
|
|
},
|
|
|
+ // 简单日期校验(支持 yyyy-MM-dd 与 yyyy-MM-dd HH:mm:ss)
|
|
|
+ validateDateByFormat(value, format) {
|
|
|
+ const v = String(value).trim()
|
|
|
+ if (!v) return false
|
|
|
+ if (format === 'yyyy-MM-dd') {
|
|
|
+ const m = v.match(/^(\d{4})-(\d{2})-(\d{2})$/)
|
|
|
+ if (!m) return false
|
|
|
+ const y = +m[1],
|
|
|
+ mo = +m[2],
|
|
|
+ d = +m[3]
|
|
|
+ const dt = new Date(y, mo - 1, d)
|
|
|
+ return (
|
|
|
+ dt.getFullYear() === y &&
|
|
|
+ dt.getMonth() === mo - 1 &&
|
|
|
+ dt.getDate() === d
|
|
|
+ )
|
|
|
+ }
|
|
|
+ if (format === 'yyyy-MM-dd HH:mm:ss') {
|
|
|
+ const m = v.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/)
|
|
|
+ if (!m) return false
|
|
|
+ const y = +m[1],
|
|
|
+ mo = +m[2],
|
|
|
+ d = +m[3],
|
|
|
+ hh = +m[4],
|
|
|
+ mm = +m[5],
|
|
|
+ ss = +m[6]
|
|
|
+ const dt = new Date(y, mo - 1, d, hh, mm, ss)
|
|
|
+ return (
|
|
|
+ dt.getFullYear() === y &&
|
|
|
+ dt.getMonth() === mo - 1 &&
|
|
|
+ dt.getDate() === d &&
|
|
|
+ dt.getHours() === hh &&
|
|
|
+ dt.getMinutes() === mm &&
|
|
|
+ dt.getSeconds() === ss
|
|
|
+ )
|
|
|
+ }
|
|
|
+ // 其它格式简单兜底:只要不是空
|
|
|
+ return !!v
|
|
|
+ },
|
|
|
+ // ===== 数字输入控件参数推导 =====
|
|
|
+ getPrecision(row) {
|
|
|
+ const r = (row && row.validateRules) || {}
|
|
|
+ const t = String(r.type || '').toLowerCase()
|
|
|
+ // integer 或 number 且小数位为 0
|
|
|
+ if (
|
|
|
+ t === 'integer' ||
|
|
|
+ (t === 'number' &&
|
|
|
+ (r.fieldTypenointlen === 0 || r.decimalLength === 0))
|
|
|
+ ) {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+ const d = r.fieldTypenointlen || r.fieldTypeNointLen || r.decimalLength
|
|
|
+ if (d === 0) return 0
|
|
|
+ if (typeof d === 'number') return d
|
|
|
+ return undefined
|
|
|
+ },
|
|
|
+ getStep(row) {
|
|
|
+ const p = this.getPrecision(row)
|
|
|
+ if (p === 0) return 1
|
|
|
+ if (typeof p === 'number' && p > 0)
|
|
|
+ return Number((1 / Math.pow(10, p)).toFixed(p))
|
|
|
+ return 1
|
|
|
+ },
|
|
|
+ getMin(row) {
|
|
|
+ const r = (row && row.validateRules) || {}
|
|
|
+ return typeof r.min === 'number' ? r.min : undefined
|
|
|
+ },
|
|
|
+ getMax(row) {
|
|
|
+ const r = (row && row.validateRules) || {}
|
|
|
+ return typeof r.max === 'number' ? r.max : undefined
|
|
|
+ },
|
|
|
// 关闭弹窗
|
|
|
handleClose() {
|
|
|
this.dialogVisible = false
|