|
|
@@ -16,14 +16,14 @@
|
|
|
:row-class-name="getRowClassName"
|
|
|
>
|
|
|
<!-- 序号列 -->
|
|
|
- <el-table-column prop="seq" label="序号" width="80" align="center">
|
|
|
+ <!-- <el-table-column prop="seq" label="序号" width="80" align="center">
|
|
|
<template slot-scope="scope">
|
|
|
<span>{{ scope.row.seq }}</span>
|
|
|
</template>
|
|
|
- </el-table-column>
|
|
|
+ </el-table-column> -->
|
|
|
|
|
|
<!-- 项目列(只读) -->
|
|
|
- <el-table-column
|
|
|
+ <!-- <el-table-column
|
|
|
prop="itemName"
|
|
|
label="项目"
|
|
|
min-width="200"
|
|
|
@@ -35,13 +35,95 @@
|
|
|
</span>
|
|
|
<span v-else>{{ scope.row.itemName }}</span>
|
|
|
</template>
|
|
|
- </el-table-column>
|
|
|
+ </el-table-column> -->
|
|
|
|
|
|
<!-- 单位列(只读) -->
|
|
|
- <el-table-column prop="unit" label="单位" width="100" align="center">
|
|
|
+ <!-- <el-table-column prop="unit" label="单位" width="100" align="center">
|
|
|
<template slot-scope="scope">
|
|
|
<span v-if="!scope.row.isCategory">{{ scope.row.unit }}</span>
|
|
|
</template>
|
|
|
+ </el-table-column> -->
|
|
|
+
|
|
|
+ <!-- 固定表头(来自父组件 fixedFields) -->
|
|
|
+ <el-table-column
|
|
|
+ v-for="label in dynamicColumns()"
|
|
|
+ :key="`fixed-col-${label}`"
|
|
|
+ :label="label"
|
|
|
+ :prop="label"
|
|
|
+ :min-width="140"
|
|
|
+ align="center"
|
|
|
+ >
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-date-picker
|
|
|
+ v-if="!scope.row.isCategory && getDateType(label) === 'date'"
|
|
|
+ v-model="scope.row[label]"
|
|
|
+ type="date"
|
|
|
+ :placeholder="`请选择${label}`"
|
|
|
+ size="mini"
|
|
|
+ :format="getDateFormat(label) || 'yyyy-MM-dd'"
|
|
|
+ :value-format="getDateFormat(label) || 'yyyy-MM-dd'"
|
|
|
+ style="width: 100%"
|
|
|
+ :disabled="isViewMode"
|
|
|
+ />
|
|
|
+ <el-date-picker
|
|
|
+ v-else-if="
|
|
|
+ !scope.row.isCategory && getDateType(label) === 'datetime'
|
|
|
+ "
|
|
|
+ v-model="scope.row[label]"
|
|
|
+ type="datetime"
|
|
|
+ :placeholder="`请选择${label}`"
|
|
|
+ size="mini"
|
|
|
+ :format="getDateFormat(label) || 'yyyy-MM-dd HH:mm:ss'"
|
|
|
+ :value-format="getDateFormat(label) || 'yyyy-MM-dd HH:mm:ss'"
|
|
|
+ style="width: 100%"
|
|
|
+ :disabled="isViewMode"
|
|
|
+ />
|
|
|
+ <el-date-picker
|
|
|
+ v-else-if="!scope.row.isCategory && getDateType(label) === 'year'"
|
|
|
+ v-model="scope.row[label]"
|
|
|
+ type="year"
|
|
|
+ :placeholder="`请选择${label}`"
|
|
|
+ size="mini"
|
|
|
+ :format="getDateFormat(label) || 'yyyy'"
|
|
|
+ :value-format="getDateFormat(label) || 'yyyy'"
|
|
|
+ style="width: 100%"
|
|
|
+ :disabled="isViewMode"
|
|
|
+ />
|
|
|
+ <el-switch
|
|
|
+ v-else-if="!scope.row.isCategory && isBooleanField(label)"
|
|
|
+ v-model="scope.row[label]"
|
|
|
+ :disabled="isViewMode"
|
|
|
+ active-value="1"
|
|
|
+ inactive-value="0"
|
|
|
+ active-text="是"
|
|
|
+ inactive-text="否"
|
|
|
+ />
|
|
|
+ <el-select
|
|
|
+ v-else-if="!scope.row.isCategory && isDictField(label)"
|
|
|
+ v-model="scope.row[label]"
|
|
|
+ :placeholder="`请选择${label}`"
|
|
|
+ size="mini"
|
|
|
+ style="width: 100%"
|
|
|
+ :disabled="isViewMode"
|
|
|
+ :clearable="true"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in getDictOptions(getDictCode(label))"
|
|
|
+ :key="item.key || item.value"
|
|
|
+ :label="item.name || item.label"
|
|
|
+ :value="item.key || item.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ <el-input
|
|
|
+ v-else
|
|
|
+ v-model="scope.row[label]"
|
|
|
+ :placeholder="`请输入${label}`"
|
|
|
+ :disabled="isViewMode"
|
|
|
+ size="mini"
|
|
|
+ :maxlength="getMaxLength(label)"
|
|
|
+ @input="onFixedFieldInput(label, scope.row, $event)"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
</el-table-column>
|
|
|
|
|
|
<!-- 动态年份列 -->
|
|
|
@@ -117,9 +199,12 @@
|
|
|
<script>
|
|
|
import { Message } from 'element-ui'
|
|
|
import { saveSingleRecordSurvey, getSurveyDetail } from '@/api/audit/survey'
|
|
|
+ import { getByTypeKey } from '@/api/dictionaryManage'
|
|
|
+ import { dictMixin } from '@/mixins/useDict'
|
|
|
|
|
|
export default {
|
|
|
name: 'FixedTableDialog',
|
|
|
+ mixins: [dictMixin],
|
|
|
props: {
|
|
|
visible: {
|
|
|
type: Boolean,
|
|
|
@@ -175,7 +260,7 @@
|
|
|
},
|
|
|
// 立项信息中的监审期间(字符串,如 '2022,2023,2024' 或 '2022-2024')
|
|
|
projectAuditPeriod: {
|
|
|
- type: String,
|
|
|
+ type: [String, Array],
|
|
|
default: '',
|
|
|
},
|
|
|
// 是否查看模式
|
|
|
@@ -213,6 +298,21 @@
|
|
|
type: [String, Number],
|
|
|
default: '',
|
|
|
},
|
|
|
+ // 从父组件传入的固定表头(逗号分隔)
|
|
|
+ fixedFields: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+ // 从父组件传入的固定表头字段ID(逗号分隔,可缺省)
|
|
|
+ fixedFieldids: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+ // 表头/校验元数据
|
|
|
+ columnsMeta: {
|
|
|
+ type: Array,
|
|
|
+ default: () => [],
|
|
|
+ },
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
@@ -222,6 +322,8 @@
|
|
|
validationErrors: [],
|
|
|
cellCodeIndex: {}, // cellCode -> row
|
|
|
dependentsMap: {}, // cellCode -> Set(rows depending on it)
|
|
|
+ // 字典数据容器(供 dictMixin 拉取并本地缓存)
|
|
|
+ dictData: {},
|
|
|
}
|
|
|
},
|
|
|
watch: {
|
|
|
@@ -229,6 +331,10 @@
|
|
|
async handler(newVal) {
|
|
|
this.dialogVisible = newVal
|
|
|
if (newVal) {
|
|
|
+ // 打印父组件传入的 fixedFields
|
|
|
+ console.log('FixedTableDialog fixedFields:', this.fixedFields)
|
|
|
+ // 打印父组件传入的 surveyData
|
|
|
+ console.log('FixedTableDialog surveyData:', this.surveyData)
|
|
|
// 先初始化表格数据
|
|
|
this.initTableData()
|
|
|
// 等待 DOM 更新后,如果有 uploadId,调用接口获取详情数据并回显
|
|
|
@@ -275,11 +381,339 @@
|
|
|
this.initTableData()
|
|
|
}
|
|
|
},
|
|
|
+ // 预加载字典选项:扫描元数据中的 dictCode 等字段(优先 surveyData.fixedHeaders)
|
|
|
+ columnsMeta: {
|
|
|
+ handler() {
|
|
|
+ this.preloadDicts()
|
|
|
+ },
|
|
|
+ immediate: true,
|
|
|
+ deep: true,
|
|
|
+ },
|
|
|
+ surveyData: {
|
|
|
+ handler() {
|
|
|
+ this.preloadDicts()
|
|
|
+ },
|
|
|
+ deep: true,
|
|
|
+ },
|
|
|
+ // 字典数据加载完成后,尝试将已存在的单元格值从“名称”规范化为“字典值/键”,以便下拉正确回显
|
|
|
+ dictData: {
|
|
|
+ handler() {
|
|
|
+ this.normalizeAllDictValues()
|
|
|
+ },
|
|
|
+ deep: true,
|
|
|
+ },
|
|
|
},
|
|
|
mounted() {
|
|
|
this.initYearColumns()
|
|
|
},
|
|
|
methods: {
|
|
|
+ // 获取字典选项(与 FixedAssetsTable.vue 一致)
|
|
|
+ getDictOptions(dictType) {
|
|
|
+ if (!dictType || !this.dictData) return []
|
|
|
+ const key = String(dictType)
|
|
|
+ const arr = this.dictData[key]
|
|
|
+ return Array.isArray(arr) ? arr : []
|
|
|
+ },
|
|
|
+ // 元数据统一出口:优先使用 surveyData.fixedHeaders,其次 columnsMeta
|
|
|
+ metaList() {
|
|
|
+ const fromSurvey =
|
|
|
+ this.surveyData && Array.isArray(this.surveyData.fixedHeaders)
|
|
|
+ ? this.surveyData.fixedHeaders
|
|
|
+ : null
|
|
|
+ if (fromSurvey) return fromSurvey
|
|
|
+ return Array.isArray(this.columnsMeta) ? this.columnsMeta : []
|
|
|
+ },
|
|
|
+ preloadDicts() {
|
|
|
+ const metas = this.metaList()
|
|
|
+ const codes = new Set()
|
|
|
+ metas.forEach((m) => {
|
|
|
+ const code =
|
|
|
+ (m && m.dictCode && String(m.dictCode).trim()) ||
|
|
|
+ (m && m.dictType && String(m.dictType).trim()) ||
|
|
|
+ (m && m.typeKey && String(m.typeKey).trim()) ||
|
|
|
+ (m && m.dictId && String(m.dictId).trim()) ||
|
|
|
+ (m && m.dictid && String(m.dictid).trim()) ||
|
|
|
+ ''
|
|
|
+ if (code) codes.add(code)
|
|
|
+ })
|
|
|
+ if (codes.size > 0) {
|
|
|
+ if (!this.dictData) this.dictData = {}
|
|
|
+ const list = Array.from(codes)
|
|
|
+ // 先本地初始化,保证 getDictOptions 可用
|
|
|
+ list.forEach((k) => {
|
|
|
+ if (!this.dictData[k]) this.$set(this.dictData, k, [])
|
|
|
+ })
|
|
|
+ // 直接逐个拉取,兼容接口返回 { value: [] }
|
|
|
+ Promise.all(
|
|
|
+ list.map((typeKey) =>
|
|
|
+ getByTypeKey({ typeKey })
|
|
|
+ .then((res) => {
|
|
|
+ const arr = Array.isArray(res)
|
|
|
+ ? res
|
|
|
+ : Array.isArray(res?.value)
|
|
|
+ ? res.value
|
|
|
+ : []
|
|
|
+ this.$set(this.dictData, typeKey, arr)
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ // 出错保持为空数组
|
|
|
+ this.$set(this.dictData, typeKey, [])
|
|
|
+ })
|
|
|
+ )
|
|
|
+ ).then(() => {
|
|
|
+ // 拉取完成后触发一次规范化回显
|
|
|
+ this.normalizeAllDictValues()
|
|
|
+ })
|
|
|
+ // 同时调用 mixin 的批量方法作为兜底(部分环境可能返回数组)
|
|
|
+ if (typeof this.getDictType === 'function') {
|
|
|
+ this.getDictType()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 由父组件传入的 fixedFields 生成表头标签数组
|
|
|
+ dynamicColumns() {
|
|
|
+ if (!this.fixedFields) return []
|
|
|
+ const labels = String(this.fixedFields)
|
|
|
+ .split(',')
|
|
|
+ .map((s) => s.trim())
|
|
|
+ .filter(Boolean)
|
|
|
+ // union: 追加任何 meta 中 (showVisible==='1' 或 有 dictCode) 的列
|
|
|
+ const metas = this.metaList()
|
|
|
+ const extras = []
|
|
|
+ metas.forEach((m) => {
|
|
|
+ const label = String(m?.label || m?.fieldName || m?.name || '').trim()
|
|
|
+ if (!label) return
|
|
|
+ const hasDict = !!(
|
|
|
+ (m.dictCode && String(m.dictCode).trim()) ||
|
|
|
+ (m.dictType && String(m.dictType).trim()) ||
|
|
|
+ (m.typeKey && String(m.typeKey).trim()) ||
|
|
|
+ (m.dictId && String(m.dictId).trim()) ||
|
|
|
+ (m.dictid && String(m.dictid).trim())
|
|
|
+ )
|
|
|
+ const sv =
|
|
|
+ m.showVisible != null && String(m.showVisible).trim() === '1'
|
|
|
+ if (
|
|
|
+ (hasDict || sv) &&
|
|
|
+ !labels.includes(label) &&
|
|
|
+ !extras.includes(label)
|
|
|
+ ) {
|
|
|
+ extras.push(label)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ const base = labels.concat(extras)
|
|
|
+ // 基于元数据控制显示:showVisible==='1' 强制显示;否则当 isAuditPeriod==='false' 隐藏
|
|
|
+ const filtered = base.filter((label) => {
|
|
|
+ const m = this.getFieldMeta(label)
|
|
|
+ if (!m) return true
|
|
|
+ // 强制显示优先
|
|
|
+ const sv = m.showVisible
|
|
|
+ if (sv != null && String(sv).trim() === '1') return true
|
|
|
+ const v = m.isAuditPeriod
|
|
|
+ if (v === undefined || v === null || v === '') return true
|
|
|
+ const str = String(v).trim().toLowerCase()
|
|
|
+ return !(str === 'false' || str === '0' || str === 'no')
|
|
|
+ })
|
|
|
+ // 若全部被过滤导致空表头,则回退为全部显示,避免页面空白
|
|
|
+ return filtered.length > 0 ? filtered : base
|
|
|
+ },
|
|
|
+ // ====== fixedFields 渲染辅助(基于 columnsMeta)======
|
|
|
+ getFieldMeta(label) {
|
|
|
+ const metas = this.metaList()
|
|
|
+ const key = String(label || '').trim()
|
|
|
+ if (!key) return null
|
|
|
+ return (
|
|
|
+ metas.find((m) => String(m?.label || '').trim() === key) ||
|
|
|
+ metas.find(
|
|
|
+ (m) => String(m?.fieldName || m?.name || '').trim() === key
|
|
|
+ ) ||
|
|
|
+ null
|
|
|
+ )
|
|
|
+ },
|
|
|
+ isBooleanField(label) {
|
|
|
+ const meta = this.getFieldMeta(label)
|
|
|
+ if (!meta) return false
|
|
|
+ const t = String(meta.fieldType || meta.type || '').toLowerCase()
|
|
|
+ return t === 'boolean' || t === 'bool'
|
|
|
+ },
|
|
|
+ toYesNo(val) {
|
|
|
+ const s = String(val == null ? '' : val)
|
|
|
+ .trim()
|
|
|
+ .toLowerCase()
|
|
|
+ if (!s) return ''
|
|
|
+ const yes = ['1', 'true', 'y', 'yes', '是']
|
|
|
+ const no = ['0', 'false', 'n', 'no', '否']
|
|
|
+ if (yes.includes(s)) return '是'
|
|
|
+ if (no.includes(s)) return '否'
|
|
|
+ return s
|
|
|
+ },
|
|
|
+ getDateType(label) {
|
|
|
+ const meta = this.getFieldMeta(label)
|
|
|
+ if (!meta) return ''
|
|
|
+ const fmtRaw = meta.format ? String(meta.format).trim() : ''
|
|
|
+ const fmtL = fmtRaw.toLowerCase()
|
|
|
+ if (fmtRaw) {
|
|
|
+ if (/(hh|hh:mm|hh:mm:ss)/i.test(fmtRaw) || fmtL.includes('hh'))
|
|
|
+ return 'datetime'
|
|
|
+ if (/^y{4}$/i.test(fmtRaw)) return 'year'
|
|
|
+ if (
|
|
|
+ (/y{4}/i.test(fmtRaw) && /m{2}/i.test(fmtRaw)) ||
|
|
|
+ fmtL.includes('yyyy-mm')
|
|
|
+ )
|
|
|
+ return 'date'
|
|
|
+ }
|
|
|
+ const t = String(meta.type || meta.fieldType || '').toLowerCase()
|
|
|
+ if (t.includes('datetime') || t.includes('timestamp')) return 'datetime'
|
|
|
+ if (t.includes('date') && !t.includes('datetime')) return 'date'
|
|
|
+ if (t.includes('year')) return 'year'
|
|
|
+ return ''
|
|
|
+ },
|
|
|
+ getDateFormat(label) {
|
|
|
+ const meta = this.getFieldMeta(label)
|
|
|
+ if (!meta) return ''
|
|
|
+ const type = this.getDateType(label)
|
|
|
+ const fmt = meta.format ? String(meta.format).trim() : ''
|
|
|
+ if (type === 'year') return fmt || 'yyyy'
|
|
|
+ if (type === 'datetime') return fmt || 'yyyy-MM-dd HH:mm:ss'
|
|
|
+ if (type === 'date') return fmt || 'yyyy-MM-dd'
|
|
|
+ return ''
|
|
|
+ },
|
|
|
+ isDictField(label) {
|
|
|
+ const meta = this.getFieldMeta(label)
|
|
|
+ if (!meta) return false
|
|
|
+ const dictCode =
|
|
|
+ (meta.dictCode && String(meta.dictCode).trim()) ||
|
|
|
+ (meta.dictType && String(meta.dictType).trim()) ||
|
|
|
+ (meta.typeKey && String(meta.typeKey).trim()) ||
|
|
|
+ (meta.dictId && String(meta.dictId).trim()) ||
|
|
|
+ (meta.dictid && String(meta.dictid).trim()) ||
|
|
|
+ ''
|
|
|
+ return !!dictCode
|
|
|
+ },
|
|
|
+ getFieldDictOptions(label) {
|
|
|
+ const meta = this.getFieldMeta(label)
|
|
|
+ if (!meta) return []
|
|
|
+ // 优先字典接口
|
|
|
+ const code =
|
|
|
+ (meta.dictCode && String(meta.dictCode).trim()) ||
|
|
|
+ (meta.dictType && String(meta.dictType).trim()) ||
|
|
|
+ (meta.typeKey && String(meta.typeKey).trim()) ||
|
|
|
+ (meta.dictId && String(meta.dictId).trim()) ||
|
|
|
+ (meta.dictid && String(meta.dictid).trim()) ||
|
|
|
+ ''
|
|
|
+ if (code && typeof this.getDictOptions === 'function') {
|
|
|
+ const list = this.getDictOptions(code)
|
|
|
+ if (Array.isArray(list) && list.length > 0) return list
|
|
|
+ }
|
|
|
+ const opts = meta.options
|
|
|
+ return Array.isArray(opts) ? opts : []
|
|
|
+ },
|
|
|
+ // 取某列的字典编码(供模板渲染时使用)
|
|
|
+ getDictCode(label) {
|
|
|
+ const meta = this.getFieldMeta(label)
|
|
|
+ if (!meta) return ''
|
|
|
+ return (
|
|
|
+ (meta.dictCode && String(meta.dictCode).trim()) ||
|
|
|
+ (meta.dictType && String(meta.dictType).trim()) ||
|
|
|
+ (meta.typeKey && String(meta.typeKey).trim()) ||
|
|
|
+ (meta.dictId && String(meta.dictId).trim()) ||
|
|
|
+ (meta.dictid && String(meta.dictid).trim()) ||
|
|
|
+ ''
|
|
|
+ )
|
|
|
+ },
|
|
|
+ // 将特定 label 列上已有的值,若匹配到字典项的 name/label,则替换为该项的 key/value
|
|
|
+ normalizeDictValuesForLabel(label) {
|
|
|
+ if (!label || !Array.isArray(this.tableData)) return
|
|
|
+ if (!this.isDictField(label)) return
|
|
|
+ const code = this.getDictCode(label)
|
|
|
+ const opts = code
|
|
|
+ ? this.getDictOptions(code)
|
|
|
+ : this.getFieldDictOptions(label)
|
|
|
+ if (!Array.isArray(opts) || opts.length === 0) return
|
|
|
+ const nameToValue = new Map()
|
|
|
+ const nameToKey = new Map()
|
|
|
+ const valueSet = new Set()
|
|
|
+ const keySet = new Set()
|
|
|
+ opts.forEach((o) => {
|
|
|
+ const key = o && (o.key != null ? String(o.key) : undefined)
|
|
|
+ const val = o && (o.value != null ? String(o.value) : undefined)
|
|
|
+ const name = o && (o.name != null ? String(o.name) : undefined)
|
|
|
+ const labelTxt = o && (o.label != null ? String(o.label) : undefined)
|
|
|
+ if (name) {
|
|
|
+ if (val != null) nameToValue.set(name, val)
|
|
|
+ if (key != null) nameToKey.set(name, key)
|
|
|
+ }
|
|
|
+ if (labelTxt) {
|
|
|
+ if (val != null) nameToValue.set(labelTxt, val)
|
|
|
+ if (key != null) nameToKey.set(labelTxt, key)
|
|
|
+ }
|
|
|
+ if (val != null) valueSet.add(val)
|
|
|
+ if (key != null) keySet.add(key)
|
|
|
+ })
|
|
|
+ this.tableData.forEach((row) => {
|
|
|
+ if (!row || row.isCategory) return
|
|
|
+ const cur = row[label]
|
|
|
+ if (cur == null || cur === '') return
|
|
|
+ const curStr = String(cur)
|
|
|
+ if (valueSet.has(curStr) || keySet.has(curStr)) return
|
|
|
+ const mappedVal = nameToValue.get(curStr)
|
|
|
+ const mappedKey = nameToKey.get(curStr)
|
|
|
+ if (mappedVal != null) {
|
|
|
+ this.$set(row, label, mappedVal)
|
|
|
+ } else if (mappedKey != null) {
|
|
|
+ this.$set(row, label, mappedKey)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 规范化所有字典列的值
|
|
|
+ normalizeAllDictValues() {
|
|
|
+ const labels = this.dynamicColumns()
|
|
|
+ if (!Array.isArray(labels)) return
|
|
|
+ labels.forEach((label) => this.normalizeDictValuesForLabel(label))
|
|
|
+ },
|
|
|
+ getMaxLength(label) {
|
|
|
+ const meta = this.getFieldMeta(label)
|
|
|
+ if (!meta) return undefined
|
|
|
+ const fmt = meta.format != null ? String(meta.format).trim() : ''
|
|
|
+ if (/^\d+$/.test(fmt)) return parseInt(fmt)
|
|
|
+ return undefined
|
|
|
+ },
|
|
|
+ onFixedFieldInput(label, row, val) {
|
|
|
+ const meta = this.getFieldMeta(label)
|
|
|
+ if (!meta || row == null) return
|
|
|
+ let s = String(val == null ? '' : val)
|
|
|
+ const t = String(meta.fieldType || meta.type || '').toLowerCase()
|
|
|
+ if (t === 'integer') {
|
|
|
+ s = s.replace(/[^0-9\-]/g, '')
|
|
|
+ s = s.replace(/(?!^)-/g, '')
|
|
|
+ const maxInt = parseInt(meta.fieldTypelen)
|
|
|
+ if (!isNaN(maxInt) && maxInt > 0) {
|
|
|
+ const neg = s.startsWith('-')
|
|
|
+ const digits = neg ? s.slice(1) : s
|
|
|
+ const clipped = digits.slice(0, maxInt)
|
|
|
+ s = neg ? '-' + clipped : clipped
|
|
|
+ }
|
|
|
+ } else if (t === 'double' || t === 'decimal' || t === 'number') {
|
|
|
+ s = s.replace(/[^0-9+\-\.]/g, '')
|
|
|
+ s = s.replace(/(?!^)-/g, '')
|
|
|
+ const firstDot = s.indexOf('.')
|
|
|
+ if (firstDot >= 0) {
|
|
|
+ s =
|
|
|
+ s.slice(0, firstDot + 1) +
|
|
|
+ s.slice(firstDot + 1).replace(/\./g, '')
|
|
|
+ }
|
|
|
+ const dlen =
|
|
|
+ parseInt(meta.fieldTypenointlen) || parseInt(meta.fieldTypeNointLen)
|
|
|
+ if (!isNaN(dlen) && dlen >= 0 && firstDot >= 0) {
|
|
|
+ const parts = s.split('.')
|
|
|
+ parts[1] = (parts[1] || '').slice(0, dlen)
|
|
|
+ s = parts.join('.')
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const max = this.getMaxLength(label)
|
|
|
+ if (max && s.length > max) s = s.slice(0, max)
|
|
|
+ }
|
|
|
+ this.$set(row, label, s)
|
|
|
+ },
|
|
|
// 初始化年份列
|
|
|
initYearColumns() {
|
|
|
// 1) 优先使用立项信息中的监审期间(数组)
|
|
|
@@ -292,8 +726,17 @@
|
|
|
})
|
|
|
} else if (this.projectAuditPeriod) {
|
|
|
// 2) 立项信息中的监审期间(字符串)
|
|
|
- const periods = this.parseAuditPeriod(this.projectAuditPeriod)
|
|
|
- this.yearColumns = periods
|
|
|
+ if (Array.isArray(this.projectAuditPeriod)) {
|
|
|
+ this.yearColumns = this.projectAuditPeriod.map((period) => {
|
|
|
+ if (typeof period === 'string' && period.includes('-')) {
|
|
|
+ return period.split('-')[0]
|
|
|
+ }
|
|
|
+ return String(period)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ const periods = this.parseAuditPeriod(this.projectAuditPeriod)
|
|
|
+ this.yearColumns = periods
|
|
|
+ }
|
|
|
} else if (this.auditPeriods && this.auditPeriods.length > 0) {
|
|
|
// 3) 组件入参的监审期间(数组)
|
|
|
// 如果传入了监审期间,使用监审期间
|
|
|
@@ -453,6 +896,8 @@
|
|
|
// 构建公式索引并计算一次
|
|
|
this.buildFormulaEngine()
|
|
|
this.recomputeAllFormulas()
|
|
|
+ // 初始化后按年份对父项进行汇总
|
|
|
+ this.recomputeAllParentYears()
|
|
|
},
|
|
|
// 创建行数据
|
|
|
createRowData(item, isChild, rowid) {
|
|
|
@@ -473,6 +918,16 @@
|
|
|
rowData[`year_${year}`] = item[`year_${year}`] || ''
|
|
|
})
|
|
|
|
|
|
+ // 初始化固定表头(fixedFields)对应的列,确保 v-model 有响应式属性
|
|
|
+ const labels = this.dynamicColumns()
|
|
|
+ if (Array.isArray(labels) && labels.length > 0) {
|
|
|
+ labels.forEach((label) => {
|
|
|
+ if (rowData[label] === undefined) {
|
|
|
+ this.$set(rowData, label, '')
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
// 初始化备注
|
|
|
rowData.remark = item.remark || ''
|
|
|
|
|
|
@@ -552,6 +1007,16 @@
|
|
|
row[`year_${year}`] = rowData[year]
|
|
|
}
|
|
|
})
|
|
|
+
|
|
|
+ // 回显固定表头字段(与保存时一致,rkey 使用中文表头 label)
|
|
|
+ const labels = this.dynamicColumns()
|
|
|
+ if (Array.isArray(labels) && labels.length > 0) {
|
|
|
+ labels.forEach((label) => {
|
|
|
+ if (rowData[label] !== undefined) {
|
|
|
+ this.$set(row, label, rowData[label])
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
}
|
|
|
})
|
|
|
|
|
|
@@ -560,12 +1025,40 @@
|
|
|
|
|
|
// 回显后根据公式重算
|
|
|
this.recomputeAllFormulas()
|
|
|
+ // 回显后根据子项汇总父项年份
|
|
|
+ this.recomputeAllParentYears()
|
|
|
+ // 回显后再做一次字典值规范化,确保显示选中项
|
|
|
+ this.normalizeAllDictValues()
|
|
|
}
|
|
|
} catch (err) {
|
|
|
console.error('获取固定表详情失败', err)
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
+ // =========== 年份父项汇总 ==========
|
|
|
+ recomputeAllParentYears() {
|
|
|
+ if (!Array.isArray(this.tableData) || this.tableData.length === 0)
|
|
|
+ return
|
|
|
+ const parents = this.tableData.filter((r) => r && r.isCategory)
|
|
|
+ const years = Array.isArray(this.yearColumns) ? this.yearColumns : []
|
|
|
+ parents.forEach((parent) => {
|
|
|
+ const pid = parent.rowid || parent.id
|
|
|
+ const children = this.tableData.filter(
|
|
|
+ (r) =>
|
|
|
+ r &&
|
|
|
+ !r.isCategory &&
|
|
|
+ (r.parentid === pid || String(r.parentid) === String(pid))
|
|
|
+ )
|
|
|
+ years.forEach((y) => {
|
|
|
+ const field = `year_${y}`
|
|
|
+ const sum = children.reduce(
|
|
|
+ (acc, c) => acc + (Number(c[field]) || 0),
|
|
|
+ 0
|
|
|
+ )
|
|
|
+ this.$set(parent, field, sum === 0 ? '' : String(sum))
|
|
|
+ })
|
|
|
+ })
|
|
|
+ },
|
|
|
// 获取假数据(用于测试)
|
|
|
getMockTableData() {
|
|
|
// return [
|
|
|
@@ -653,6 +1146,8 @@
|
|
|
this.validateLinkage(row, year)
|
|
|
// 实时联动计算
|
|
|
this.recomputeDependentsForRow(row, year)
|
|
|
+ // 失焦也触发父项汇总
|
|
|
+ this.recomputeAllParentYears()
|
|
|
},
|
|
|
// 仅数字输入:按行规则限制小数位;整数不允许小数
|
|
|
handleNumericInput(row, year, val) {
|
|
|
@@ -693,6 +1188,8 @@
|
|
|
// 写回且触发联动
|
|
|
this.$set(row, field, s)
|
|
|
this.handleCellInput(row, year)
|
|
|
+ // 年份联动:子项变更后,汇总到父项
|
|
|
+ this.recomputeAllParentYears()
|
|
|
},
|
|
|
// 单元格失焦事件
|
|
|
handleCellBlur(row, year) {
|
|
|
@@ -891,38 +1388,226 @@
|
|
|
},
|
|
|
// 保存前验证
|
|
|
validateBeforeSave() {
|
|
|
- this.validationErrors = []
|
|
|
- let isValid = true
|
|
|
+ // 参考 FixedAssetsTable 的校验方式:优先使用 surveyData.fixedHeaders(metaList)
|
|
|
+ const metas = this.metaList()
|
|
|
+ const metaByLabel = new Map()
|
|
|
+ const metaByFieldName = new Map()
|
|
|
+ metas.forEach((m) => {
|
|
|
+ if (!m) return
|
|
|
+ if (m.label) metaByLabel.set(String(m.label).trim(), m)
|
|
|
+ if (m.fieldName) metaByFieldName.set(String(m.fieldName).trim(), m)
|
|
|
+ })
|
|
|
|
|
|
- // 验证所有单元格
|
|
|
- this.tableData.forEach((row) => {
|
|
|
- if (row.isCategory) return
|
|
|
+ const isEmpty = (v) => v === undefined || v === null || v === ''
|
|
|
+ const isIntWithLen = (v, len) => {
|
|
|
+ const s = String(v == null ? '' : v)
|
|
|
+ if (!/^[-+]?\d+$/.test(s)) return false
|
|
|
+ const digits = s.replace(/^[-+]?/, '')
|
|
|
+ return len === undefined ? true : digits.length <= Number(len)
|
|
|
+ }
|
|
|
+ const isDecWithLen = (v, intLen, decLen) => {
|
|
|
+ const s = String(v == null ? '' : v)
|
|
|
+ if (!/^[-+]?\d*(?:\.\d+)?$/.test(s)) return false
|
|
|
+ const [i, d = ''] = s.replace(/^[-+]?/, '').split('.')
|
|
|
+ const io = intLen === undefined ? true : i.length <= Number(intLen)
|
|
|
+ const doK = decLen === undefined ? true : d.length <= Number(decLen)
|
|
|
+ return io && doK && (i.length > 0 || d.length > 0)
|
|
|
+ }
|
|
|
+ const inferType = (meta) => {
|
|
|
+ if (!meta) return ''
|
|
|
+ const fmt = meta.format
|
|
|
+ ? String(meta.format).trim().toLowerCase()
|
|
|
+ : ''
|
|
|
+ if (fmt) {
|
|
|
+ if (/(hh|hh:mm|hh:mm:ss)/i.test(fmt) || fmt.includes('hh'))
|
|
|
+ return 'datetime'
|
|
|
+ if (/^y{4}$/.test(fmt)) return 'year'
|
|
|
+ if (
|
|
|
+ (/y{4}/.test(fmt) && /m{2}/.test(fmt)) ||
|
|
|
+ fmt.includes('yyyy-mm')
|
|
|
+ )
|
|
|
+ return 'date'
|
|
|
+ }
|
|
|
+ const t = String(meta.type || meta.fieldType || '').toLowerCase()
|
|
|
+ if (t.includes('datetime') || t.includes('timestamp'))
|
|
|
+ return 'datetime'
|
|
|
+ if (t.includes('date') && !t.includes('datetime')) return 'date'
|
|
|
+ if (t.includes('year')) return 'year'
|
|
|
+ if (
|
|
|
+ t.includes('int') ||
|
|
|
+ t.includes('number') ||
|
|
|
+ t.includes('decimal') ||
|
|
|
+ t.includes('float') ||
|
|
|
+ t.includes('double')
|
|
|
+ )
|
|
|
+ return 'number'
|
|
|
+ return ''
|
|
|
+ }
|
|
|
|
|
|
- this.yearColumns.forEach((year) => {
|
|
|
- const cellValid = this.validateCell(row, year)
|
|
|
- if (!cellValid) {
|
|
|
- isValid = false
|
|
|
- this.validationErrors.push({
|
|
|
- row: row.itemName,
|
|
|
- year,
|
|
|
- message: `${row.itemName}的${year}年数据验证失败`,
|
|
|
- })
|
|
|
- }
|
|
|
- })
|
|
|
+ const errors = []
|
|
|
+ const fixedLabels = this.dynamicColumns()
|
|
|
|
|
|
- // 验证勾稽关系
|
|
|
- this.yearColumns.forEach((year) => {
|
|
|
- const linkageValid = this.validateLinkage(row, year)
|
|
|
- if (!linkageValid) {
|
|
|
- isValid = false
|
|
|
- }
|
|
|
- })
|
|
|
+ // 校验每行固定表头列
|
|
|
+ this.tableData.forEach((row, rowIndex) => {
|
|
|
+ if (row.isCategory) return
|
|
|
+ if (Array.isArray(fixedLabels)) {
|
|
|
+ fixedLabels.forEach((label) => {
|
|
|
+ const meta =
|
|
|
+ metaByLabel.get(String(label).trim()) ||
|
|
|
+ metaByFieldName.get(String(label).trim())
|
|
|
+ const val = row[label]
|
|
|
+ const reqRaw =
|
|
|
+ meta &&
|
|
|
+ (meta.required != null ? meta.required : meta.isRequired)
|
|
|
+ const req =
|
|
|
+ reqRaw != null &&
|
|
|
+ ['1', 'true', 'y', 'yes', '是'].includes(
|
|
|
+ String(reqRaw).trim().toLowerCase()
|
|
|
+ )
|
|
|
+ if (meta && req && isEmpty(val)) {
|
|
|
+ errors.push(`第${rowIndex + 1}行【${label}】不能为空`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (isEmpty(val)) return
|
|
|
+ const t = inferType(meta)
|
|
|
+ const intLen =
|
|
|
+ (meta &&
|
|
|
+ (meta.fieldTypelen ||
|
|
|
+ meta.fieldTypeLen ||
|
|
|
+ meta.totalLength)) ??
|
|
|
+ undefined
|
|
|
+ const decLenRaw =
|
|
|
+ (meta &&
|
|
|
+ (meta.fieldTypenointlen ||
|
|
|
+ meta.fieldTypeNointLen ||
|
|
|
+ meta.decimalLength ||
|
|
|
+ meta.scale)) ??
|
|
|
+ undefined
|
|
|
+ const decLen =
|
|
|
+ t === 'number' &&
|
|
|
+ String(meta?.fieldType || '')
|
|
|
+ .toLowerCase()
|
|
|
+ .includes('int')
|
|
|
+ ? 0
|
|
|
+ : decLenRaw
|
|
|
+
|
|
|
+ if (t === 'year') {
|
|
|
+ if (!(typeof val === 'string' && /^\d{4}$/.test(val))) {
|
|
|
+ errors.push(`第${rowIndex + 1}行【${label}】必须为四位年份`)
|
|
|
+ }
|
|
|
+ } else if (t === 'date') {
|
|
|
+ if (
|
|
|
+ !(typeof val === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(val))
|
|
|
+ ) {
|
|
|
+ errors.push(
|
|
|
+ `第${rowIndex + 1}行【${label}】日期格式应为YYYY-MM-DD`
|
|
|
+ )
|
|
|
+ }
|
|
|
+ } else if (t === 'datetime') {
|
|
|
+ if (
|
|
|
+ !(
|
|
|
+ typeof val === 'string' &&
|
|
|
+ /^(\d{4}-\d{2}-\d{2})\s+\d{2}:\d{2}(:\d{2})?$/.test(val)
|
|
|
+ )
|
|
|
+ ) {
|
|
|
+ errors.push(
|
|
|
+ `第${
|
|
|
+ rowIndex + 1
|
|
|
+ }行【${label}】日期时间格式应为YYYY-MM-DD HH:mm:ss`
|
|
|
+ )
|
|
|
+ }
|
|
|
+ } else if (t === 'number') {
|
|
|
+ const treatAsInt =
|
|
|
+ String(meta?.type || meta?.fieldType || '')
|
|
|
+ .toLowerCase()
|
|
|
+ .includes('int') || decLen === 0
|
|
|
+ if (treatAsInt) {
|
|
|
+ if (!isIntWithLen(val, intLen)) {
|
|
|
+ errors.push(
|
|
|
+ `第${rowIndex + 1}行【${label}】必须为整数且整数位不超过${
|
|
|
+ intLen || '限定'
|
|
|
+ }位`
|
|
|
+ )
|
|
|
+ }
|
|
|
+ } else if (!isDecWithLen(val, intLen, decLen)) {
|
|
|
+ errors.push(
|
|
|
+ `第${rowIndex + 1}行【${label}】必须为数字,整数不超过${
|
|
|
+ intLen || '限定'
|
|
|
+ }位,小数不超过${decLen}位`
|
|
|
+ )
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 普通文本按 format 数字限制最大长度(如 format: '5')
|
|
|
+ const fmt =
|
|
|
+ meta && meta.format != null ? String(meta.format).trim() : ''
|
|
|
+ if (/^\d+$/.test(fmt)) {
|
|
|
+ const max = parseInt(fmt)
|
|
|
+ const s = String(val == null ? '' : val)
|
|
|
+ if (!isNaN(max) && s.length > max) {
|
|
|
+ errors.push(
|
|
|
+ `第${rowIndex + 1}行【${label}】长度不能超过${max}个字符`
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 字典/枚举校验:优先字典接口选项,其次 meta.options
|
|
|
+ if (meta) {
|
|
|
+ const collectValues = (list) =>
|
|
|
+ new Set(
|
|
|
+ (Array.isArray(list) ? list : [])
|
|
|
+ .map((o) => {
|
|
|
+ const v =
|
|
|
+ o &&
|
|
|
+ (o.key != null
|
|
|
+ ? o.key
|
|
|
+ : o.value != null
|
|
|
+ ? o.value
|
|
|
+ : o.label)
|
|
|
+ return v != null ? String(v) : undefined
|
|
|
+ })
|
|
|
+ .filter(Boolean)
|
|
|
+ )
|
|
|
+ let allowed = new Set()
|
|
|
+ const code =
|
|
|
+ (meta.dictCode && String(meta.dictCode).trim()) ||
|
|
|
+ (meta.dictType && String(meta.dictType).trim()) ||
|
|
|
+ (meta.typeKey && String(meta.typeKey).trim()) ||
|
|
|
+ (meta.dictId && String(meta.dictId).trim()) ||
|
|
|
+ (meta.dictid && String(meta.dictid).trim()) ||
|
|
|
+ ''
|
|
|
+ if (code && typeof this.getDictOptions === 'function') {
|
|
|
+ const dictList = this.getDictOptions(code)
|
|
|
+ if (Array.isArray(dictList) && dictList.length > 0) {
|
|
|
+ allowed = collectValues(dictList)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (allowed.size === 0 && Array.isArray(meta.options)) {
|
|
|
+ allowed = collectValues(meta.options)
|
|
|
+ }
|
|
|
+ if (allowed.size > 0 && !allowed.has(String(val))) {
|
|
|
+ errors.push(
|
|
|
+ `第${rowIndex + 1}行【${label}】不在允许的取值范围内`
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
- // 验证所有勾稽关系
|
|
|
+ // 同时校验年份列(沿用现有 validateCell 与勾稽关系)
|
|
|
+ // this.tableData.forEach((row) => {
|
|
|
+ // if (row.isCategory) return
|
|
|
+ // this.yearColumns.forEach((year) => {
|
|
|
+ // if (!this.validateCell(row, year)) {
|
|
|
+ // errors.push(`${row.itemName}的${year}年数据验证失败`)
|
|
|
+ // }
|
|
|
+ // })
|
|
|
+ // })
|
|
|
+
|
|
|
+ // 勾稽关系校验
|
|
|
this.validateAllLinkages()
|
|
|
-
|
|
|
- return isValid
|
|
|
+ this.validationErrors = errors
|
|
|
+ return errors.length === 0
|
|
|
},
|
|
|
// 验证所有勾稽关系
|
|
|
validateAllLinkages() {
|
|
|
@@ -1033,10 +1718,14 @@
|
|
|
async handleSave() {
|
|
|
// 验证数据
|
|
|
if (!this.validateBeforeSave()) {
|
|
|
- const errorMessages = this.validationErrors
|
|
|
- .map((err) => `${err.row}的${err.year}年:${err.message}`)
|
|
|
- .join('\n')
|
|
|
- Message.error('数据验证失败:\n' + errorMessages)
|
|
|
+ const msgs = Array.isArray(this.validationErrors)
|
|
|
+ ? this.validationErrors
|
|
|
+ : []
|
|
|
+ // 仅展示前若干条,避免过长
|
|
|
+ const preview = msgs.slice(0, 5).join('\n')
|
|
|
+ Message.error(
|
|
|
+ `数据验证失败:\n${preview}${msgs.length > 5 ? '\n……' : ''}`
|
|
|
+ )
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -1083,6 +1772,21 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 保存固定表头字段的值(来自 fixedFields / dynamicColumns)——扩展到所有行
|
|
|
+ const fixedLabels = this.dynamicColumns()
|
|
|
+ if (Array.isArray(fixedLabels) && fixedLabels.length > 0) {
|
|
|
+ fixedLabels.forEach((label) => {
|
|
|
+ const val = row[label]
|
|
|
+ if (val !== undefined && val !== null && val !== '') {
|
|
|
+ saveData.push({
|
|
|
+ rowid: rowid,
|
|
|
+ rkey: label,
|
|
|
+ rvalue: String(val),
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
// 保存年份数据(所有行都可以有年份数据)
|
|
|
this.yearColumns.forEach((year) => {
|
|
|
const yearValue = row[`year_${year}`]
|