FixedTableDialog.vue 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084
  1. <template>
  2. <el-dialog
  3. title="调查表填报"
  4. :visible.sync="dialogVisible"
  5. width="90%"
  6. :close-on-click-modal="false"
  7. :show-close="true"
  8. append-to-body
  9. :modal="false"
  10. @close="handleClose"
  11. >
  12. <el-table
  13. :data="tableData"
  14. border
  15. style="width: 100%"
  16. :row-class-name="getRowClassName"
  17. >
  18. <!-- 序号列 -->
  19. <el-table-column prop="seq" label="序号" width="80" align="center">
  20. <template slot-scope="scope">
  21. <span>{{ scope.row.seq }}</span>
  22. </template>
  23. </el-table-column>
  24. <!-- 项目列(只读) -->
  25. <el-table-column
  26. prop="itemName"
  27. label="项目"
  28. min-width="200"
  29. align="left"
  30. >
  31. <template slot-scope="scope">
  32. <span v-if="scope.row.isCategory" class="category-name">
  33. {{ scope.row.itemName }}
  34. </span>
  35. <span v-else>{{ scope.row.itemName }}</span>
  36. </template>
  37. </el-table-column>
  38. <!-- 单位列(只读) -->
  39. <el-table-column prop="unit" label="单位" width="100" align="center">
  40. <template slot-scope="scope">
  41. <span v-if="!scope.row.isCategory">{{ scope.row.unit }}</span>
  42. </template>
  43. </el-table-column>
  44. <!-- 指标编号(cellCode,只读) -->
  45. <el-table-column
  46. prop="cellCode"
  47. label="指标编号"
  48. width="120"
  49. align="center"
  50. >
  51. <template slot-scope="scope">
  52. <span v-if="!scope.row.isCategory">{{ scope.row.cellCode }}</span>
  53. </template>
  54. </el-table-column>
  55. <!-- 计算方式(calculationFormula,只读) -->
  56. <el-table-column
  57. prop="calculationFormula"
  58. label="计算方式"
  59. min-width="180"
  60. align="left"
  61. >
  62. <template slot-scope="scope">
  63. <span v-if="!scope.row.isCategory">
  64. {{ scope.row.calculationFormula }}
  65. </span>
  66. </template>
  67. </el-table-column>
  68. <!-- 动态年份列 -->
  69. <el-table-column
  70. v-for="year in yearColumns"
  71. :key="year"
  72. :label="`${year}年`"
  73. :prop="`year_${year}`"
  74. width="150"
  75. align="center"
  76. >
  77. <template slot-scope="scope">
  78. <el-input
  79. v-if="!scope.row.isCategory"
  80. v-model="scope.row[`year_${year}`]"
  81. :placeholder="`请输入${year}年数据`"
  82. :disabled="isViewMode"
  83. @blur="handleCellBlur(scope.row, year)"
  84. @input="handleCellInput(scope.row, year)"
  85. />
  86. </template>
  87. </el-table-column>
  88. <!-- 备注列 -->
  89. <el-table-column prop="remark" label="备注" min-width="150" align="left">
  90. <template slot-scope="scope">
  91. <el-input
  92. v-if="!scope.row.isCategory"
  93. v-model="scope.row.remark"
  94. placeholder="请输入备注"
  95. :disabled="isViewMode"
  96. />
  97. </template>
  98. </el-table-column>
  99. </el-table>
  100. <div slot="footer" class="dialog-footer">
  101. <el-button type="primary" @click="handleSave">保存</el-button>
  102. <el-button @click="handleCancel">取消</el-button>
  103. </div>
  104. </el-dialog>
  105. </template>
  106. <script>
  107. import { Message } from 'element-ui'
  108. import { saveSingleRecordSurvey, getSurveyDetail } from '@/api/audit/survey'
  109. export default {
  110. name: 'FixedTableDialog',
  111. props: {
  112. visible: {
  113. type: Boolean,
  114. default: false,
  115. },
  116. surveyData: {
  117. type: Object,
  118. default: () => ({}),
  119. },
  120. // 表格数据配置
  121. // 格式: [
  122. // {
  123. // id: '1',
  124. // itemName: '班级数',
  125. // unit: '个',
  126. // isCategory: false,
  127. // parentId: null,
  128. // categoryId: 'III',
  129. // seq: 1,
  130. // validateRules: {
  131. // required: true,
  132. // type: 'number',
  133. // min: 0,
  134. // },
  135. // linkageRules: {
  136. // parent: 'III',
  137. // relation: 'sum',
  138. // },
  139. // },
  140. // {
  141. // id: 'III',
  142. // itemName: '在取做保职工总人数',
  143. // unit: '人',
  144. // isCategory: true,
  145. // categorySeq: 'III',
  146. // children: [...],
  147. // }
  148. // ]
  149. tableItems: {
  150. type: Array,
  151. default: () => [],
  152. },
  153. // 监审期间(年份数组)
  154. // 格式: ['2022', '2023', '2024']
  155. auditPeriods: {
  156. type: Array,
  157. default: () => [],
  158. },
  159. // 立项信息中的监审期间(优先使用)- 数组
  160. projectAuditPeriods: {
  161. type: Array,
  162. default: () => [],
  163. },
  164. // 立项信息中的监审期间(字符串,如 '2022,2023,2024' 或 '2022-2024')
  165. projectAuditPeriod: {
  166. type: String,
  167. default: '',
  168. },
  169. // 是否查看模式
  170. isViewMode: {
  171. type: Boolean,
  172. default: false,
  173. },
  174. // 被监审单位ID
  175. auditedUnitId: {
  176. type: String,
  177. default: '',
  178. },
  179. // 上传记录ID
  180. uploadId: {
  181. type: String,
  182. default: '',
  183. },
  184. // 成本调查表模板ID
  185. surveyTemplateId: {
  186. type: String,
  187. default: '',
  188. },
  189. // 目录ID
  190. catalogId: {
  191. type: String,
  192. default: '',
  193. },
  194. // 统一控制接口 type(1=成本调查表,2=报送资料)
  195. requestType: {
  196. type: [String, Number],
  197. default: 1,
  198. },
  199. // 任务ID
  200. taskId: {
  201. type: [String, Number],
  202. default: '',
  203. },
  204. },
  205. data() {
  206. return {
  207. dialogVisible: false,
  208. tableData: [],
  209. yearColumns: [],
  210. validationErrors: [],
  211. cellCodeIndex: {}, // cellCode -> row
  212. dependentsMap: {}, // cellCode -> Set(rows depending on it)
  213. }
  214. },
  215. watch: {
  216. visible: {
  217. async handler(newVal) {
  218. this.dialogVisible = newVal
  219. if (newVal) {
  220. // 先初始化表格数据
  221. this.initTableData()
  222. // 等待 DOM 更新后,如果有 uploadId,调用接口获取详情数据并回显
  223. await this.$nextTick()
  224. this.loadDetailData()
  225. }
  226. },
  227. immediate: true,
  228. },
  229. dialogVisible(newVal) {
  230. if (!newVal) {
  231. this.$emit('update:visible', false)
  232. }
  233. },
  234. tableItems: {
  235. handler() {
  236. if (this.dialogVisible) {
  237. this.initTableData()
  238. }
  239. },
  240. deep: true,
  241. },
  242. auditPeriods: {
  243. handler() {
  244. if (this.dialogVisible) {
  245. this.initYearColumns()
  246. this.initTableData()
  247. }
  248. },
  249. deep: true,
  250. },
  251. projectAuditPeriods: {
  252. handler() {
  253. if (this.dialogVisible) {
  254. this.initYearColumns()
  255. this.initTableData()
  256. }
  257. },
  258. deep: true,
  259. },
  260. projectAuditPeriod(newVal) {
  261. if (this.dialogVisible) {
  262. this.initYearColumns()
  263. this.initTableData()
  264. }
  265. },
  266. },
  267. mounted() {
  268. this.initYearColumns()
  269. },
  270. methods: {
  271. // 初始化年份列
  272. initYearColumns() {
  273. // 1) 优先使用立项信息中的监审期间(数组)
  274. if (this.projectAuditPeriods && this.projectAuditPeriods.length > 0) {
  275. this.yearColumns = this.projectAuditPeriods.map((period) => {
  276. if (typeof period === 'string' && period.includes('-')) {
  277. return period.split('-')[0]
  278. }
  279. return String(period)
  280. })
  281. } else if (this.projectAuditPeriod) {
  282. // 2) 立项信息中的监审期间(字符串)
  283. const periods = this.parseAuditPeriod(this.projectAuditPeriod)
  284. this.yearColumns = periods
  285. } else if (this.auditPeriods && this.auditPeriods.length > 0) {
  286. // 3) 组件入参的监审期间(数组)
  287. // 如果传入了监审期间,使用监审期间
  288. this.yearColumns = this.auditPeriods.map((period) => {
  289. // 如果是日期格式,提取年份
  290. if (typeof period === 'string' && period.includes('-')) {
  291. return period.split('-')[0]
  292. }
  293. return String(period)
  294. })
  295. } else if (this.surveyData && this.surveyData.auditPeriod) {
  296. // 如果从 surveyData 中获取监审期间
  297. const periods = this.parseAuditPeriod(this.surveyData.auditPeriod)
  298. this.yearColumns = periods
  299. } else {
  300. // 默认使用最近3年
  301. const currentYear = new Date().getFullYear()
  302. this.yearColumns = [
  303. String(currentYear - 2),
  304. String(currentYear - 1),
  305. String(currentYear),
  306. ]
  307. }
  308. },
  309. // 解析监审期间字符串(如 "2022,2023,2024" 或 "2022-2024")
  310. parseAuditPeriod(periodStr) {
  311. if (!periodStr) return []
  312. if (periodStr.includes(',')) {
  313. return periodStr.split(',').map((p) => p.trim())
  314. }
  315. if (periodStr.includes('-')) {
  316. const parts = periodStr.split('-')
  317. if (parts.length === 2) {
  318. const start = parseInt(parts[0].trim())
  319. const end = parseInt(parts[1].trim())
  320. const years = []
  321. for (let year = start; year <= end; year++) {
  322. years.push(String(year))
  323. }
  324. return years
  325. }
  326. }
  327. return [String(periodStr)]
  328. },
  329. // 初始化表格数据
  330. initTableData() {
  331. console.log(this.isViewMode, '只读')
  332. if (!this.tableItems || this.tableItems.length === 0) {
  333. // 如果没有传入数据,使用假数据
  334. this.tableData = this.getMockTableData()
  335. return
  336. }
  337. // 根据 parentid 关系排列:parentid 为 '-1' 的是父项,子项的 parentid 等于父项的 rowid/id
  338. const flatData = []
  339. const processedIds = new Set()
  340. // 先处理所有父项(parentid 为 '-1')
  341. this.tableItems.forEach((item) => {
  342. const parentid = item.parentid
  343. const rowid = item.rowid || item.id
  344. if (parentid === '-1' || parentid === -1) {
  345. const rowData = this.createRowData(item, false, rowid)
  346. flatData.push(rowData)
  347. processedIds.add(rowid)
  348. }
  349. })
  350. // 再处理所有子项,紧跟在父项后面,按正序排列
  351. // 先收集所有子项,按序号或顺序排序
  352. const childItems = this.tableItems.filter((item) => {
  353. const rowid = item.rowid || item.id
  354. const parentid = item.parentid
  355. return (
  356. !processedIds.has(rowid) && parentid !== '-1' && parentid !== -1
  357. )
  358. })
  359. // 对子项按序号正序排序
  360. childItems.sort((a, b) => {
  361. const seqA =
  362. a.seq !== undefined
  363. ? typeof a.seq === 'number'
  364. ? a.seq
  365. : parseInt(a.seq) || 0
  366. : 0
  367. const seqB =
  368. b.seq !== undefined
  369. ? typeof b.seq === 'number'
  370. ? b.seq
  371. : parseInt(b.seq) || 0
  372. : 0
  373. return seqA - seqB
  374. })
  375. // 需要多次遍历,确保所有子项都能找到父项
  376. let hasUnprocessed = true
  377. while (hasUnprocessed && childItems.length > 0) {
  378. hasUnprocessed = false
  379. const remainingItems = []
  380. childItems.forEach((item) => {
  381. const rowid = item.rowid || item.id
  382. const parentid = item.parentid
  383. // 跳过已处理的项
  384. if (processedIds.has(rowid)) {
  385. return
  386. }
  387. // 查找父项在 flatData 中的位置
  388. const parentIndex = flatData.findIndex((row) => {
  389. const parentRowid = row.rowid || row.id
  390. return parentRowid === parentid
  391. })
  392. if (parentIndex >= 0) {
  393. // 找到父项,插入到父项后面
  394. // 找到父项后面最后一个子项的位置
  395. let insertIndex = parentIndex + 1
  396. while (
  397. insertIndex < flatData.length &&
  398. (flatData[insertIndex].rowid || flatData[insertIndex].id) !==
  399. parentid &&
  400. flatData[insertIndex].parentid === parentid
  401. ) {
  402. insertIndex++
  403. }
  404. const rowData = this.createRowData(item, true, rowid)
  405. flatData.splice(insertIndex, 0, rowData)
  406. processedIds.add(rowid)
  407. } else {
  408. // 如果找不到父项,标记为未处理,等待下一轮
  409. remainingItems.push(item)
  410. hasUnprocessed = true
  411. }
  412. })
  413. // 更新待处理的子项列表
  414. childItems.length = 0
  415. childItems.push(...remainingItems)
  416. }
  417. // 处理剩余未找到父项的项(可能是数据异常)
  418. this.tableItems.forEach((item) => {
  419. const rowid = item.rowid || item.id
  420. if (!processedIds.has(rowid)) {
  421. const rowData = this.createRowData(item, false, rowid)
  422. flatData.push(rowData)
  423. processedIds.add(rowid)
  424. }
  425. })
  426. this.tableData = flatData
  427. // 构建公式索引并计算一次
  428. this.buildFormulaEngine()
  429. this.recomputeAllFormulas()
  430. },
  431. // 创建行数据
  432. createRowData(item, isChild, rowid) {
  433. const rowData = {
  434. ...item,
  435. rowid: rowid || item.rowid || item.id,
  436. seq:
  437. item.seq !== undefined
  438. ? item.seq
  439. : isChild
  440. ? ''
  441. : item.categorySeq || item.id || '',
  442. isCategory: item.isCategory || false,
  443. }
  444. // 初始化年份数据
  445. this.yearColumns.forEach((year) => {
  446. rowData[`year_${year}`] = item[`year_${year}`] || ''
  447. })
  448. // 初始化备注
  449. rowData.remark = item.remark || ''
  450. // 如果有传入的数据,填充值
  451. if (this.surveyData && this.surveyData[rowid]) {
  452. const savedData = this.surveyData[rowid]
  453. this.yearColumns.forEach((year) => {
  454. if (savedData[year] !== undefined) {
  455. rowData[`year_${year}`] = savedData[year]
  456. }
  457. })
  458. if (savedData.remark !== undefined) {
  459. rowData.remark = savedData.remark
  460. }
  461. }
  462. return rowData
  463. },
  464. // 加载详情数据并回显
  465. async loadDetailData() {
  466. // 如果有 uploadId 和 auditedUnitId,调用接口获取详情数据
  467. const uploadId =
  468. this.uploadId || this.surveyData.uploadId || this.surveyData.id
  469. const auditedUnitId =
  470. this.auditedUnitId || this.surveyData.auditedUnitId
  471. // 只要有 uploadId 就尝试获取回显数据
  472. if (uploadId) {
  473. try {
  474. const params = {
  475. uploadId: uploadId,
  476. auditedUnitId: auditedUnitId,
  477. type: this.requestType,
  478. }
  479. const res = await getSurveyDetail(params)
  480. console.log('固定表详情数据', res)
  481. if (res && res.code === 200 && res.value) {
  482. // 将接口返回的数据转换为表格数据格式
  483. const detailData = Array.isArray(res.value)
  484. ? res.value
  485. : res.value.items || res.value.records || []
  486. // 按 rowid 分组数据
  487. const dataByRowid = {}
  488. detailData.forEach((item) => {
  489. if (item.rowid) {
  490. if (!dataByRowid[item.rowid]) {
  491. dataByRowid[item.rowid] = {}
  492. }
  493. dataByRowid[item.rowid][item.rkey] = item.rvalue
  494. }
  495. })
  496. // 回显数据到表格
  497. this.tableData.forEach((row) => {
  498. const rowid = row.rowid || row.id
  499. const rowData = dataByRowid[rowid]
  500. if (rowData) {
  501. // 回显基本信息
  502. if (rowData['序号'] !== undefined) {
  503. row.seq = rowData['序号']
  504. }
  505. if (rowData['项目'] !== undefined) {
  506. row.itemName = rowData['项目']
  507. }
  508. if (rowData['单位'] !== undefined) {
  509. row.unit = rowData['单位']
  510. }
  511. if (rowData['备注'] !== undefined) {
  512. row.remark = rowData['备注']
  513. }
  514. // 回显年份数据
  515. this.yearColumns.forEach((year) => {
  516. if (rowData[year] !== undefined) {
  517. row[`year_${year}`] = rowData[year]
  518. }
  519. })
  520. }
  521. })
  522. // 强制更新视图
  523. this.$forceUpdate()
  524. // 回显后根据公式重算
  525. this.recomputeAllFormulas()
  526. }
  527. } catch (err) {
  528. console.error('获取固定表详情失败', err)
  529. }
  530. }
  531. },
  532. // 获取假数据(用于测试)
  533. getMockTableData() {
  534. return [
  535. {
  536. id: '1',
  537. itemName: '班级数',
  538. unit: '个',
  539. isCategory: false,
  540. seq: 1,
  541. year_2022: '',
  542. year_2023: '',
  543. year_2024: '',
  544. remark: '',
  545. },
  546. {
  547. id: '2',
  548. itemName: '幼儿学生人数',
  549. unit: '人',
  550. isCategory: false,
  551. seq: 2,
  552. year_2022: '',
  553. year_2023: '',
  554. year_2024: '',
  555. remark: '',
  556. },
  557. {
  558. id: 'III',
  559. itemName: '在取做保职工总人数',
  560. unit: '人',
  561. isCategory: true,
  562. categorySeq: 'III',
  563. seq: 'III',
  564. },
  565. {
  566. id: '3-1',
  567. itemName: '行政管理人员数',
  568. unit: '人',
  569. isCategory: false,
  570. categoryId: 'III',
  571. categorySeq: 'III',
  572. seq: 3,
  573. year_2022: '',
  574. year_2023: '',
  575. year_2024: '',
  576. remark: '',
  577. },
  578. {
  579. id: '3-2',
  580. itemName: '教师人数',
  581. unit: '人',
  582. isCategory: false,
  583. categoryId: 'III',
  584. categorySeq: 'III',
  585. seq: 4,
  586. year_2022: '',
  587. year_2023: '',
  588. year_2024: '',
  589. remark: '',
  590. },
  591. {
  592. id: '3-3',
  593. itemName: '保育员人数',
  594. unit: '人',
  595. isCategory: false,
  596. categoryId: 'III',
  597. categorySeq: 'III',
  598. seq: 5,
  599. year_2022: '',
  600. year_2023: '',
  601. year_2024: '',
  602. remark: '',
  603. },
  604. ]
  605. },
  606. // 获取行样式类名
  607. getRowClassName({ row }) {
  608. if (row.isCategory) {
  609. return 'category-row'
  610. }
  611. return ''
  612. },
  613. // 单元格输入事件
  614. handleCellInput(row, year) {
  615. // 实时验证勾稽关系
  616. this.validateLinkage(row, year)
  617. // 实时联动计算
  618. this.recomputeDependentsForRow(row, year)
  619. },
  620. // 单元格失焦事件
  621. handleCellBlur(row, year) {
  622. // 验证格式和非空
  623. this.validateCell(row, year)
  624. // 验证勾稽关系
  625. this.validateLinkage(row, year)
  626. // 失焦后再次联动计算,确保取整/格式等影响后结果一致
  627. this.recomputeDependentsForRow(row, year)
  628. },
  629. // 验证单元格(非空和格式验证)
  630. validateCell(row, year) {
  631. const fieldName = `year_${year}`
  632. const value = row[fieldName]
  633. // 非空验证
  634. if (row.validateRules && row.validateRules.required && !value) {
  635. this.showFieldError(
  636. row,
  637. year,
  638. `${row.itemName}的${year}年数据不能为空`
  639. )
  640. return false
  641. }
  642. // 格式验证
  643. if (value && row.validateRules) {
  644. if (row.validateRules.type === 'number') {
  645. const numValue = Number(value)
  646. if (isNaN(numValue)) {
  647. this.showFieldError(
  648. row,
  649. year,
  650. `${row.itemName}的${year}年数据必须是数字`
  651. )
  652. return false
  653. }
  654. if (
  655. row.validateRules.min !== undefined &&
  656. numValue < row.validateRules.min
  657. ) {
  658. this.showFieldError(
  659. row,
  660. year,
  661. `${row.itemName}的${year}年数据不能小于${row.validateRules.min}`
  662. )
  663. return false
  664. }
  665. if (
  666. row.validateRules.max !== undefined &&
  667. numValue > row.validateRules.max
  668. ) {
  669. this.showFieldError(
  670. row,
  671. year,
  672. `${row.itemName}的${year}年数据不能大于${row.validateRules.max}`
  673. )
  674. return false
  675. }
  676. }
  677. }
  678. return true
  679. },
  680. // 验证勾稽关系
  681. validateLinkage(row, year) {
  682. if (!row.linkageRules) return true
  683. const { parent, relation } = row.linkageRules
  684. if (relation === 'sum') {
  685. // 验证子项之和等于父项
  686. const parentRow = this.tableData.find((r) => r.id === parent)
  687. if (!parentRow || parentRow.isCategory) return true
  688. const children = this.tableData.filter(
  689. (r) => r.categoryId === parent && !r.isCategory
  690. )
  691. const parentValue = Number(parentRow[`year_${year}`]) || 0
  692. const sumValue = children.reduce((sum, child) => {
  693. return sum + (Number(child[`year_${year}`]) || 0)
  694. }, 0)
  695. if (parentValue !== sumValue) {
  696. // 可以选择自动修正或提示错误
  697. // 这里仅提示,不自动修正
  698. console.warn(
  699. `${parentRow.itemName}的${year}年数据(${parentValue})与子项之和(${sumValue})不相等`
  700. )
  701. }
  702. }
  703. return true
  704. },
  705. // 显示字段错误
  706. showFieldError(row, year, message) {
  707. // 这里可以添加更详细的错误提示
  708. console.error(message)
  709. },
  710. // 保存前验证
  711. validateBeforeSave() {
  712. this.validationErrors = []
  713. let isValid = true
  714. // 验证所有单元格
  715. this.tableData.forEach((row) => {
  716. if (row.isCategory) return
  717. this.yearColumns.forEach((year) => {
  718. const cellValid = this.validateCell(row, year)
  719. if (!cellValid) {
  720. isValid = false
  721. this.validationErrors.push({
  722. row: row.itemName,
  723. year,
  724. message: `${row.itemName}的${year}年数据验证失败`,
  725. })
  726. }
  727. })
  728. // 验证勾稽关系
  729. this.yearColumns.forEach((year) => {
  730. const linkageValid = this.validateLinkage(row, year)
  731. if (!linkageValid) {
  732. isValid = false
  733. }
  734. })
  735. })
  736. // 验证所有勾稽关系
  737. this.validateAllLinkages()
  738. return isValid
  739. },
  740. // 验证所有勾稽关系
  741. validateAllLinkages() {
  742. const categories = this.tableData.filter((r) => r.isCategory)
  743. categories.forEach((category) => {
  744. const children = this.tableData.filter(
  745. (r) => r.categoryId === category.id && !r.isCategory
  746. )
  747. this.yearColumns.forEach((year) => {
  748. const categoryValue = Number(category[`year_${year}`]) || 0
  749. const sumValue = children.reduce((sum, child) => {
  750. return sum + (Number(child[`year_${year}`]) || 0)
  751. }, 0)
  752. if (categoryValue !== 0 && categoryValue !== sumValue) {
  753. this.validationErrors.push({
  754. row: category.itemName,
  755. year,
  756. message: `${category.itemName}的${year}年数据(${categoryValue})与子项之和(${sumValue})不相等`,
  757. })
  758. }
  759. })
  760. })
  761. },
  762. // 关闭弹窗
  763. handleClose() {
  764. this.dialogVisible = false
  765. this.$emit('update:visible', false)
  766. },
  767. // 取消
  768. handleCancel() {
  769. this.handleClose()
  770. },
  771. // 保存
  772. async handleSave() {
  773. // 验证数据
  774. if (!this.validateBeforeSave()) {
  775. const errorMessages = this.validationErrors
  776. .map((err) => `${err.row}的${err.year}年:${err.message}`)
  777. .join('\n')
  778. Message.error('数据验证失败:\n' + errorMessages)
  779. return
  780. }
  781. try {
  782. // 判断是否有数据(编辑模式):如果有 uploadId,说明是编辑已有数据
  783. const hasData = !!(
  784. this.uploadId ||
  785. this.surveyData.uploadId ||
  786. this.surveyData.id
  787. )
  788. // 格式化保存数据为接口需要的格式
  789. const saveData = []
  790. // 遍历所有行数据
  791. this.tableData.forEach((row) => {
  792. const rowid = row.rowid || row.id
  793. // 跳过分类行(如果分类行不需要保存基本信息)
  794. if (!row.isCategory) {
  795. // 保存基本信息:序号、项目、单位
  796. if (row.seq !== undefined && row.seq !== null && row.seq !== '') {
  797. saveData.push({
  798. rowid: rowid,
  799. rkey: '序号',
  800. rvalue: String(row.seq),
  801. })
  802. }
  803. if (row.itemName) {
  804. saveData.push({
  805. rowid: rowid,
  806. rkey: '项目',
  807. rvalue: String(row.itemName),
  808. })
  809. }
  810. if (row.unit) {
  811. saveData.push({
  812. rowid: rowid,
  813. rkey: '单位',
  814. rvalue: String(row.unit),
  815. })
  816. }
  817. }
  818. // 保存年份数据(所有行都可以有年份数据)
  819. this.yearColumns.forEach((year) => {
  820. const yearValue = row[`year_${year}`]
  821. if (
  822. yearValue !== undefined &&
  823. yearValue !== null &&
  824. yearValue !== ''
  825. ) {
  826. saveData.push({
  827. rowid: rowid,
  828. rkey: String(year),
  829. rvalue: String(yearValue),
  830. })
  831. }
  832. })
  833. // 保存备注(所有行都可以有备注)
  834. if (
  835. row.remark !== undefined &&
  836. row.remark !== null &&
  837. row.remark !== ''
  838. ) {
  839. saveData.push({
  840. rowid: rowid,
  841. rkey: '备注',
  842. rvalue: String(row.remark),
  843. })
  844. }
  845. })
  846. // 为每条数据添加公共字段
  847. const finalSaveData = saveData.map((item) => {
  848. const dataItem = {
  849. ...item,
  850. auditedUnitId:
  851. this.auditedUnitId || this.surveyData.auditedUnitId || '',
  852. surveyTemplateId:
  853. this.surveyTemplateId || this.surveyData.surveyTemplateId || '',
  854. catalogId: this.catalogId || this.surveyData.catalogId || '',
  855. taskId: this.taskId || this.surveyData.taskId || '',
  856. type: this.requestType,
  857. }
  858. // 如果有数据(编辑模式),添加 uploadId 字段
  859. if (hasData) {
  860. dataItem.uploadId =
  861. this.uploadId ||
  862. this.surveyData.uploadId ||
  863. this.surveyData.id ||
  864. ''
  865. }
  866. return dataItem
  867. })
  868. console.log('保存数据:', finalSaveData)
  869. // 调用保存接口
  870. const res = await saveSingleRecordSurvey(finalSaveData)
  871. if (res && res.code === 200) {
  872. Message.success('保存成功')
  873. // 触发保存事件
  874. this.$emit('save', finalSaveData)
  875. // 触发刷新事件
  876. this.$emit('refresh')
  877. this.handleClose()
  878. } else {
  879. Message.error(res.message || '保存失败')
  880. }
  881. } catch (err) {
  882. console.error('保存失败', err)
  883. // Message.error(err.message || '保存失败')
  884. }
  885. },
  886. // 构建 cellCode 索引与依赖图
  887. buildFormulaEngine() {
  888. const index = {}
  889. const deps = {}
  890. this.tableData.forEach((row) => {
  891. if (row && row.cellCode) {
  892. index[row.cellCode] = row
  893. }
  894. })
  895. // 构建依赖映射:A1 -> [依赖 A1 的行...]
  896. this.tableData.forEach((row) => {
  897. if (!row || !row.calculationFormula) return
  898. const codes =
  899. row.calculationFormula.toString().match(/[A-Z]+\d+/g) || []
  900. codes.forEach((code) => {
  901. if (!deps[code]) deps[code] = new Set()
  902. deps[code].add(row)
  903. })
  904. })
  905. this.cellCodeIndex = index
  906. this.dependentsMap = deps
  907. },
  908. // 重新计算一个公式行在某年的值
  909. recomputeRowForYear(row, year) {
  910. if (!row || !row.calculationFormula) return
  911. const expr = row.calculationFormula.toString()
  912. // 替换变量为对应年的数值
  913. const replaced = expr.replace(/[A-Z]+\d+/g, (code) => {
  914. const refRow = this.cellCodeIndex[code]
  915. const v = refRow ? Number(refRow[`year_${year}`]) : 0
  916. return isNaN(v) ? '0' : String(v)
  917. })
  918. // 仅允许数字与运算符
  919. const safe = this.safeEvalExpression(replaced)
  920. if (safe !== null && !isNaN(safe)) {
  921. row[`year_${year}`] = String(safe)
  922. }
  923. },
  924. // 在某年针对一个输入行,联动所有依赖它的行
  925. recomputeDependentsForRow(row, year) {
  926. if (!row) return
  927. const code = row.cellCode
  928. if (!code) return
  929. const dependents = this.dependentsMap[code]
  930. if (!dependents || dependents.size === 0) return
  931. dependents.forEach((depRow) => this.recomputeRowForYear(depRow, year))
  932. // 可能存在链式依赖,递归触发
  933. dependents.forEach((depRow) =>
  934. this.recomputeDependentsForRow(depRow, year)
  935. )
  936. },
  937. // 对所有含公式的行、所有年重算
  938. recomputeAllFormulas() {
  939. if (!this.yearColumns || this.yearColumns.length === 0) return
  940. const formulaRows = this.tableData.filter(
  941. (r) => r && r.calculationFormula
  942. )
  943. this.yearColumns.forEach((year) => {
  944. formulaRows.forEach((r) => this.recomputeRowForYear(r, year))
  945. })
  946. this.$forceUpdate()
  947. },
  948. // 简单安全表达式求值:仅支持数字、+ - * / 和括号
  949. safeEvalExpression(expr) {
  950. const cleaned = expr.replace(/\s+/g, '')
  951. if (!/^[-+*/().\d]+$/.test(cleaned)) {
  952. // 存在不允许的字符,放弃计算
  953. return null
  954. }
  955. try {
  956. // eslint-disable-next-line no-new-func
  957. const fn = new Function(`return (${cleaned})`)
  958. const res = fn()
  959. return typeof res === 'number' && isFinite(res) ? res : null
  960. } catch (e) {
  961. return null
  962. }
  963. },
  964. },
  965. }
  966. </script>
  967. <style scoped lang="scss">
  968. .dialog-footer {
  969. text-align: center;
  970. margin-top: 20px;
  971. .el-button {
  972. margin: 0 10px;
  973. }
  974. }
  975. ::v-deep .el-dialog__header {
  976. padding: 20px 20px 10px;
  977. .el-dialog__title {
  978. font-size: 18px;
  979. font-weight: 600;
  980. color: #303133;
  981. }
  982. }
  983. // 分类行样式
  984. ::v-deep .category-row {
  985. background-color: #f5f7fa !important;
  986. td {
  987. background-color: #f5f7fa !important;
  988. font-weight: bold;
  989. }
  990. .category-name {
  991. color: #409eff;
  992. font-weight: bold;
  993. }
  994. .category-seq {
  995. color: #409eff;
  996. font-weight: bold;
  997. }
  998. }
  999. // 表格输入框样式
  1000. ::v-deep .el-table {
  1001. .el-input {
  1002. .el-input__inner {
  1003. border: none;
  1004. padding: 0 5px;
  1005. text-align: center;
  1006. &:focus {
  1007. border: 1px solid #409eff;
  1008. }
  1009. }
  1010. }
  1011. }
  1012. </style>