FixedTableDialog.vue 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293
  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. inputmode="decimal"
  82. :placeholder="`请输入${year}年数据`"
  83. :disabled="isViewMode"
  84. style="width: 100%"
  85. @input="handleNumericInput(scope.row, year, $event)"
  86. @blur="handleCellBlur(scope.row, year)"
  87. />
  88. </template>
  89. </el-table-column>
  90. <!-- 备注列 -->
  91. <el-table-column prop="remark" label="备注" min-width="150" align="left">
  92. <template slot-scope="scope">
  93. <el-input
  94. v-if="!scope.row.isCategory"
  95. v-model="scope.row.remark"
  96. placeholder="请输入备注"
  97. :disabled="isViewMode"
  98. />
  99. </template>
  100. </el-table-column>
  101. </el-table>
  102. <div slot="footer" class="dialog-footer">
  103. <el-button type="primary" @click="handleSave">保存</el-button>
  104. <el-button @click="handleCancel">取消</el-button>
  105. </div>
  106. </el-dialog>
  107. </template>
  108. <script>
  109. import { Message } from 'element-ui'
  110. import { saveSingleRecordSurvey, getSurveyDetail } from '@/api/audit/survey'
  111. export default {
  112. name: 'FixedTableDialog',
  113. props: {
  114. visible: {
  115. type: Boolean,
  116. default: false,
  117. },
  118. surveyData: {
  119. type: Object,
  120. default: () => ({}),
  121. },
  122. // 表格数据配置
  123. // 格式: [
  124. // {
  125. // id: '1',
  126. // itemName: '班级数',
  127. // unit: '个',
  128. // isCategory: false,
  129. // parentId: null,
  130. // categoryId: 'III',
  131. // seq: 1,
  132. // validateRules: {
  133. // required: true,
  134. // type: 'number',
  135. // min: 0,
  136. // },
  137. // linkageRules: {
  138. // parent: 'III',
  139. // relation: 'sum',
  140. // },
  141. // },
  142. // {
  143. // id: 'III',
  144. // itemName: '在取做保职工总人数',
  145. // unit: '人',
  146. // isCategory: true,
  147. // categorySeq: 'III',
  148. // children: [...],
  149. // }
  150. // ]
  151. tableItems: {
  152. type: Array,
  153. default: () => [],
  154. },
  155. // 监审期间(年份数组)
  156. // 格式: ['2022', '2023', '2024']
  157. auditPeriods: {
  158. type: Array,
  159. default: () => [],
  160. },
  161. // 立项信息中的监审期间(优先使用)- 数组
  162. projectAuditPeriods: {
  163. type: Array,
  164. default: () => [],
  165. },
  166. // 立项信息中的监审期间(字符串,如 '2022,2023,2024' 或 '2022-2024')
  167. projectAuditPeriod: {
  168. type: String,
  169. default: '',
  170. },
  171. // 是否查看模式
  172. isViewMode: {
  173. type: Boolean,
  174. default: false,
  175. },
  176. // 被监审单位ID
  177. auditedUnitId: {
  178. type: String,
  179. default: '',
  180. },
  181. // 上传记录ID
  182. uploadId: {
  183. type: String,
  184. default: '',
  185. },
  186. // 成本调查表模板ID
  187. surveyTemplateId: {
  188. type: String,
  189. default: '',
  190. },
  191. // 目录ID
  192. catalogId: {
  193. type: String,
  194. default: '',
  195. },
  196. // 统一控制接口 type(1=成本调查表,2=报送资料)
  197. requestType: {
  198. type: [String, Number],
  199. default: 1,
  200. },
  201. // 任务ID
  202. taskId: {
  203. type: [String, Number],
  204. default: '',
  205. },
  206. },
  207. data() {
  208. return {
  209. dialogVisible: false,
  210. tableData: [],
  211. yearColumns: [],
  212. validationErrors: [],
  213. cellCodeIndex: {}, // cellCode -> row
  214. dependentsMap: {}, // cellCode -> Set(rows depending on it)
  215. }
  216. },
  217. watch: {
  218. visible: {
  219. async handler(newVal) {
  220. this.dialogVisible = newVal
  221. if (newVal) {
  222. // 先初始化表格数据
  223. this.initTableData()
  224. // 等待 DOM 更新后,如果有 uploadId,调用接口获取详情数据并回显
  225. await this.$nextTick()
  226. this.loadDetailData()
  227. }
  228. },
  229. immediate: true,
  230. },
  231. dialogVisible(newVal) {
  232. if (!newVal) {
  233. this.$emit('update:visible', false)
  234. }
  235. },
  236. tableItems: {
  237. handler() {
  238. if (this.dialogVisible) {
  239. this.initTableData()
  240. }
  241. },
  242. deep: true,
  243. },
  244. auditPeriods: {
  245. handler() {
  246. if (this.dialogVisible) {
  247. this.initYearColumns()
  248. this.initTableData()
  249. }
  250. },
  251. deep: true,
  252. },
  253. projectAuditPeriods: {
  254. handler() {
  255. if (this.dialogVisible) {
  256. this.initYearColumns()
  257. this.initTableData()
  258. }
  259. },
  260. deep: true,
  261. },
  262. projectAuditPeriod(newVal) {
  263. if (this.dialogVisible) {
  264. this.initYearColumns()
  265. this.initTableData()
  266. }
  267. },
  268. },
  269. mounted() {
  270. this.initYearColumns()
  271. },
  272. methods: {
  273. // 初始化年份列
  274. initYearColumns() {
  275. // 1) 优先使用立项信息中的监审期间(数组)
  276. if (this.projectAuditPeriods && this.projectAuditPeriods.length > 0) {
  277. this.yearColumns = this.projectAuditPeriods.map((period) => {
  278. if (typeof period === 'string' && period.includes('-')) {
  279. return period.split('-')[0]
  280. }
  281. return String(period)
  282. })
  283. } else if (this.projectAuditPeriod) {
  284. // 2) 立项信息中的监审期间(字符串)
  285. const periods = this.parseAuditPeriod(this.projectAuditPeriod)
  286. this.yearColumns = periods
  287. } else if (this.auditPeriods && this.auditPeriods.length > 0) {
  288. // 3) 组件入参的监审期间(数组)
  289. // 如果传入了监审期间,使用监审期间
  290. this.yearColumns = this.auditPeriods.map((period) => {
  291. // 如果是日期格式,提取年份
  292. if (typeof period === 'string' && period.includes('-')) {
  293. return period.split('-')[0]
  294. }
  295. return String(period)
  296. })
  297. } else if (this.surveyData && this.surveyData.auditPeriod) {
  298. // 如果从 surveyData 中获取监审期间
  299. const periods = this.parseAuditPeriod(this.surveyData.auditPeriod)
  300. this.yearColumns = periods
  301. } else {
  302. // 默认使用最近3年
  303. const currentYear = new Date().getFullYear()
  304. this.yearColumns = [
  305. String(currentYear - 2),
  306. String(currentYear - 1),
  307. String(currentYear),
  308. ]
  309. }
  310. },
  311. // 解析监审期间字符串(如 "2022,2023,2024" 或 "2022-2024")
  312. parseAuditPeriod(periodStr) {
  313. if (!periodStr) return []
  314. if (periodStr.includes(',')) {
  315. return periodStr.split(',').map((p) => p.trim())
  316. }
  317. if (periodStr.includes('-')) {
  318. const parts = periodStr.split('-')
  319. if (parts.length === 2) {
  320. const start = parseInt(parts[0].trim())
  321. const end = parseInt(parts[1].trim())
  322. const years = []
  323. for (let year = start; year <= end; year++) {
  324. years.push(String(year))
  325. }
  326. return years
  327. }
  328. }
  329. return [String(periodStr)]
  330. },
  331. // 初始化表格数据
  332. initTableData() {
  333. console.log(this.isViewMode, '只读')
  334. if (!this.tableItems || this.tableItems.length === 0) {
  335. // 如果没有传入数据,使用假数据
  336. this.tableData = this.getMockTableData()
  337. return
  338. }
  339. // 根据 parentid 关系排列:parentid 为 '-1' 的是父项,子项的 parentid 等于父项的 rowid/id
  340. const flatData = []
  341. const processedIds = new Set()
  342. // 先处理所有父项(parentid 为 '-1')
  343. this.tableItems.forEach((item) => {
  344. const parentid = item.parentid
  345. const rowid = item.rowid || item.id
  346. if (parentid === '-1' || parentid === -1) {
  347. const rowData = this.createRowData(item, false, rowid)
  348. flatData.push(rowData)
  349. processedIds.add(rowid)
  350. }
  351. })
  352. // 再处理所有子项,紧跟在父项后面,按正序排列
  353. // 先收集所有子项,按序号或顺序排序
  354. const childItems = this.tableItems.filter((item) => {
  355. const rowid = item.rowid || item.id
  356. const parentid = item.parentid
  357. return (
  358. !processedIds.has(rowid) && parentid !== '-1' && parentid !== -1
  359. )
  360. })
  361. // 对子项按序号正序排序
  362. childItems.sort((a, b) => {
  363. const seqA =
  364. a.seq !== undefined
  365. ? typeof a.seq === 'number'
  366. ? a.seq
  367. : parseInt(a.seq) || 0
  368. : 0
  369. const seqB =
  370. b.seq !== undefined
  371. ? typeof b.seq === 'number'
  372. ? b.seq
  373. : parseInt(b.seq) || 0
  374. : 0
  375. return seqA - seqB
  376. })
  377. // 需要多次遍历,确保所有子项都能找到父项
  378. let hasUnprocessed = true
  379. while (hasUnprocessed && childItems.length > 0) {
  380. hasUnprocessed = false
  381. const remainingItems = []
  382. childItems.forEach((item) => {
  383. const rowid = item.rowid || item.id
  384. const parentid = item.parentid
  385. // 跳过已处理的项
  386. if (processedIds.has(rowid)) {
  387. return
  388. }
  389. // 查找父项在 flatData 中的位置
  390. const parentIndex = flatData.findIndex((row) => {
  391. const parentRowid = row.rowid || row.id
  392. return parentRowid === parentid
  393. })
  394. if (parentIndex >= 0) {
  395. // 找到父项,插入到父项后面
  396. // 找到父项后面最后一个子项的位置
  397. let insertIndex = parentIndex + 1
  398. while (
  399. insertIndex < flatData.length &&
  400. (flatData[insertIndex].rowid || flatData[insertIndex].id) !==
  401. parentid &&
  402. flatData[insertIndex].parentid === parentid
  403. ) {
  404. insertIndex++
  405. }
  406. const rowData = this.createRowData(item, true, rowid)
  407. flatData.splice(insertIndex, 0, rowData)
  408. processedIds.add(rowid)
  409. } else {
  410. // 如果找不到父项,标记为未处理,等待下一轮
  411. remainingItems.push(item)
  412. hasUnprocessed = true
  413. }
  414. })
  415. // 更新待处理的子项列表
  416. childItems.length = 0
  417. childItems.push(...remainingItems)
  418. }
  419. // 处理剩余未找到父项的项(可能是数据异常)
  420. this.tableItems.forEach((item) => {
  421. const rowid = item.rowid || item.id
  422. if (!processedIds.has(rowid)) {
  423. const rowData = this.createRowData(item, false, rowid)
  424. flatData.push(rowData)
  425. processedIds.add(rowid)
  426. }
  427. })
  428. this.tableData = flatData
  429. // 构建公式索引并计算一次
  430. this.buildFormulaEngine()
  431. this.recomputeAllFormulas()
  432. },
  433. // 创建行数据
  434. createRowData(item, isChild, rowid) {
  435. const rowData = {
  436. ...item,
  437. rowid: rowid || item.rowid || item.id,
  438. seq:
  439. item.seq !== undefined
  440. ? item.seq
  441. : isChild
  442. ? ''
  443. : item.categorySeq || item.id || '',
  444. isCategory: item.isCategory || false,
  445. }
  446. // 初始化年份数据
  447. this.yearColumns.forEach((year) => {
  448. rowData[`year_${year}`] = item[`year_${year}`] || ''
  449. })
  450. // 初始化备注
  451. rowData.remark = item.remark || ''
  452. // 如果有传入的数据,填充值
  453. if (this.surveyData && this.surveyData[rowid]) {
  454. const savedData = this.surveyData[rowid]
  455. this.yearColumns.forEach((year) => {
  456. if (savedData[year] !== undefined) {
  457. rowData[`year_${year}`] = savedData[year]
  458. }
  459. })
  460. if (savedData.remark !== undefined) {
  461. rowData.remark = savedData.remark
  462. }
  463. }
  464. return rowData
  465. },
  466. // 加载详情数据并回显
  467. async loadDetailData() {
  468. // 如果有 uploadId 和 auditedUnitId,调用接口获取详情数据
  469. const uploadId =
  470. this.uploadId || this.surveyData.uploadId || this.surveyData.id
  471. const auditedUnitId =
  472. this.auditedUnitId || this.surveyData.auditedUnitId
  473. // 只要有 uploadId 就尝试获取回显数据
  474. if (uploadId) {
  475. try {
  476. const params = {
  477. uploadId: uploadId,
  478. auditedUnitId: auditedUnitId,
  479. type: this.requestType,
  480. }
  481. const res = await getSurveyDetail(params)
  482. console.log('固定表详情数据', res)
  483. if (res && res.code === 200 && res.value) {
  484. // 将接口返回的数据转换为表格数据格式
  485. const detailData = Array.isArray(res.value)
  486. ? res.value
  487. : res.value.items || res.value.records || []
  488. // 按 rowid 分组数据
  489. const dataByRowid = {}
  490. detailData.forEach((item) => {
  491. if (item.rowid) {
  492. if (!dataByRowid[item.rowid]) {
  493. dataByRowid[item.rowid] = {}
  494. }
  495. dataByRowid[item.rowid][item.rkey] = item.rvalue
  496. }
  497. })
  498. // 回显数据到表格
  499. this.tableData.forEach((row) => {
  500. const rowid = row.rowid || row.id
  501. const rowData = dataByRowid[rowid]
  502. if (rowData) {
  503. // 回显基本信息
  504. if (rowData['序号'] !== undefined) {
  505. row.seq = rowData['序号']
  506. }
  507. if (rowData['项目'] !== undefined) {
  508. row.itemName = rowData['项目']
  509. }
  510. if (rowData['单位'] !== undefined) {
  511. row.unit = rowData['单位']
  512. }
  513. if (rowData['备注'] !== undefined) {
  514. row.remark = rowData['备注']
  515. }
  516. // 回显年份数据
  517. this.yearColumns.forEach((year) => {
  518. if (rowData[year] !== undefined) {
  519. row[`year_${year}`] = rowData[year]
  520. }
  521. })
  522. }
  523. })
  524. // 强制更新视图
  525. this.$forceUpdate()
  526. // 回显后根据公式重算
  527. this.recomputeAllFormulas()
  528. }
  529. } catch (err) {
  530. console.error('获取固定表详情失败', err)
  531. }
  532. }
  533. },
  534. // 获取假数据(用于测试)
  535. getMockTableData() {
  536. return [
  537. {
  538. id: '1',
  539. itemName: '班级数',
  540. unit: '个',
  541. isCategory: false,
  542. seq: 1,
  543. year_2022: '',
  544. year_2023: '',
  545. year_2024: '',
  546. remark: '',
  547. },
  548. {
  549. id: '2',
  550. itemName: '幼儿学生人数',
  551. unit: '人',
  552. isCategory: false,
  553. seq: 2,
  554. year_2022: '',
  555. year_2023: '',
  556. year_2024: '',
  557. remark: '',
  558. },
  559. {
  560. id: 'III',
  561. itemName: '在取做保职工总人数',
  562. unit: '人',
  563. isCategory: true,
  564. categorySeq: 'III',
  565. seq: 'III',
  566. },
  567. {
  568. id: '3-1',
  569. itemName: '行政管理人员数',
  570. unit: '人',
  571. isCategory: false,
  572. categoryId: 'III',
  573. categorySeq: 'III',
  574. seq: 3,
  575. year_2022: '',
  576. year_2023: '',
  577. year_2024: '',
  578. remark: '',
  579. },
  580. {
  581. id: '3-2',
  582. itemName: '教师人数',
  583. unit: '人',
  584. isCategory: false,
  585. categoryId: 'III',
  586. categorySeq: 'III',
  587. seq: 4,
  588. year_2022: '',
  589. year_2023: '',
  590. year_2024: '',
  591. remark: '',
  592. },
  593. {
  594. id: '3-3',
  595. itemName: '保育员人数',
  596. unit: '人',
  597. isCategory: false,
  598. categoryId: 'III',
  599. categorySeq: 'III',
  600. seq: 5,
  601. year_2022: '',
  602. year_2023: '',
  603. year_2024: '',
  604. remark: '',
  605. },
  606. ]
  607. },
  608. // 获取行样式类名
  609. getRowClassName({ row }) {
  610. if (row.isCategory) {
  611. return 'category-row'
  612. }
  613. return ''
  614. },
  615. // 单元格输入事件
  616. handleCellInput(row, year) {
  617. // 实时验证勾稽关系
  618. this.validateLinkage(row, year)
  619. // 实时联动计算
  620. this.recomputeDependentsForRow(row, year)
  621. },
  622. // 仅数字输入:按行规则限制小数位;整数不允许小数
  623. handleNumericInput(row, year, val) {
  624. if (!row) return
  625. const rules = (row && row.validateRules) || {}
  626. const field = `year_${year}`
  627. let s = String(val == null ? '' : val)
  628. // 允许负号和小数点,其余去除
  629. s = s.replace(/[^0-9+\-\.]/g, '')
  630. // 只保留第一个负号在最前
  631. s = s.replace(/(?!^)-/g, '')
  632. if (s.startsWith('+')) s = s.slice(1)
  633. // 多个点只保留第一个
  634. const firstDot = s.indexOf('.')
  635. if (firstDot >= 0) {
  636. s =
  637. s.slice(0, firstDot + 1) + s.slice(firstDot + 1).replace(/\./g, '')
  638. }
  639. const t = String(rules.type || '').toLowerCase()
  640. const isInteger =
  641. t === 'integer' ||
  642. (t === 'number' &&
  643. (rules.fieldTypenointlen === 0 || rules.decimalLength === 0))
  644. if (isInteger && s.includes('.')) {
  645. s = s.split('.')[0]
  646. }
  647. if (!isInteger) {
  648. const dlen =
  649. rules.fieldTypenointlen ||
  650. rules.fieldTypeNointLen ||
  651. rules.decimalLength
  652. if (typeof dlen === 'number' && dlen >= 0 && firstDot >= 0) {
  653. const parts = s.split('.')
  654. parts[1] = (parts[1] || '').slice(0, dlen)
  655. s = parts.join('.')
  656. }
  657. }
  658. // 写回且触发联动
  659. this.$set(row, field, s)
  660. this.handleCellInput(row, year)
  661. },
  662. // 单元格失焦事件
  663. handleCellBlur(row, year) {
  664. // 验证格式和非空
  665. this.validateCell(row, year)
  666. // 验证勾稽关系
  667. this.validateLinkage(row, year)
  668. // 失焦后再次联动计算,确保取整/格式等影响后结果一致
  669. this.recomputeDependentsForRow(row, year)
  670. },
  671. // 验证单元格(非空和格式验证)
  672. validateCell(row, year) {
  673. const fieldName = `year_${year}`
  674. const value = row[fieldName]
  675. // 非空验证
  676. if (row.validateRules && row.validateRules.required && !value) {
  677. this.showFieldError(
  678. row,
  679. year,
  680. `${row.itemName}的${year}年数据不能为空`
  681. )
  682. return false
  683. }
  684. // 类型/规则验证:支持 integer/decimal/number/boolean/date/dict
  685. if (
  686. value !== undefined &&
  687. value !== null &&
  688. value !== '' &&
  689. row.validateRules
  690. ) {
  691. const r = row.validateRules || {}
  692. const rawStr = String(value).trim()
  693. const type = String(r.type || '').toLowerCase()
  694. // 数值范围通用检查
  695. const checkRange = (numVal) => {
  696. if (r.min !== undefined && numVal < r.min) {
  697. this.showFieldError(
  698. row,
  699. year,
  700. `${row.itemName}的${year}年数据不能小于${r.min}`
  701. )
  702. return false
  703. }
  704. if (r.max !== undefined && numVal > r.max) {
  705. this.showFieldError(
  706. row,
  707. year,
  708. `${row.itemName}的${year}年数据不能大于${r.max}`
  709. )
  710. return false
  711. }
  712. return true
  713. }
  714. if (
  715. type === 'integer' ||
  716. (type === 'number' &&
  717. (r.fieldTypenointlen === 0 || r.decimalLength === 0))
  718. ) {
  719. // 仅整数;可选长度限制 fieldTypelen/fieldTypelen 别名
  720. const intPattern = /^-?\d+$/
  721. if (!intPattern.test(rawStr)) {
  722. this.showFieldError(
  723. row,
  724. year,
  725. `${row.itemName}的${year}年数据必须为整数`
  726. )
  727. return false
  728. }
  729. const limit = r.fieldTypelen || r.fieldTypeLen || r.totalLength
  730. if (limit && rawStr.replace('-', '').length > Number(limit)) {
  731. this.showFieldError(
  732. row,
  733. year,
  734. `${row.itemName}的${year}年整数长度不能超过${limit}位`
  735. )
  736. return false
  737. }
  738. const numVal = Number(rawStr)
  739. if (!checkRange(numVal)) return false
  740. } else if (
  741. type === 'decimal' ||
  742. type === 'double' ||
  743. type === 'float' ||
  744. type === 'number'
  745. ) {
  746. const numVal = Number(rawStr)
  747. if (Number.isNaN(numVal)) {
  748. this.showFieldError(
  749. row,
  750. year,
  751. `${row.itemName}的${year}年数据必须为数字`
  752. )
  753. return false
  754. }
  755. const dlen =
  756. r.fieldTypenointlen || r.fieldTypeNointLen || r.decimalLength
  757. if (dlen !== undefined && dlen !== null) {
  758. const decimals = rawStr.split('.')[1] || ''
  759. if (decimals.length > Number(dlen)) {
  760. this.showFieldError(
  761. row,
  762. year,
  763. `${row.itemName}的${year}年数据小数位不能超过${dlen}位`
  764. )
  765. return false
  766. }
  767. }
  768. if (!checkRange(numVal)) return false
  769. } else if (type === 'boolean' || type === 'bool') {
  770. const ok = [
  771. 'true',
  772. 'false',
  773. '1',
  774. '0',
  775. '是',
  776. '否',
  777. 'Y',
  778. 'N',
  779. 'y',
  780. 'n',
  781. ].includes(rawStr)
  782. if (!ok) {
  783. this.showFieldError(
  784. row,
  785. year,
  786. `${row.itemName}的${year}年数据必须为布尔值(是/否/true/false/1/0)`
  787. )
  788. return false
  789. }
  790. } else if (type === 'date' || type === 'datetime' || r.dateFormat) {
  791. const fmt =
  792. r.dateFormat ||
  793. (type === 'datetime' ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd')
  794. if (!this.validateDateByFormat(rawStr, fmt)) {
  795. this.showFieldError(
  796. row,
  797. year,
  798. `${row.itemName}的${year}年数据不符合日期格式 ${fmt}`
  799. )
  800. return false
  801. }
  802. } else if (type === 'dict' || r.allowedValues) {
  803. const arr = Array.isArray(r.allowedValues) ? r.allowedValues : []
  804. if (arr.length > 0 && !arr.includes(rawStr)) {
  805. this.showFieldError(
  806. row,
  807. year,
  808. `${row.itemName}的${year}年数据必须为字典项之一`
  809. )
  810. return false
  811. }
  812. }
  813. }
  814. return true
  815. },
  816. // 验证勾稽关系
  817. validateLinkage(row, year) {
  818. if (!row.linkageRules) return true
  819. const { parent, relation } = row.linkageRules
  820. if (relation === 'sum') {
  821. // 验证子项之和等于父项
  822. const parentRow = this.tableData.find((r) => r.id === parent)
  823. if (!parentRow || parentRow.isCategory) return true
  824. const children = this.tableData.filter(
  825. (r) => r.categoryId === parent && !r.isCategory
  826. )
  827. const parentValue = Number(parentRow[`year_${year}`]) || 0
  828. const sumValue = children.reduce((sum, child) => {
  829. return sum + (Number(child[`year_${year}`]) || 0)
  830. }, 0)
  831. if (parentValue !== sumValue) {
  832. // 可以选择自动修正或提示错误
  833. // 这里仅提示,不自动修正
  834. console.warn(
  835. `${parentRow.itemName}的${year}年数据(${parentValue})与子项之和(${sumValue})不相等`
  836. )
  837. }
  838. }
  839. return true
  840. },
  841. // 显示字段错误
  842. showFieldError(row, year, message) {
  843. // 这里可以添加更详细的错误提示
  844. console.error(message)
  845. },
  846. // 保存前验证
  847. validateBeforeSave() {
  848. this.validationErrors = []
  849. let isValid = true
  850. // 验证所有单元格
  851. this.tableData.forEach((row) => {
  852. if (row.isCategory) return
  853. this.yearColumns.forEach((year) => {
  854. const cellValid = this.validateCell(row, year)
  855. if (!cellValid) {
  856. isValid = false
  857. this.validationErrors.push({
  858. row: row.itemName,
  859. year,
  860. message: `${row.itemName}的${year}年数据验证失败`,
  861. })
  862. }
  863. })
  864. // 验证勾稽关系
  865. this.yearColumns.forEach((year) => {
  866. const linkageValid = this.validateLinkage(row, year)
  867. if (!linkageValid) {
  868. isValid = false
  869. }
  870. })
  871. })
  872. // 验证所有勾稽关系
  873. this.validateAllLinkages()
  874. return isValid
  875. },
  876. // 验证所有勾稽关系
  877. validateAllLinkages() {
  878. const categories = this.tableData.filter((r) => r.isCategory)
  879. categories.forEach((category) => {
  880. const children = this.tableData.filter(
  881. (r) => r.categoryId === category.id && !r.isCategory
  882. )
  883. this.yearColumns.forEach((year) => {
  884. const categoryValue = Number(category[`year_${year}`]) || 0
  885. const sumValue = children.reduce((sum, child) => {
  886. return sum + (Number(child[`year_${year}`]) || 0)
  887. }, 0)
  888. if (categoryValue !== 0 && categoryValue !== sumValue) {
  889. this.validationErrors.push({
  890. row: category.itemName,
  891. year,
  892. message: `${category.itemName}的${year}年数据(${categoryValue})与子项之和(${sumValue})不相等`,
  893. })
  894. }
  895. })
  896. })
  897. },
  898. // 简单日期校验(支持 yyyy-MM-dd 与 yyyy-MM-dd HH:mm:ss)
  899. validateDateByFormat(value, format) {
  900. const v = String(value).trim()
  901. if (!v) return false
  902. if (format === 'yyyy-MM-dd') {
  903. const m = v.match(/^(\d{4})-(\d{2})-(\d{2})$/)
  904. if (!m) return false
  905. const y = +m[1],
  906. mo = +m[2],
  907. d = +m[3]
  908. const dt = new Date(y, mo - 1, d)
  909. return (
  910. dt.getFullYear() === y &&
  911. dt.getMonth() === mo - 1 &&
  912. dt.getDate() === d
  913. )
  914. }
  915. if (format === 'yyyy-MM-dd HH:mm:ss') {
  916. const m = v.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/)
  917. if (!m) return false
  918. const y = +m[1],
  919. mo = +m[2],
  920. d = +m[3],
  921. hh = +m[4],
  922. mm = +m[5],
  923. ss = +m[6]
  924. const dt = new Date(y, mo - 1, d, hh, mm, ss)
  925. return (
  926. dt.getFullYear() === y &&
  927. dt.getMonth() === mo - 1 &&
  928. dt.getDate() === d &&
  929. dt.getHours() === hh &&
  930. dt.getMinutes() === mm &&
  931. dt.getSeconds() === ss
  932. )
  933. }
  934. // 其它格式简单兜底:只要不是空
  935. return !!v
  936. },
  937. // ===== 数字输入控件参数推导 =====
  938. getPrecision(row) {
  939. const r = (row && row.validateRules) || {}
  940. const t = String(r.type || '').toLowerCase()
  941. // integer 或 number 且小数位为 0
  942. if (
  943. t === 'integer' ||
  944. (t === 'number' &&
  945. (r.fieldTypenointlen === 0 || r.decimalLength === 0))
  946. ) {
  947. return 0
  948. }
  949. const d = r.fieldTypenointlen || r.fieldTypeNointLen || r.decimalLength
  950. if (d === 0) return 0
  951. if (typeof d === 'number') return d
  952. return undefined
  953. },
  954. getStep(row) {
  955. const p = this.getPrecision(row)
  956. if (p === 0) return 1
  957. if (typeof p === 'number' && p > 0)
  958. return Number((1 / Math.pow(10, p)).toFixed(p))
  959. return 1
  960. },
  961. getMin(row) {
  962. const r = (row && row.validateRules) || {}
  963. return typeof r.min === 'number' ? r.min : undefined
  964. },
  965. getMax(row) {
  966. const r = (row && row.validateRules) || {}
  967. return typeof r.max === 'number' ? r.max : undefined
  968. },
  969. // 关闭弹窗
  970. handleClose() {
  971. this.dialogVisible = false
  972. this.$emit('update:visible', false)
  973. },
  974. // 取消
  975. handleCancel() {
  976. this.handleClose()
  977. },
  978. // 保存
  979. async handleSave() {
  980. // 验证数据
  981. if (!this.validateBeforeSave()) {
  982. const errorMessages = this.validationErrors
  983. .map((err) => `${err.row}的${err.year}年:${err.message}`)
  984. .join('\n')
  985. Message.error('数据验证失败:\n' + errorMessages)
  986. return
  987. }
  988. try {
  989. // 判断是否有数据(编辑模式):如果有 uploadId,说明是编辑已有数据
  990. const hasData = !!(
  991. this.uploadId ||
  992. this.surveyData.uploadId ||
  993. this.surveyData.id
  994. )
  995. // 格式化保存数据为接口需要的格式
  996. const saveData = []
  997. // 遍历所有行数据
  998. this.tableData.forEach((row) => {
  999. const rowid = row.rowid || row.id
  1000. // 跳过分类行(如果分类行不需要保存基本信息)
  1001. if (!row.isCategory) {
  1002. // 保存基本信息:序号、项目、单位
  1003. if (row.seq !== undefined && row.seq !== null && row.seq !== '') {
  1004. saveData.push({
  1005. rowid: rowid,
  1006. rkey: '序号',
  1007. rvalue: String(row.seq),
  1008. })
  1009. }
  1010. if (row.itemName) {
  1011. saveData.push({
  1012. rowid: rowid,
  1013. rkey: '项目',
  1014. rvalue: String(row.itemName),
  1015. })
  1016. }
  1017. if (row.unit) {
  1018. saveData.push({
  1019. rowid: rowid,
  1020. rkey: '单位',
  1021. rvalue: String(row.unit),
  1022. })
  1023. }
  1024. }
  1025. // 保存年份数据(所有行都可以有年份数据)
  1026. this.yearColumns.forEach((year) => {
  1027. const yearValue = row[`year_${year}`]
  1028. if (
  1029. yearValue !== undefined &&
  1030. yearValue !== null &&
  1031. yearValue !== ''
  1032. ) {
  1033. saveData.push({
  1034. rowid: rowid,
  1035. rkey: String(year),
  1036. rvalue: String(yearValue),
  1037. })
  1038. }
  1039. })
  1040. // 保存备注(所有行都可以有备注)
  1041. if (
  1042. row.remark !== undefined &&
  1043. row.remark !== null &&
  1044. row.remark !== ''
  1045. ) {
  1046. saveData.push({
  1047. rowid: rowid,
  1048. rkey: '备注',
  1049. rvalue: String(row.remark),
  1050. })
  1051. }
  1052. })
  1053. // 为每条数据添加公共字段
  1054. const finalSaveData = saveData.map((item) => {
  1055. const dataItem = {
  1056. ...item,
  1057. auditedUnitId:
  1058. this.auditedUnitId || this.surveyData.auditedUnitId || '',
  1059. surveyTemplateId:
  1060. this.surveyTemplateId || this.surveyData.surveyTemplateId || '',
  1061. catalogId: this.catalogId || this.surveyData.catalogId || '',
  1062. taskId: this.taskId || this.surveyData.taskId || '',
  1063. type: this.requestType,
  1064. }
  1065. // 如果有数据(编辑模式),添加 uploadId 字段
  1066. if (hasData) {
  1067. dataItem.uploadId =
  1068. this.uploadId ||
  1069. this.surveyData.uploadId ||
  1070. this.surveyData.id ||
  1071. ''
  1072. }
  1073. return dataItem
  1074. })
  1075. console.log('保存数据:', finalSaveData)
  1076. // 调用保存接口
  1077. const res = await saveSingleRecordSurvey(finalSaveData)
  1078. if (res && res.code === 200) {
  1079. Message.success('保存成功')
  1080. // 触发保存事件
  1081. this.$emit('save', finalSaveData)
  1082. // 触发刷新事件
  1083. this.$emit('refresh')
  1084. this.handleClose()
  1085. } else {
  1086. Message.error(res.message || '保存失败')
  1087. }
  1088. } catch (err) {
  1089. console.error('保存失败', err)
  1090. // Message.error(err.message || '保存失败')
  1091. }
  1092. },
  1093. // 构建 cellCode 索引与依赖图
  1094. buildFormulaEngine() {
  1095. const index = {}
  1096. const deps = {}
  1097. this.tableData.forEach((row) => {
  1098. if (row && row.cellCode) {
  1099. index[row.cellCode] = row
  1100. }
  1101. })
  1102. // 构建依赖映射:A1 -> [依赖 A1 的行...]
  1103. this.tableData.forEach((row) => {
  1104. if (!row || !row.calculationFormula) return
  1105. const codes =
  1106. row.calculationFormula.toString().match(/[A-Z]+\d+/g) || []
  1107. codes.forEach((code) => {
  1108. if (!deps[code]) deps[code] = new Set()
  1109. deps[code].add(row)
  1110. })
  1111. })
  1112. this.cellCodeIndex = index
  1113. this.dependentsMap = deps
  1114. },
  1115. // 重新计算一个公式行在某年的值
  1116. recomputeRowForYear(row, year) {
  1117. if (!row || !row.calculationFormula) return
  1118. const expr = row.calculationFormula.toString()
  1119. // 替换变量为对应年的数值
  1120. const replaced = expr.replace(/[A-Z]+\d+/g, (code) => {
  1121. const refRow = this.cellCodeIndex[code]
  1122. const v = refRow ? Number(refRow[`year_${year}`]) : 0
  1123. return isNaN(v) ? '0' : String(v)
  1124. })
  1125. // 仅允许数字与运算符
  1126. const safe = this.safeEvalExpression(replaced)
  1127. if (safe !== null && !isNaN(safe)) {
  1128. row[`year_${year}`] = String(safe)
  1129. }
  1130. },
  1131. // 在某年针对一个输入行,联动所有依赖它的行
  1132. recomputeDependentsForRow(row, year) {
  1133. if (!row) return
  1134. const code = row.cellCode
  1135. if (!code) return
  1136. const dependents = this.dependentsMap[code]
  1137. if (!dependents || dependents.size === 0) return
  1138. dependents.forEach((depRow) => this.recomputeRowForYear(depRow, year))
  1139. // 可能存在链式依赖,递归触发
  1140. dependents.forEach((depRow) =>
  1141. this.recomputeDependentsForRow(depRow, year)
  1142. )
  1143. },
  1144. // 对所有含公式的行、所有年重算
  1145. recomputeAllFormulas() {
  1146. if (!this.yearColumns || this.yearColumns.length === 0) return
  1147. const formulaRows = this.tableData.filter(
  1148. (r) => r && r.calculationFormula
  1149. )
  1150. this.yearColumns.forEach((year) => {
  1151. formulaRows.forEach((r) => this.recomputeRowForYear(r, year))
  1152. })
  1153. this.$forceUpdate()
  1154. },
  1155. // 简单安全表达式求值:仅支持数字、+ - * / 和括号
  1156. safeEvalExpression(expr) {
  1157. const cleaned = expr.replace(/\s+/g, '')
  1158. if (!/^[-+*/().\d]+$/.test(cleaned)) {
  1159. // 存在不允许的字符,放弃计算
  1160. return null
  1161. }
  1162. try {
  1163. // eslint-disable-next-line no-new-func
  1164. const fn = new Function(`return (${cleaned})`)
  1165. const res = fn()
  1166. return typeof res === 'number' && isFinite(res) ? res : null
  1167. } catch (e) {
  1168. return null
  1169. }
  1170. },
  1171. },
  1172. }
  1173. </script>
  1174. <style scoped lang="scss">
  1175. .dialog-footer {
  1176. text-align: center;
  1177. margin-top: 20px;
  1178. .el-button {
  1179. margin: 0 10px;
  1180. }
  1181. }
  1182. ::v-deep .el-dialog__header {
  1183. padding: 20px 20px 10px;
  1184. .el-dialog__title {
  1185. font-size: 18px;
  1186. font-weight: 600;
  1187. color: #303133;
  1188. }
  1189. }
  1190. // 分类行样式
  1191. ::v-deep .category-row {
  1192. background-color: #f5f7fa !important;
  1193. td {
  1194. background-color: #f5f7fa !important;
  1195. font-weight: bold;
  1196. }
  1197. .category-name {
  1198. color: #409eff;
  1199. font-weight: bold;
  1200. }
  1201. .category-seq {
  1202. color: #409eff;
  1203. font-weight: bold;
  1204. }
  1205. }
  1206. // 表格输入框样式
  1207. ::v-deep .el-table {
  1208. .el-input {
  1209. .el-input__inner {
  1210. border: none;
  1211. padding: 0 5px;
  1212. text-align: center;
  1213. &:focus {
  1214. border: 1px solid #409eff;
  1215. }
  1216. }
  1217. }
  1218. }
  1219. </style>