| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293 |
- <template>
- <el-dialog
- title="调查表填报"
- :visible.sync="dialogVisible"
- width="90%"
- :close-on-click-modal="false"
- :show-close="true"
- append-to-body
- :modal="false"
- @close="handleClose"
- >
- <el-table
- :data="tableData"
- border
- style="width: 100%"
- :row-class-name="getRowClassName"
- >
- <!-- 序号列 -->
- <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
- prop="itemName"
- label="项目"
- min-width="200"
- align="left"
- >
- <template slot-scope="scope">
- <span v-if="scope.row.isCategory" class="category-name">
- {{ scope.row.itemName }}
- </span>
- <span v-else>{{ scope.row.itemName }}</span>
- </template>
- </el-table-column>
- <!-- 单位列(只读) -->
- <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>
- <!-- 指标编号(cellCode,只读) -->
- <el-table-column
- prop="cellCode"
- label="指标编号"
- width="120"
- align="center"
- >
- <template slot-scope="scope">
- <span v-if="!scope.row.isCategory">{{ scope.row.cellCode }}</span>
- </template>
- </el-table-column>
- <!-- 计算方式(calculationFormula,只读) -->
- <el-table-column
- prop="calculationFormula"
- label="计算方式"
- min-width="180"
- align="left"
- >
- <template slot-scope="scope">
- <span v-if="!scope.row.isCategory">
- {{ scope.row.calculationFormula }}
- </span>
- </template>
- </el-table-column>
- <!-- 动态年份列 -->
- <el-table-column
- v-for="year in yearColumns"
- :key="year"
- :label="`${year}年`"
- :prop="`year_${year}`"
- width="150"
- align="center"
- >
- <template slot-scope="scope">
- <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)"
- />
- </template>
- </el-table-column>
- <!-- 备注列 -->
- <el-table-column prop="remark" label="备注" min-width="150" align="left">
- <template slot-scope="scope">
- <el-input
- v-if="!scope.row.isCategory"
- v-model="scope.row.remark"
- placeholder="请输入备注"
- :disabled="isViewMode"
- />
- </template>
- </el-table-column>
- </el-table>
- <div slot="footer" class="dialog-footer">
- <el-button type="primary" @click="handleSave">保存</el-button>
- <el-button @click="handleCancel">取消</el-button>
- </div>
- </el-dialog>
- </template>
- <script>
- import { Message } from 'element-ui'
- import { saveSingleRecordSurvey, getSurveyDetail } from '@/api/audit/survey'
- export default {
- name: 'FixedTableDialog',
- props: {
- visible: {
- type: Boolean,
- default: false,
- },
- surveyData: {
- type: Object,
- default: () => ({}),
- },
- // 表格数据配置
- // 格式: [
- // {
- // id: '1',
- // itemName: '班级数',
- // unit: '个',
- // isCategory: false,
- // parentId: null,
- // categoryId: 'III',
- // seq: 1,
- // validateRules: {
- // required: true,
- // type: 'number',
- // min: 0,
- // },
- // linkageRules: {
- // parent: 'III',
- // relation: 'sum',
- // },
- // },
- // {
- // id: 'III',
- // itemName: '在取做保职工总人数',
- // unit: '人',
- // isCategory: true,
- // categorySeq: 'III',
- // children: [...],
- // }
- // ]
- tableItems: {
- type: Array,
- default: () => [],
- },
- // 监审期间(年份数组)
- // 格式: ['2022', '2023', '2024']
- auditPeriods: {
- type: Array,
- default: () => [],
- },
- // 立项信息中的监审期间(优先使用)- 数组
- projectAuditPeriods: {
- type: Array,
- default: () => [],
- },
- // 立项信息中的监审期间(字符串,如 '2022,2023,2024' 或 '2022-2024')
- projectAuditPeriod: {
- type: String,
- default: '',
- },
- // 是否查看模式
- isViewMode: {
- type: Boolean,
- default: false,
- },
- // 被监审单位ID
- auditedUnitId: {
- type: String,
- default: '',
- },
- // 上传记录ID
- uploadId: {
- type: String,
- default: '',
- },
- // 成本调查表模板ID
- surveyTemplateId: {
- type: String,
- default: '',
- },
- // 目录ID
- catalogId: {
- type: String,
- default: '',
- },
- // 统一控制接口 type(1=成本调查表,2=报送资料)
- requestType: {
- type: [String, Number],
- default: 1,
- },
- // 任务ID
- taskId: {
- type: [String, Number],
- default: '',
- },
- },
- data() {
- return {
- dialogVisible: false,
- tableData: [],
- yearColumns: [],
- validationErrors: [],
- cellCodeIndex: {}, // cellCode -> row
- dependentsMap: {}, // cellCode -> Set(rows depending on it)
- }
- },
- watch: {
- visible: {
- async handler(newVal) {
- this.dialogVisible = newVal
- if (newVal) {
- // 先初始化表格数据
- this.initTableData()
- // 等待 DOM 更新后,如果有 uploadId,调用接口获取详情数据并回显
- await this.$nextTick()
- this.loadDetailData()
- }
- },
- immediate: true,
- },
- dialogVisible(newVal) {
- if (!newVal) {
- this.$emit('update:visible', false)
- }
- },
- tableItems: {
- handler() {
- if (this.dialogVisible) {
- this.initTableData()
- }
- },
- deep: true,
- },
- auditPeriods: {
- handler() {
- if (this.dialogVisible) {
- this.initYearColumns()
- this.initTableData()
- }
- },
- deep: true,
- },
- projectAuditPeriods: {
- handler() {
- if (this.dialogVisible) {
- this.initYearColumns()
- this.initTableData()
- }
- },
- deep: true,
- },
- projectAuditPeriod(newVal) {
- if (this.dialogVisible) {
- this.initYearColumns()
- this.initTableData()
- }
- },
- },
- mounted() {
- this.initYearColumns()
- },
- methods: {
- // 初始化年份列
- initYearColumns() {
- // 1) 优先使用立项信息中的监审期间(数组)
- if (this.projectAuditPeriods && this.projectAuditPeriods.length > 0) {
- this.yearColumns = this.projectAuditPeriods.map((period) => {
- if (typeof period === 'string' && period.includes('-')) {
- return period.split('-')[0]
- }
- return String(period)
- })
- } else if (this.projectAuditPeriod) {
- // 2) 立项信息中的监审期间(字符串)
- const periods = this.parseAuditPeriod(this.projectAuditPeriod)
- this.yearColumns = periods
- } else if (this.auditPeriods && this.auditPeriods.length > 0) {
- // 3) 组件入参的监审期间(数组)
- // 如果传入了监审期间,使用监审期间
- this.yearColumns = this.auditPeriods.map((period) => {
- // 如果是日期格式,提取年份
- if (typeof period === 'string' && period.includes('-')) {
- return period.split('-')[0]
- }
- return String(period)
- })
- } else if (this.surveyData && this.surveyData.auditPeriod) {
- // 如果从 surveyData 中获取监审期间
- const periods = this.parseAuditPeriod(this.surveyData.auditPeriod)
- this.yearColumns = periods
- } else {
- // 默认使用最近3年
- const currentYear = new Date().getFullYear()
- this.yearColumns = [
- String(currentYear - 2),
- String(currentYear - 1),
- String(currentYear),
- ]
- }
- },
- // 解析监审期间字符串(如 "2022,2023,2024" 或 "2022-2024")
- parseAuditPeriod(periodStr) {
- if (!periodStr) return []
- if (periodStr.includes(',')) {
- return periodStr.split(',').map((p) => p.trim())
- }
- if (periodStr.includes('-')) {
- const parts = periodStr.split('-')
- if (parts.length === 2) {
- const start = parseInt(parts[0].trim())
- const end = parseInt(parts[1].trim())
- const years = []
- for (let year = start; year <= end; year++) {
- years.push(String(year))
- }
- return years
- }
- }
- return [String(periodStr)]
- },
- // 初始化表格数据
- initTableData() {
- console.log(this.isViewMode, '只读')
- if (!this.tableItems || this.tableItems.length === 0) {
- // 如果没有传入数据,使用假数据
- this.tableData = this.getMockTableData()
- return
- }
- // 根据 parentid 关系排列:parentid 为 '-1' 的是父项,子项的 parentid 等于父项的 rowid/id
- const flatData = []
- const processedIds = new Set()
- // 先处理所有父项(parentid 为 '-1')
- this.tableItems.forEach((item) => {
- const parentid = item.parentid
- const rowid = item.rowid || item.id
- if (parentid === '-1' || parentid === -1) {
- const rowData = this.createRowData(item, false, rowid)
- flatData.push(rowData)
- processedIds.add(rowid)
- }
- })
- // 再处理所有子项,紧跟在父项后面,按正序排列
- // 先收集所有子项,按序号或顺序排序
- const childItems = this.tableItems.filter((item) => {
- const rowid = item.rowid || item.id
- const parentid = item.parentid
- return (
- !processedIds.has(rowid) && parentid !== '-1' && parentid !== -1
- )
- })
- // 对子项按序号正序排序
- childItems.sort((a, b) => {
- const seqA =
- a.seq !== undefined
- ? typeof a.seq === 'number'
- ? a.seq
- : parseInt(a.seq) || 0
- : 0
- const seqB =
- b.seq !== undefined
- ? typeof b.seq === 'number'
- ? b.seq
- : parseInt(b.seq) || 0
- : 0
- return seqA - seqB
- })
- // 需要多次遍历,确保所有子项都能找到父项
- let hasUnprocessed = true
- while (hasUnprocessed && childItems.length > 0) {
- hasUnprocessed = false
- const remainingItems = []
- childItems.forEach((item) => {
- const rowid = item.rowid || item.id
- const parentid = item.parentid
- // 跳过已处理的项
- if (processedIds.has(rowid)) {
- return
- }
- // 查找父项在 flatData 中的位置
- const parentIndex = flatData.findIndex((row) => {
- const parentRowid = row.rowid || row.id
- return parentRowid === parentid
- })
- if (parentIndex >= 0) {
- // 找到父项,插入到父项后面
- // 找到父项后面最后一个子项的位置
- let insertIndex = parentIndex + 1
- while (
- insertIndex < flatData.length &&
- (flatData[insertIndex].rowid || flatData[insertIndex].id) !==
- parentid &&
- flatData[insertIndex].parentid === parentid
- ) {
- insertIndex++
- }
- const rowData = this.createRowData(item, true, rowid)
- flatData.splice(insertIndex, 0, rowData)
- processedIds.add(rowid)
- } else {
- // 如果找不到父项,标记为未处理,等待下一轮
- remainingItems.push(item)
- hasUnprocessed = true
- }
- })
- // 更新待处理的子项列表
- childItems.length = 0
- childItems.push(...remainingItems)
- }
- // 处理剩余未找到父项的项(可能是数据异常)
- this.tableItems.forEach((item) => {
- const rowid = item.rowid || item.id
- if (!processedIds.has(rowid)) {
- const rowData = this.createRowData(item, false, rowid)
- flatData.push(rowData)
- processedIds.add(rowid)
- }
- })
- this.tableData = flatData
- // 构建公式索引并计算一次
- this.buildFormulaEngine()
- this.recomputeAllFormulas()
- },
- // 创建行数据
- createRowData(item, isChild, rowid) {
- const rowData = {
- ...item,
- rowid: rowid || item.rowid || item.id,
- seq:
- item.seq !== undefined
- ? item.seq
- : isChild
- ? ''
- : item.categorySeq || item.id || '',
- isCategory: item.isCategory || false,
- }
- // 初始化年份数据
- this.yearColumns.forEach((year) => {
- rowData[`year_${year}`] = item[`year_${year}`] || ''
- })
- // 初始化备注
- rowData.remark = item.remark || ''
- // 如果有传入的数据,填充值
- if (this.surveyData && this.surveyData[rowid]) {
- const savedData = this.surveyData[rowid]
- this.yearColumns.forEach((year) => {
- if (savedData[year] !== undefined) {
- rowData[`year_${year}`] = savedData[year]
- }
- })
- if (savedData.remark !== undefined) {
- rowData.remark = savedData.remark
- }
- }
- return rowData
- },
- // 加载详情数据并回显
- async loadDetailData() {
- // 如果有 uploadId 和 auditedUnitId,调用接口获取详情数据
- const uploadId =
- this.uploadId || this.surveyData.uploadId || this.surveyData.id
- const auditedUnitId =
- this.auditedUnitId || this.surveyData.auditedUnitId
- // 只要有 uploadId 就尝试获取回显数据
- if (uploadId) {
- try {
- const params = {
- uploadId: uploadId,
- auditedUnitId: auditedUnitId,
- type: this.requestType,
- }
- const res = await getSurveyDetail(params)
- console.log('固定表详情数据', res)
- if (res && res.code === 200 && res.value) {
- // 将接口返回的数据转换为表格数据格式
- const detailData = Array.isArray(res.value)
- ? res.value
- : res.value.items || res.value.records || []
- // 按 rowid 分组数据
- const dataByRowid = {}
- detailData.forEach((item) => {
- if (item.rowid) {
- if (!dataByRowid[item.rowid]) {
- dataByRowid[item.rowid] = {}
- }
- dataByRowid[item.rowid][item.rkey] = item.rvalue
- }
- })
- // 回显数据到表格
- this.tableData.forEach((row) => {
- const rowid = row.rowid || row.id
- const rowData = dataByRowid[rowid]
- if (rowData) {
- // 回显基本信息
- if (rowData['序号'] !== undefined) {
- row.seq = rowData['序号']
- }
- if (rowData['项目'] !== undefined) {
- row.itemName = rowData['项目']
- }
- if (rowData['单位'] !== undefined) {
- row.unit = rowData['单位']
- }
- if (rowData['备注'] !== undefined) {
- row.remark = rowData['备注']
- }
- // 回显年份数据
- this.yearColumns.forEach((year) => {
- if (rowData[year] !== undefined) {
- row[`year_${year}`] = rowData[year]
- }
- })
- }
- })
- // 强制更新视图
- this.$forceUpdate()
- // 回显后根据公式重算
- this.recomputeAllFormulas()
- }
- } catch (err) {
- console.error('获取固定表详情失败', err)
- }
- }
- },
- // 获取假数据(用于测试)
- getMockTableData() {
- return [
- {
- id: '1',
- itemName: '班级数',
- unit: '个',
- isCategory: false,
- seq: 1,
- year_2022: '',
- year_2023: '',
- year_2024: '',
- remark: '',
- },
- {
- id: '2',
- itemName: '幼儿学生人数',
- unit: '人',
- isCategory: false,
- seq: 2,
- year_2022: '',
- year_2023: '',
- year_2024: '',
- remark: '',
- },
- {
- id: 'III',
- itemName: '在取做保职工总人数',
- unit: '人',
- isCategory: true,
- categorySeq: 'III',
- seq: 'III',
- },
- {
- id: '3-1',
- itemName: '行政管理人员数',
- unit: '人',
- isCategory: false,
- categoryId: 'III',
- categorySeq: 'III',
- seq: 3,
- year_2022: '',
- year_2023: '',
- year_2024: '',
- remark: '',
- },
- {
- id: '3-2',
- itemName: '教师人数',
- unit: '人',
- isCategory: false,
- categoryId: 'III',
- categorySeq: 'III',
- seq: 4,
- year_2022: '',
- year_2023: '',
- year_2024: '',
- remark: '',
- },
- {
- id: '3-3',
- itemName: '保育员人数',
- unit: '人',
- isCategory: false,
- categoryId: 'III',
- categorySeq: 'III',
- seq: 5,
- year_2022: '',
- year_2023: '',
- year_2024: '',
- remark: '',
- },
- ]
- },
- // 获取行样式类名
- getRowClassName({ row }) {
- if (row.isCategory) {
- return 'category-row'
- }
- return ''
- },
- // 单元格输入事件
- handleCellInput(row, year) {
- // 实时验证勾稽关系
- this.validateLinkage(row, year)
- // 实时联动计算
- 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) {
- // 验证格式和非空
- this.validateCell(row, year)
- // 验证勾稽关系
- this.validateLinkage(row, year)
- // 失焦后再次联动计算,确保取整/格式等影响后结果一致
- this.recomputeDependentsForRow(row, year)
- },
- // 验证单元格(非空和格式验证)
- validateCell(row, year) {
- const fieldName = `year_${year}`
- const value = row[fieldName]
- // 非空验证
- if (row.validateRules && row.validateRules.required && !value) {
- this.showFieldError(
- row,
- year,
- `${row.itemName}的${year}年数据不能为空`
- )
- return false
- }
- // 类型/规则验证:支持 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}年数据不能小于${r.min}`
- )
- return false
- }
- if (r.max !== undefined && numVal > r.max) {
- this.showFieldError(
- row,
- year,
- `${row.itemName}的${year}年数据不能大于${r.max}`
- )
- return false
- }
- 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}年数据必须为字典项之一`
- )
- return false
- }
- }
- }
- return true
- },
- // 验证勾稽关系
- validateLinkage(row, year) {
- if (!row.linkageRules) return true
- const { parent, relation } = row.linkageRules
- if (relation === 'sum') {
- // 验证子项之和等于父项
- const parentRow = this.tableData.find((r) => r.id === parent)
- if (!parentRow || parentRow.isCategory) return true
- const children = this.tableData.filter(
- (r) => r.categoryId === parent && !r.isCategory
- )
- const parentValue = Number(parentRow[`year_${year}`]) || 0
- const sumValue = children.reduce((sum, child) => {
- return sum + (Number(child[`year_${year}`]) || 0)
- }, 0)
- if (parentValue !== sumValue) {
- // 可以选择自动修正或提示错误
- // 这里仅提示,不自动修正
- console.warn(
- `${parentRow.itemName}的${year}年数据(${parentValue})与子项之和(${sumValue})不相等`
- )
- }
- }
- return true
- },
- // 显示字段错误
- showFieldError(row, year, message) {
- // 这里可以添加更详细的错误提示
- console.error(message)
- },
- // 保存前验证
- validateBeforeSave() {
- this.validationErrors = []
- let isValid = true
- // 验证所有单元格
- this.tableData.forEach((row) => {
- if (row.isCategory) 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}年数据验证失败`,
- })
- }
- })
- // 验证勾稽关系
- this.yearColumns.forEach((year) => {
- const linkageValid = this.validateLinkage(row, year)
- if (!linkageValid) {
- isValid = false
- }
- })
- })
- // 验证所有勾稽关系
- this.validateAllLinkages()
- return isValid
- },
- // 验证所有勾稽关系
- validateAllLinkages() {
- const categories = this.tableData.filter((r) => r.isCategory)
- categories.forEach((category) => {
- const children = this.tableData.filter(
- (r) => r.categoryId === category.id && !r.isCategory
- )
- this.yearColumns.forEach((year) => {
- const categoryValue = Number(category[`year_${year}`]) || 0
- const sumValue = children.reduce((sum, child) => {
- return sum + (Number(child[`year_${year}`]) || 0)
- }, 0)
- if (categoryValue !== 0 && categoryValue !== sumValue) {
- this.validationErrors.push({
- row: category.itemName,
- year,
- message: `${category.itemName}的${year}年数据(${categoryValue})与子项之和(${sumValue})不相等`,
- })
- }
- })
- })
- },
- // 简单日期校验(支持 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
- this.$emit('update:visible', false)
- },
- // 取消
- handleCancel() {
- this.handleClose()
- },
- // 保存
- async handleSave() {
- // 验证数据
- if (!this.validateBeforeSave()) {
- const errorMessages = this.validationErrors
- .map((err) => `${err.row}的${err.year}年:${err.message}`)
- .join('\n')
- Message.error('数据验证失败:\n' + errorMessages)
- return
- }
- try {
- // 判断是否有数据(编辑模式):如果有 uploadId,说明是编辑已有数据
- const hasData = !!(
- this.uploadId ||
- this.surveyData.uploadId ||
- this.surveyData.id
- )
- // 格式化保存数据为接口需要的格式
- const saveData = []
- // 遍历所有行数据
- this.tableData.forEach((row) => {
- const rowid = row.rowid || row.id
- // 跳过分类行(如果分类行不需要保存基本信息)
- if (!row.isCategory) {
- // 保存基本信息:序号、项目、单位
- if (row.seq !== undefined && row.seq !== null && row.seq !== '') {
- saveData.push({
- rowid: rowid,
- rkey: '序号',
- rvalue: String(row.seq),
- })
- }
- if (row.itemName) {
- saveData.push({
- rowid: rowid,
- rkey: '项目',
- rvalue: String(row.itemName),
- })
- }
- if (row.unit) {
- saveData.push({
- rowid: rowid,
- rkey: '单位',
- rvalue: String(row.unit),
- })
- }
- }
- // 保存年份数据(所有行都可以有年份数据)
- this.yearColumns.forEach((year) => {
- const yearValue = row[`year_${year}`]
- if (
- yearValue !== undefined &&
- yearValue !== null &&
- yearValue !== ''
- ) {
- saveData.push({
- rowid: rowid,
- rkey: String(year),
- rvalue: String(yearValue),
- })
- }
- })
- // 保存备注(所有行都可以有备注)
- if (
- row.remark !== undefined &&
- row.remark !== null &&
- row.remark !== ''
- ) {
- saveData.push({
- rowid: rowid,
- rkey: '备注',
- rvalue: String(row.remark),
- })
- }
- })
- // 为每条数据添加公共字段
- const finalSaveData = saveData.map((item) => {
- const dataItem = {
- ...item,
- auditedUnitId:
- this.auditedUnitId || this.surveyData.auditedUnitId || '',
- surveyTemplateId:
- this.surveyTemplateId || this.surveyData.surveyTemplateId || '',
- catalogId: this.catalogId || this.surveyData.catalogId || '',
- taskId: this.taskId || this.surveyData.taskId || '',
- type: this.requestType,
- }
- // 如果有数据(编辑模式),添加 uploadId 字段
- if (hasData) {
- dataItem.uploadId =
- this.uploadId ||
- this.surveyData.uploadId ||
- this.surveyData.id ||
- ''
- }
- return dataItem
- })
- console.log('保存数据:', finalSaveData)
- // 调用保存接口
- const res = await saveSingleRecordSurvey(finalSaveData)
- if (res && res.code === 200) {
- Message.success('保存成功')
- // 触发保存事件
- this.$emit('save', finalSaveData)
- // 触发刷新事件
- this.$emit('refresh')
- this.handleClose()
- } else {
- Message.error(res.message || '保存失败')
- }
- } catch (err) {
- console.error('保存失败', err)
- // Message.error(err.message || '保存失败')
- }
- },
- // 构建 cellCode 索引与依赖图
- buildFormulaEngine() {
- const index = {}
- const deps = {}
- this.tableData.forEach((row) => {
- if (row && row.cellCode) {
- index[row.cellCode] = row
- }
- })
- // 构建依赖映射:A1 -> [依赖 A1 的行...]
- this.tableData.forEach((row) => {
- if (!row || !row.calculationFormula) return
- const codes =
- row.calculationFormula.toString().match(/[A-Z]+\d+/g) || []
- codes.forEach((code) => {
- if (!deps[code]) deps[code] = new Set()
- deps[code].add(row)
- })
- })
- this.cellCodeIndex = index
- this.dependentsMap = deps
- },
- // 重新计算一个公式行在某年的值
- recomputeRowForYear(row, year) {
- if (!row || !row.calculationFormula) return
- const expr = row.calculationFormula.toString()
- // 替换变量为对应年的数值
- const replaced = expr.replace(/[A-Z]+\d+/g, (code) => {
- const refRow = this.cellCodeIndex[code]
- const v = refRow ? Number(refRow[`year_${year}`]) : 0
- return isNaN(v) ? '0' : String(v)
- })
- // 仅允许数字与运算符
- const safe = this.safeEvalExpression(replaced)
- if (safe !== null && !isNaN(safe)) {
- row[`year_${year}`] = String(safe)
- }
- },
- // 在某年针对一个输入行,联动所有依赖它的行
- recomputeDependentsForRow(row, year) {
- if (!row) return
- const code = row.cellCode
- if (!code) return
- const dependents = this.dependentsMap[code]
- if (!dependents || dependents.size === 0) return
- dependents.forEach((depRow) => this.recomputeRowForYear(depRow, year))
- // 可能存在链式依赖,递归触发
- dependents.forEach((depRow) =>
- this.recomputeDependentsForRow(depRow, year)
- )
- },
- // 对所有含公式的行、所有年重算
- recomputeAllFormulas() {
- if (!this.yearColumns || this.yearColumns.length === 0) return
- const formulaRows = this.tableData.filter(
- (r) => r && r.calculationFormula
- )
- this.yearColumns.forEach((year) => {
- formulaRows.forEach((r) => this.recomputeRowForYear(r, year))
- })
- this.$forceUpdate()
- },
- // 简单安全表达式求值:仅支持数字、+ - * / 和括号
- safeEvalExpression(expr) {
- const cleaned = expr.replace(/\s+/g, '')
- if (!/^[-+*/().\d]+$/.test(cleaned)) {
- // 存在不允许的字符,放弃计算
- return null
- }
- try {
- // eslint-disable-next-line no-new-func
- const fn = new Function(`return (${cleaned})`)
- const res = fn()
- return typeof res === 'number' && isFinite(res) ? res : null
- } catch (e) {
- return null
- }
- },
- },
- }
- </script>
- <style scoped lang="scss">
- .dialog-footer {
- text-align: center;
- margin-top: 20px;
- .el-button {
- margin: 0 10px;
- }
- }
- ::v-deep .el-dialog__header {
- padding: 20px 20px 10px;
- .el-dialog__title {
- font-size: 18px;
- font-weight: 600;
- color: #303133;
- }
- }
- // 分类行样式
- ::v-deep .category-row {
- background-color: #f5f7fa !important;
- td {
- background-color: #f5f7fa !important;
- font-weight: bold;
- }
- .category-name {
- color: #409eff;
- font-weight: bold;
- }
- .category-seq {
- color: #409eff;
- font-weight: bold;
- }
- }
- // 表格输入框样式
- ::v-deep .el-table {
- .el-input {
- .el-input__inner {
- border: none;
- padding: 0 5px;
- text-align: center;
- &:focus {
- border: 1px solid #409eff;
- }
- }
- }
- }
- </style>
|