FixedAssetsTable.vue 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608
  1. <template>
  2. <div class="fixed-assets-table-container">
  3. <el-table
  4. :data="flattenedTableData"
  5. border
  6. style="width: 100%"
  7. size="small"
  8. :row-class-name="getRowClassName"
  9. >
  10. <!-- 序号列 -->
  11. <!-- <el-table-column prop="seq" label="序号" width="80" align="center">
  12. <template slot-scope="scope">
  13. <span v-if="scope.row.isCategory" class="category-seq">
  14. </span>
  15. <span v-else>{{ scope.row.seq }}</span>
  16. </template>
  17. </el-table-column> -->
  18. <!-- 动态表头列 -->
  19. <el-table-column
  20. v-for="column in dynamicColumns"
  21. :key="column.prop"
  22. :prop="column.prop"
  23. :label="column.label"
  24. :min-width="column.minWidth || 120"
  25. :align="column.align || 'center'"
  26. >
  27. <template slot-scope="scope">
  28. <!-- parentid 为 -1 的行只读显示 -->
  29. <span
  30. v-if="
  31. scope.row.parentid === '-1' ||
  32. scope.row.parentid === -1 ||
  33. scope.row.parentId === '-1' ||
  34. scope.row.parentId === -1 ||
  35. scope.row.isCategory
  36. "
  37. class="category-value"
  38. >
  39. {{ scope.row[column.prop] || '-' }}
  40. </span>
  41. <!-- 子项可编辑 -->
  42. <el-date-picker
  43. v-else-if="column.type === 'date'"
  44. v-model="scope.row[column.prop]"
  45. type="date"
  46. placeholder="选择日期"
  47. size="mini"
  48. format="yyyy-MM-dd"
  49. value-format="yyyy-MM-dd"
  50. :disabled="
  51. isViewMode ||
  52. scope.row.notDeletable === true ||
  53. scope.row.fromTemplate === true ||
  54. scope.row.notEditable === true
  55. "
  56. style="width: 100%"
  57. />
  58. <el-input
  59. v-else
  60. v-model="scope.row[column.prop]"
  61. :placeholder="column.placeholder || '请输入' + column.label"
  62. size="mini"
  63. :disabled="
  64. isViewMode ||
  65. scope.row.notDeletable === true ||
  66. scope.row.fromTemplate === true ||
  67. scope.row.notEditable === true
  68. "
  69. @blur="handleCellBlur(scope.row, column.prop)"
  70. />
  71. </template>
  72. </el-table-column>
  73. <!-- 操作列(仅非查看模式显示) -->
  74. <el-table-column
  75. v-if="!isViewMode"
  76. label="操作"
  77. width="100"
  78. align="center"
  79. fixed="right"
  80. >
  81. <template slot-scope="scope">
  82. <div
  83. v-if="scope.row.parentid === '-1' || scope.row.parentid === -1"
  84. class="operation-buttons"
  85. >
  86. <el-button
  87. type="text"
  88. size="mini"
  89. icon="el-icon-plus"
  90. :disabled="isViewMode"
  91. @click="handleAddRow(scope.row)"
  92. />
  93. <!-- <el-button
  94. type="text"
  95. size="mini"
  96. icon="el-icon-minus"
  97. :disabled="isViewMode"
  98. @click="handleDeleteRow(scope.row)"
  99. /> -->
  100. </div>
  101. <div v-else class="operation-buttons">
  102. <el-button
  103. type="text"
  104. size="mini"
  105. icon="el-icon-minus"
  106. :disabled="
  107. isViewMode ||
  108. scope.row.notDeletable === true ||
  109. scope.row.fromTemplate === true ||
  110. scope.row.notEditable === true
  111. "
  112. @click="handleDeleteChildRow(scope.row)"
  113. />
  114. </div>
  115. </template>
  116. </el-table-column>
  117. </el-table>
  118. </div>
  119. </template>
  120. <script>
  121. import { Message } from 'element-ui'
  122. import { saveSingleRecordSurvey } from '@/api/audit/survey'
  123. export default {
  124. name: 'FixedAssetsTable',
  125. props: {
  126. // 表格数据配置(嵌套结构)
  127. tableItems: {
  128. type: Array,
  129. default: () => [],
  130. },
  131. // 是否有保存的数据
  132. savedData: {
  133. type: [Object, Array],
  134. default: () => ({}),
  135. },
  136. // 是否查看模式
  137. isViewMode: {
  138. type: Boolean,
  139. default: false,
  140. },
  141. // 固定字段标签(逗号分隔的字符串)
  142. fixedFields: {
  143. type: String,
  144. default: '',
  145. },
  146. // 固定字段ID(逗号分隔的字符串)
  147. fixedFieldids: {
  148. type: String,
  149. default: '',
  150. },
  151. // 保存所需的公共字段
  152. auditedUnitId: {
  153. type: String,
  154. default: '',
  155. },
  156. surveyTemplateId: {
  157. type: String,
  158. default: '',
  159. },
  160. catalogId: {
  161. type: String,
  162. default: '',
  163. },
  164. uploadId: {
  165. type: String,
  166. default: '',
  167. },
  168. // 动态表调查填报表格的行ID
  169. periodRecordId: {
  170. type: String,
  171. default: '',
  172. },
  173. },
  174. data() {
  175. return {
  176. // 嵌套的表格数据
  177. fixedAssetsData: [],
  178. // 验证错误
  179. validationErrors: [],
  180. // 记录被删除的行rowid/id,用于保存时过滤
  181. deletedRowids: [],
  182. // 扁平化的表格数据(响应式)
  183. flattenedData: [],
  184. autoIdSeed: 1,
  185. // 原始保存的数据(用于回显)
  186. rawSavedData: null,
  187. }
  188. },
  189. computed: {
  190. // 扁平化的表格数据(从嵌套结构生成)
  191. flattenedTableData() {
  192. return this.flattenedData
  193. },
  194. // 动态表头列配置
  195. dynamicColumns() {
  196. if (!this.fixedFields || !this.fixedFieldids) {
  197. // 如果没有 fixedFields,返回默认列配置
  198. return this.getDefaultColumns()
  199. }
  200. const labels = this.fixedFields.split(',').map((item) => item.trim())
  201. const ids = this.fixedFieldids.split(',').map((item) => item.trim())
  202. return labels.map((label, index) => {
  203. const fieldId = ids[index] || `field_${index}`
  204. // 数据中的字段名就是 label(中文),所以 prop 使用 label
  205. // fieldId 用于保存时映射回字段ID
  206. const prop = label
  207. // 根据字段名判断类型
  208. const isDateField =
  209. label.includes('日期') ||
  210. label.includes('时间') ||
  211. label.includes('Date') ||
  212. label.includes('Time')
  213. return {
  214. prop: prop, // 使用中文字段名作为 prop,直接对应数据中的键名
  215. fieldId: fieldId, // 保存字段ID,用于保存时映射
  216. label: label,
  217. minWidth: isDateField ? 180 : 120,
  218. align: 'center',
  219. type: isDateField ? 'date' : 'input',
  220. placeholder: `请输入${label}`,
  221. }
  222. })
  223. },
  224. },
  225. watch: {
  226. tableItems: {
  227. handler(newVal) {
  228. if (newVal && newVal.length > 0) {
  229. // 若 tableItems 为扁平结构(带 parentId/parentid),按父子关系组装后再渲染
  230. const normalized = this.normalizeTableItemsSource(newVal)
  231. this.fixedAssetsData =
  232. normalized && normalized.length > 0
  233. ? normalized
  234. : this.deepClone(newVal)
  235. } else {
  236. // 不使用假数据,等待接口返回数据
  237. this.fixedAssetsData = []
  238. }
  239. // 在渲染前将传入的子项标记为只读
  240. this.markInitialChildrenReadonly()
  241. // 重新生成扁平数据
  242. this.generateFlattenedData()
  243. },
  244. immediate: true,
  245. deep: true,
  246. },
  247. savedData: {
  248. handler(newVal) {
  249. console.log('newVal', newVal)
  250. // 保存原始数据用于回显
  251. this.rawSavedData = newVal
  252. // 回显新数据时清空已删除记录
  253. this.deletedRowids = []
  254. if (newVal) {
  255. const normalized = this.normalizeSavedData(newVal)
  256. if (normalized && normalized.length > 0) {
  257. // 使用接口返回的数据,不使用假数据
  258. this.fixedAssetsData = normalized
  259. } else {
  260. // 如果没有数据,清空
  261. this.fixedAssetsData = []
  262. }
  263. } else {
  264. this.fixedAssetsData = []
  265. }
  266. // 在渲染前将传入的子项标记为只读
  267. this.markInitialChildrenReadonly()
  268. // 数据变化时重新生成扁平数据
  269. this.generateFlattenedData()
  270. },
  271. deep: true,
  272. immediate: true,
  273. },
  274. },
  275. methods: {
  276. // 将传入的子项标记为只读(不可编辑/删除),新增的行不受影响
  277. markInitialChildrenReadonly() {
  278. const data = this.fixedAssetsData || []
  279. data.forEach((node) => {
  280. if (!node || typeof node !== 'object') return
  281. const children = Array.isArray(node.children) ? node.children : []
  282. children.forEach((child) => {
  283. if (!child || typeof child !== 'object') return
  284. // 仅标记已有的传入子项(保留模板标记)
  285. if (child.notEditable !== false) {
  286. child.notEditable = true
  287. }
  288. })
  289. })
  290. },
  291. // 生成扁平数据
  292. generateFlattenedData() {
  293. // 先同步扁平数据中的修改到嵌套结构,避免数据丢失
  294. // 只有在已有扁平数据时才同步(避免初始化时出错)
  295. const shouldSync =
  296. this.flattenedData &&
  297. this.flattenedData.length > 0 &&
  298. this.fixedAssetsData &&
  299. this.fixedAssetsData.length > 0
  300. if (shouldSync) {
  301. this.syncFlattenedDataToNested()
  302. }
  303. const result = []
  304. let seq = 1
  305. const processItem = (item, parentCategory = null) => {
  306. const isCategory = this.isCategoryItem(item)
  307. const currentItem = this.normalizeItemNode(item)
  308. if (isCategory) {
  309. // 分类行(父项)
  310. const categoryRowData = {
  311. ...currentItem,
  312. seq: currentItem.categorySeq || currentItem.id,
  313. isCategory: true,
  314. categorySeq: currentItem.categorySeq || currentItem.id,
  315. }
  316. // 直接使用 currentItem 中的数据(normalizeSavedData 已经保留了所有原始字段)
  317. // 将所有字段值复制到 categoryRowData(除了系统字段)
  318. Object.keys(currentItem).forEach((key) => {
  319. if (
  320. currentItem[key] !== undefined &&
  321. key !== 'id' &&
  322. key !== 'children' &&
  323. key !== 'isCategory' &&
  324. key !== 'categorySeq' &&
  325. key !== 'seq' &&
  326. !key.startsWith('_')
  327. ) {
  328. categoryRowData[key] = currentItem[key]
  329. }
  330. })
  331. // 根据表头字段确保所有列都有值(如果数据中没有对应字段,初始化为空)
  332. this.dynamicColumns.forEach((column) => {
  333. if (categoryRowData[column.prop] === undefined) {
  334. categoryRowData[column.prop] = currentItem[column.prop] || ''
  335. }
  336. })
  337. result.push(categoryRowData)
  338. // 处理分类下的子项
  339. if (currentItem.children && Array.isArray(currentItem.children)) {
  340. currentItem.children.forEach((child) => {
  341. processItem(child, currentItem)
  342. })
  343. }
  344. } else {
  345. // 普通行
  346. const rowData = {
  347. ...currentItem,
  348. seq: seq++,
  349. isCategory: false,
  350. }
  351. // 如果有父分类,设置分类信息
  352. if (parentCategory) {
  353. rowData.categoryId = parentCategory.id
  354. rowData.categorySeq =
  355. parentCategory.categorySeq || parentCategory.id
  356. }
  357. // 直接使用 currentItem 中的数据(normalizeSavedData 已经保留了所有原始字段)
  358. // 将所有字段值复制到 rowData(除了系统字段)
  359. Object.keys(currentItem).forEach((key) => {
  360. if (
  361. currentItem[key] !== undefined &&
  362. key !== 'id' &&
  363. key !== 'children' &&
  364. key !== 'isCategory' &&
  365. key !== 'categorySeq' &&
  366. key !== 'seq' &&
  367. !key.startsWith('_')
  368. ) {
  369. rowData[key] = currentItem[key]
  370. }
  371. })
  372. // 根据表头字段确保所有列都有值(优先使用数据中的值,如果没有则初始化为空)
  373. this.dynamicColumns.forEach((column) => {
  374. // 如果数据中没有该字段,初始化为空字符串
  375. if (rowData[column.prop] === undefined) {
  376. rowData[column.prop] = currentItem[column.prop] || ''
  377. }
  378. })
  379. result.push(rowData)
  380. // 如果有子项,递归处理
  381. if (item.children && Array.isArray(item.children)) {
  382. item.children.forEach((child) => {
  383. processItem(child, item)
  384. })
  385. }
  386. }
  387. }
  388. // 处理所有项
  389. this.fixedAssetsData.forEach((item) => {
  390. processItem(item)
  391. })
  392. // 使用 Vue.set 确保响应式
  393. this.$set(this, 'flattenedData', result)
  394. },
  395. // 深拷贝
  396. deepClone(obj) {
  397. if (obj === null || typeof obj !== 'object') return obj
  398. if (obj instanceof Date) return new Date(obj.getTime())
  399. if (obj instanceof Array) return obj.map((item) => this.deepClone(item))
  400. if (typeof obj === 'object') {
  401. const clonedObj = {}
  402. for (const key in obj) {
  403. if (obj.hasOwnProperty(key)) {
  404. clonedObj[key] = this.deepClone(obj[key])
  405. }
  406. }
  407. return clonedObj
  408. }
  409. },
  410. // 将三元组明细数组(rowid,parentId,rkey,rvalue)聚合为树形行数据
  411. transformTripletArrayToRows(items) {
  412. if (!Array.isArray(items) || items.length === 0) return []
  413. const rowsByRowid = new Map()
  414. const parentRowids = new Set()
  415. items.forEach((rec) => {
  416. if (!rec || typeof rec !== 'object') return
  417. const rowid = rec.rowid || rec.rowId
  418. if (!rowid) return
  419. const parentId = rec.parentId ?? rec.parentID ?? rec.parentid
  420. if (parentId && parentId !== '-1' && parentId !== -1) {
  421. parentRowids.add(String(parentId))
  422. }
  423. if (!rowsByRowid.has(rowid)) {
  424. rowsByRowid.set(rowid, {
  425. id: rowid,
  426. rowid: rowid,
  427. parentid:
  428. parentId === undefined || parentId === null
  429. ? ''
  430. : String(parentId),
  431. children: [],
  432. })
  433. }
  434. const row = rowsByRowid.get(rowid)
  435. const key = rec.rkey
  436. const val = rec.rvalue
  437. if (key !== undefined) {
  438. row[key] = val
  439. }
  440. })
  441. const allRows = Array.from(rowsByRowid.values())
  442. allRows.forEach((row) => {
  443. if (
  444. row.parentid === '-1' ||
  445. row.parentid === -1 ||
  446. parentRowids.has(row.rowid)
  447. ) {
  448. row.isCategory = true
  449. }
  450. })
  451. const byRowid = new Map(allRows.map((r) => [r.rowid, r]))
  452. const roots = []
  453. allRows.forEach((row) => {
  454. const p = row.parentid
  455. if (p && p !== '-1' && p !== -1) {
  456. const parent = byRowid.get(String(p))
  457. if (parent) {
  458. if (!Array.isArray(parent.children)) parent.children = []
  459. parent.children.push(row)
  460. parent.isCategory = true
  461. } else {
  462. roots.push(row)
  463. }
  464. } else {
  465. roots.push(row)
  466. }
  467. })
  468. return roots
  469. },
  470. // 当没有详情数据时,tableItems 可能为扁平结构,这里根据 parentId/parentid 组装树
  471. normalizeTableItemsSource(items) {
  472. if (!Array.isArray(items) || items.length === 0) return []
  473. const cloned = this.deepClone(items)
  474. // 三元组格式直接转换
  475. const isTriplet =
  476. cloned.length > 0 &&
  477. cloned.every(
  478. (it) =>
  479. it &&
  480. typeof it === 'object' &&
  481. 'rkey' in it &&
  482. 'rvalue' in it &&
  483. ('rowid' in it || 'rowId' in it)
  484. )
  485. if (isTriplet) {
  486. return this.transformTripletArrayToRows(cloned)
  487. }
  488. const hasNestedChildren = cloned.some(
  489. (it) => it && Array.isArray(it.children) && it.children.length > 0
  490. )
  491. if (hasNestedChildren) {
  492. return cloned.map((it) => this.normalizeItemNode(it))
  493. }
  494. // 扁平结构:根据 parentId/parentid 组装
  495. const idMap = new Map()
  496. const rowidMap = new Map()
  497. const nodes = cloned.map((raw) => this.normalizeItemNode(raw))
  498. nodes.forEach((node, idx) => {
  499. const raw = cloned[idx]
  500. const id = node.id
  501. const rowid = raw.rowid || raw.rowId || node.rowid || node.id
  502. if (id) idMap.set(id, node)
  503. if (rowid) rowidMap.set(rowid, node)
  504. })
  505. const roots = []
  506. cloned.forEach((raw, idx) => {
  507. const node = nodes[idx]
  508. const parentKey =
  509. raw.parentId ??
  510. raw.parentid ??
  511. raw.parentID ??
  512. raw.parentCode ??
  513. raw.parent_code
  514. if (parentKey && parentKey !== '-1' && parentKey !== -1) {
  515. // 先按 id 匹配,再按 rowid 兜底
  516. const parent = idMap.get(parentKey) || rowidMap.get(parentKey)
  517. if (parent) {
  518. if (!Array.isArray(parent.children)) parent.children = []
  519. parent.children.push(node)
  520. parent.isCategory = true
  521. } else {
  522. roots.push(node)
  523. }
  524. } else {
  525. roots.push(node)
  526. }
  527. })
  528. return roots.length > 0 ? roots : nodes
  529. },
  530. // 获取行样式类名
  531. getRowClassName({ row }) {
  532. // parentid 为 -1 的行(父项)使用分类行样式
  533. if (
  534. row.parentid === '-1' ||
  535. row.parentid === -1 ||
  536. row.parentId === '-1' ||
  537. row.parentId === -1 ||
  538. row.isCategory
  539. ) {
  540. return 'category-row'
  541. }
  542. return ''
  543. },
  544. // 添加行(添加子项)
  545. handleAddRow(row) {
  546. // 先同步扁平数据中的修改到嵌套结构,避免数据丢失
  547. this.syncFlattenedDataToNested()
  548. // row 是 parentid 为 -1 的父项
  549. // 找到对应的父项节点
  550. const parentNode = this.findNodeByRowid(row.rowid)
  551. if (parentNode) {
  552. // 生成新的子项
  553. const newRowid = `${Date.now()}.${Math.random()
  554. .toString(36)
  555. .substr(2, 9)}`
  556. const newItem = {
  557. parentid: row.rowid, // 设置父项的 rowid
  558. rowid: newRowid,
  559. id: newRowid, // 临时使用 rowid 作为 id
  560. }
  561. // 根据表头字段初始化新项的所有字段
  562. this.dynamicColumns.forEach((column) => {
  563. newItem[column.prop] = ''
  564. })
  565. // 在父项的 children 数组末尾添加新行
  566. if (!parentNode.children) {
  567. this.$set(parentNode, 'children', [])
  568. }
  569. parentNode.children.push(newItem)
  570. // 重新生成扁平数据(generateFlattenedData 内部会再次同步,确保数据不丢失)
  571. this.generateFlattenedData()
  572. Message.success('添加子项成功')
  573. } else {
  574. Message.error('找不到父项')
  575. }
  576. },
  577. // 删除行(删除父项下的最后一个子项)
  578. handleDeleteRow(row) {
  579. // 先同步扁平数据中的修改到嵌套结构,避免数据丢失
  580. this.syncFlattenedDataToNested()
  581. // row 是 parentid 为 -1 的父项
  582. // 找到对应的父项节点
  583. const parentNode = this.findNodeByRowid(row.rowid)
  584. if (
  585. parentNode &&
  586. parentNode.children &&
  587. parentNode.children.length > 0
  588. ) {
  589. // 删除最后一个子项
  590. const removedChild = parentNode.children.pop()
  591. // 记录被删除的行标识,保存时过滤
  592. const removedKey = String(
  593. (removedChild &&
  594. (removedChild.rowid || removedChild.rowId || removedChild.id)) ||
  595. ''
  596. )
  597. if (removedKey) {
  598. if (!this.deletedRowids.includes(removedKey)) {
  599. this.deletedRowids.push(removedKey)
  600. }
  601. // 同步从扁平数据中移除该行,避免同步时被回填
  602. const withoutRemoved = (this.flattenedData || []).filter((r) => {
  603. const rk = String(r.rowid || r.rowId || r.id || '')
  604. return rk !== removedKey
  605. })
  606. this.$set(this, 'flattenedData', withoutRemoved)
  607. }
  608. // 重新生成扁平数据
  609. this.generateFlattenedData()
  610. Message.success('删除子项成功')
  611. } else {
  612. Message.warning('该父项下没有可删除的子项')
  613. }
  614. },
  615. // 删除子项(点击子项行的减号删除当前这条)
  616. handleDeleteChildRow(row) {
  617. // 模板数据不可删除
  618. if (row && (row.notDeletable === true || row.fromTemplate === true)) {
  619. Message.warning('该行由模板生成,无法删除')
  620. return
  621. }
  622. // 先同步扁平数据中的修改到嵌套结构,避免数据丢失
  623. this.syncFlattenedDataToNested()
  624. const childRowid = row.rowid || row.rowId || row.id
  625. if (!childRowid) {
  626. Message.error('无法识别该行标识,删除失败')
  627. return
  628. }
  629. // 记录被删除的行标识,保存时过滤
  630. const key = String(childRowid)
  631. if (key && !this.deletedRowids.includes(key)) {
  632. this.deletedRowids.push(key)
  633. }
  634. // 从整棵树中移除此节点
  635. const removed = this.removeRowByRowid(childRowid)
  636. if (removed) {
  637. // 同步从扁平数据中移除该行,避免同步时被回填
  638. const withoutRemoved = (this.flattenedData || []).filter((r) => {
  639. const rk = String(r.rowid || r.rowId || r.id || '')
  640. return rk !== key
  641. })
  642. this.$set(this, 'flattenedData', withoutRemoved)
  643. // 重新生成扁平数据并刷新视图
  644. this.generateFlattenedData()
  645. this.$nextTick(() => {
  646. this.$forceUpdate()
  647. })
  648. Message.success('删除成功')
  649. } else {
  650. Message.warning('未找到可删除的记录')
  651. }
  652. },
  653. // 在嵌套数据中移除指定 rowid 的节点
  654. removeRowByRowid(targetRowid) {
  655. if (!Array.isArray(this.fixedAssetsData)) return false
  656. const toKey = (n) => String(n.rowid || n.rowId || n.id || '')
  657. const targetKey = String(targetRowid)
  658. // 尝试在根级删除
  659. const idx = this.fixedAssetsData.findIndex(
  660. (n) => toKey(n) === targetKey
  661. )
  662. if (idx >= 0) {
  663. this.fixedAssetsData.splice(idx, 1)
  664. return true
  665. }
  666. // 递归在 children 中删除
  667. const dfsRemove = (nodes) => {
  668. for (const node of nodes) {
  669. if (Array.isArray(node.children) && node.children.length > 0) {
  670. const i = node.children.findIndex((c) => toKey(c) === targetKey)
  671. if (i >= 0) {
  672. node.children.splice(i, 1)
  673. return true
  674. }
  675. if (dfsRemove(node.children)) return true
  676. }
  677. }
  678. return false
  679. }
  680. return dfsRemove(this.fixedAssetsData)
  681. },
  682. // 同步扁平数据中的修改到嵌套结构
  683. syncFlattenedDataToNested() {
  684. const flatData = this.flattenedData
  685. if (!flatData || flatData.length === 0) return
  686. // 建立 rowid 到嵌套节点的映射
  687. const nodeMap = new Map()
  688. const buildMap = (items) => {
  689. items.forEach((item) => {
  690. const rowid = item.rowid || item.rowId || item.id
  691. if (rowid) {
  692. nodeMap.set(rowid, item)
  693. }
  694. if (item.children && Array.isArray(item.children)) {
  695. buildMap(item.children)
  696. }
  697. })
  698. }
  699. buildMap(this.fixedAssetsData)
  700. // 将扁平数据中的修改同步到嵌套结构
  701. flatData.forEach((flatRow) => {
  702. const rowid = flatRow.rowid || flatRow.rowId || flatRow.id
  703. if (!rowid) return
  704. const nestedNode = nodeMap.get(rowid)
  705. if (nestedNode) {
  706. // 同步所有表头字段的值(使用 hasOwnProperty 确保即使值为空字符串也能同步)
  707. this.dynamicColumns.forEach((column) => {
  708. if (flatRow.hasOwnProperty(column.prop)) {
  709. nestedNode[column.prop] = flatRow[column.prop]
  710. }
  711. })
  712. // 同步其他字段(除了系统字段)
  713. Object.keys(flatRow).forEach((key) => {
  714. if (
  715. key !== 'id' &&
  716. key !== 'rowid' &&
  717. key !== 'rowId' &&
  718. key !== 'children' &&
  719. key !== 'isCategory' &&
  720. key !== 'categorySeq' &&
  721. key !== 'seq' &&
  722. key !== 'categoryId' &&
  723. !key.startsWith('_')
  724. ) {
  725. // 使用 hasOwnProperty 确保即使值为空字符串也能同步
  726. if (flatRow.hasOwnProperty(key)) {
  727. nestedNode[key] = flatRow[key]
  728. }
  729. }
  730. })
  731. // 确保 parentid 被保留
  732. if (flatRow.hasOwnProperty('parentid')) {
  733. nestedNode.parentid = flatRow.parentid
  734. }
  735. } else {
  736. // 如果找不到节点,可能是新添加的行,需要添加到对应的父节点下
  737. const parentid = flatRow.parentid || flatRow.parentId
  738. if (parentid && parentid !== '-1') {
  739. const parentNode = nodeMap.get(parentid)
  740. if (parentNode) {
  741. // 创建新节点并添加到父节点的 children 中
  742. const newNode = {
  743. ...flatRow,
  744. id: rowid,
  745. rowid: rowid,
  746. parentid: parentid,
  747. }
  748. // 移除系统字段
  749. delete newNode.isCategory
  750. delete newNode.categorySeq
  751. delete newNode.seq
  752. delete newNode.categoryId
  753. if (!parentNode.children) {
  754. this.$set(parentNode, 'children', [])
  755. }
  756. // 检查是否已存在,避免重复添加
  757. const exists = parentNode.children.some(
  758. (child) => (child.rowid || child.rowId || child.id) === rowid
  759. )
  760. if (!exists) {
  761. parentNode.children.push(newNode)
  762. } else {
  763. // 如果已存在,更新数据
  764. const existingNode = parentNode.children.find(
  765. (child) =>
  766. (child.rowid || child.rowId || child.id) === rowid
  767. )
  768. if (existingNode) {
  769. // 同步所有字段
  770. this.dynamicColumns.forEach((column) => {
  771. if (flatRow.hasOwnProperty(column.prop)) {
  772. existingNode[column.prop] = flatRow[column.prop]
  773. }
  774. })
  775. Object.keys(flatRow).forEach((key) => {
  776. if (
  777. key !== 'id' &&
  778. key !== 'rowid' &&
  779. key !== 'rowId' &&
  780. key !== 'children' &&
  781. key !== 'isCategory' &&
  782. key !== 'categorySeq' &&
  783. key !== 'seq' &&
  784. key !== 'categoryId' &&
  785. !key.startsWith('_')
  786. ) {
  787. if (flatRow.hasOwnProperty(key)) {
  788. existingNode[key] = flatRow[key]
  789. }
  790. }
  791. })
  792. }
  793. }
  794. }
  795. }
  796. }
  797. })
  798. },
  799. // 根据 rowid 查找节点
  800. findNodeByRowid(rowid) {
  801. const findInArray = (items) => {
  802. for (const item of items) {
  803. if (item.rowid === rowid || item.rowId === rowid) {
  804. return item
  805. }
  806. if (item.children && Array.isArray(item.children)) {
  807. const found = findInArray(item.children)
  808. if (found) return found
  809. }
  810. }
  811. return null
  812. }
  813. return findInArray(this.fixedAssetsData)
  814. },
  815. // 根据ID在保存的数据中查找(支持通过 id 或 rowid 查找)
  816. findSavedItemById(id) {
  817. // 优先从 rawSavedData 中查找(原始数据)
  818. const dataSource = this.rawSavedData || this.savedData
  819. if (!dataSource) return null
  820. // 递归查找(支持通过 id、rowid、rowId 匹配)
  821. const findInArray = (items) => {
  822. if (!Array.isArray(items)) return null
  823. for (const item of items) {
  824. // 匹配 id、rowid、rowId
  825. if (
  826. item.id === id ||
  827. item.rowid === id ||
  828. item.rowId === id ||
  829. item.itemId === id ||
  830. item.code === id ||
  831. item.key === id
  832. ) {
  833. return item
  834. }
  835. if (item.children && Array.isArray(item.children)) {
  836. const found = findInArray(item.children)
  837. if (found) return found
  838. }
  839. }
  840. return null
  841. }
  842. // 如果 dataSource 是数组
  843. if (Array.isArray(dataSource)) {
  844. return findInArray(dataSource)
  845. }
  846. // 如果 dataSource 是对象,尝试查找
  847. if (typeof dataSource === 'object') {
  848. // 优先查找 itemlist
  849. if (Array.isArray(dataSource.itemlist)) {
  850. const found = findInArray(dataSource.itemlist)
  851. if (found) return found
  852. }
  853. if (Array.isArray(dataSource.items)) {
  854. const found = findInArray(dataSource.items)
  855. if (found) return found
  856. }
  857. if (Array.isArray(dataSource.data)) {
  858. const found = findInArray(dataSource.data)
  859. if (found) return found
  860. }
  861. if (Array.isArray(dataSource.value)) {
  862. const found = findInArray(dataSource.value)
  863. if (found) return found
  864. }
  865. // 可能是一个映射对象,key 是 id
  866. if (dataSource[id]) {
  867. return dataSource[id]
  868. }
  869. // 或者是嵌套结构
  870. return findInArray([dataSource])
  871. }
  872. return null
  873. },
  874. // 根据ID查找分类
  875. findCategoryById(id) {
  876. const findInArray = (items) => {
  877. for (const item of items) {
  878. if (item.id === id) {
  879. return item
  880. }
  881. if (item.children && Array.isArray(item.children)) {
  882. const found = findInArray(item.children)
  883. if (found) return found
  884. }
  885. }
  886. return null
  887. }
  888. return findInArray(this.fixedAssetsData)
  889. },
  890. // 单元格失焦验证
  891. handleCellBlur(row, field) {
  892. // 实时验证格式
  893. if (field === 'originalValue' || field === 'depreciationExpense') {
  894. const value = row[field]
  895. if (value && !/^\d+(\.\d+)?$/.test(value)) {
  896. Message.warning(
  897. `${this.getFieldLabel(field)}格式不正确,请输入数字`
  898. )
  899. }
  900. }
  901. if (field === 'depreciationPeriod') {
  902. const value = row[field]
  903. if (value && !/^\d+$/.test(value)) {
  904. Message.warning(`${this.getFieldLabel(field)}必须是正整数`)
  905. }
  906. }
  907. },
  908. // 获取字段标签
  909. getFieldLabel(field) {
  910. const labels = {
  911. originalValue: '固定资产原值',
  912. depreciationPeriod: '折旧年限',
  913. depreciationExpense: '折旧费',
  914. }
  915. return labels[field] || field
  916. },
  917. // 获取默认列配置(当没有 fixedFields 时使用)
  918. getDefaultColumns() {
  919. return [
  920. {
  921. prop: 'unit',
  922. label: '计量单位',
  923. minWidth: 120,
  924. align: 'center',
  925. type: 'input',
  926. placeholder: '请输入计量单位',
  927. },
  928. {
  929. prop: 'originalValue',
  930. label: '固定资产原值',
  931. minWidth: 150,
  932. align: 'center',
  933. type: 'input',
  934. placeholder: '请输入原值',
  935. },
  936. {
  937. prop: 'entryDate',
  938. label: '入帐或竣工验收日期',
  939. minWidth: 180,
  940. align: 'center',
  941. type: 'date',
  942. placeholder: '选择日期',
  943. },
  944. {
  945. prop: 'depreciationPeriod',
  946. label: '折旧年限',
  947. minWidth: 120,
  948. align: 'center',
  949. type: 'input',
  950. placeholder: '请输入年限',
  951. },
  952. {
  953. prop: 'depreciationExpense',
  954. label: '折旧费',
  955. minWidth: 120,
  956. align: 'center',
  957. type: 'input',
  958. placeholder: '请输入费用',
  959. },
  960. {
  961. prop: 'fundSource',
  962. label: '资金来源',
  963. minWidth: 120,
  964. align: 'center',
  965. type: 'input',
  966. placeholder: '请输入来源',
  967. },
  968. {
  969. prop: 'remark',
  970. label: '备注',
  971. minWidth: 150,
  972. align: 'left',
  973. type: 'input',
  974. placeholder: '请输入备注',
  975. },
  976. ]
  977. },
  978. // 验证表单
  979. // validate() {
  980. // this.validationErrors = []
  981. // const errors = []
  982. // // 验证扁平数据(因为用户编辑的是扁平数据)
  983. // const flatData = this.flattenedTableData
  984. // flatData.forEach((item, index) => {
  985. // if (!item.isCategory) {
  986. // // 非空验证
  987. // if (!item.itemName || String(item.itemName).trim() === '') {
  988. // errors.push(`第${item.seq}行:项目名称不能为空`)
  989. // }
  990. // if (!item.unit || String(item.unit).trim() === '') {
  991. // errors.push(`第${item.seq}行:计量单位不能为空`)
  992. // }
  993. // if (
  994. // !item.originalValue ||
  995. // String(item.originalValue).trim() === ''
  996. // ) {
  997. // errors.push(`第${item.seq}行:固定资产原值不能为空`)
  998. // }
  999. // if (!item.entryDate || String(item.entryDate).trim() === '') {
  1000. // errors.push(`第${item.seq}行:入帐或竣工验收日期不能为空`)
  1001. // }
  1002. // if (
  1003. // !item.depreciationPeriod ||
  1004. // String(item.depreciationPeriod).trim() === ''
  1005. // ) {
  1006. // errors.push(`第${item.seq}行:折旧年限不能为空`)
  1007. // }
  1008. // if (
  1009. // !item.depreciationExpense ||
  1010. // String(item.depreciationExpense).trim() === ''
  1011. // ) {
  1012. // errors.push(`第${item.seq}行:折旧费不能为空`)
  1013. // }
  1014. // if (!item.fundSource || String(item.fundSource).trim() === '') {
  1015. // errors.push(`第${item.seq}行:资金来源不能为空`)
  1016. // }
  1017. // // 格式验证
  1018. // if (
  1019. // item.originalValue &&
  1020. // String(item.originalValue).trim() !== '' &&
  1021. // !/^\d+(\.\d+)?$/.test(String(item.originalValue))
  1022. // ) {
  1023. // errors.push(`第${item.seq}行:固定资产原值格式不正确,请输入数字`)
  1024. // }
  1025. // if (
  1026. // item.depreciationPeriod &&
  1027. // String(item.depreciationPeriod).trim() !== '' &&
  1028. // !/^\d+$/.test(String(item.depreciationPeriod))
  1029. // ) {
  1030. // errors.push(`第${item.seq}行:折旧年限必须是正整数`)
  1031. // }
  1032. // if (
  1033. // item.depreciationExpense &&
  1034. // String(item.depreciationExpense).trim() !== '' &&
  1035. // !/^\d+(\.\d+)?$/.test(String(item.depreciationExpense))
  1036. // ) {
  1037. // errors.push(`第${item.seq}行:折旧费格式不正确,请输入数字`)
  1038. // }
  1039. // }
  1040. // })
  1041. // this.validationErrors = errors
  1042. // return errors.length === 0
  1043. // },
  1044. // 获取表格数据(用于保存)
  1045. // 需要将扁平数据同步回嵌套结构
  1046. getTableData() {
  1047. // 同步扁平数据的修改到嵌套结构
  1048. const flatData = this.flattenedData
  1049. const syncDataToNested = (items) => {
  1050. return items.map((item) => {
  1051. if (item.isCategory) {
  1052. // 分类行
  1053. const newItem = { ...item }
  1054. if (item.children && Array.isArray(item.children)) {
  1055. newItem.children = syncDataToNested(item.children)
  1056. }
  1057. return newItem
  1058. } else {
  1059. // 普通行,从扁平数据中同步
  1060. const flatItem = flatData.find(
  1061. (f) => f.id === item.id && !f.isCategory
  1062. )
  1063. if (flatItem) {
  1064. return {
  1065. ...item,
  1066. itemName: flatItem.itemName,
  1067. unit: flatItem.unit,
  1068. originalValue: flatItem.originalValue,
  1069. entryDate: flatItem.entryDate,
  1070. depreciationPeriod: flatItem.depreciationPeriod,
  1071. depreciationExpense: flatItem.depreciationExpense,
  1072. fundSource: flatItem.fundSource,
  1073. remark: flatItem.remark,
  1074. }
  1075. }
  1076. return item
  1077. }
  1078. })
  1079. }
  1080. return syncDataToNested(this.fixedAssetsData)
  1081. },
  1082. // 保存数据(调用 saveSingleRecordSurvey 接口)
  1083. async saveData() {
  1084. // 保存前校验
  1085. const validationErrors = this.validateBeforeSave()
  1086. if (validationErrors && validationErrors.length > 0) {
  1087. Message.error('表单验证失败,请检查后再保存')
  1088. return false
  1089. }
  1090. try {
  1091. // 格式化保存数据为接口需要的格式
  1092. const saveData = []
  1093. // 需要过滤掉已删除的行
  1094. const isDeleted = (row) => {
  1095. const key = String(row.rowid || row.rowId || row.id || '')
  1096. return key && this.deletedRowids.includes(key)
  1097. }
  1098. const flatData = (this.flattenedData || []).filter(
  1099. (row) => !isDeleted(row)
  1100. )
  1101. // 建立 rowid -> id 的映射,用于计算 parentId(子项需要父项的 id)
  1102. const rowidToIdMap = new Map()
  1103. flatData.forEach((row) => {
  1104. const rowid = row.rowid || row.id
  1105. const id = row.id || row.rowid || row.rowId
  1106. if (rowid && id) {
  1107. rowidToIdMap.set(rowid, id)
  1108. }
  1109. })
  1110. // 遍历所有行数据(包括父项和子项)
  1111. flatData.forEach((row) => {
  1112. const rowid = row.rowid || row.id
  1113. // 计算 parentId:父项为 '-1';子项为父项的 id(通过 parent 的 rowid 映射得到)
  1114. let parentId = '-1'
  1115. const parentRowid = row.parentid || row.parentId || row.parentID
  1116. if (parentRowid && parentRowid !== '-1' && parentRowid !== -1) {
  1117. parentId =
  1118. rowidToIdMap.get(parentRowid) ||
  1119. row.categoryId || // 兼容 generateFlattenedData 填充的父分类 id
  1120. ''
  1121. }
  1122. // 遍历所有表头字段,保存每个字段的值
  1123. this.dynamicColumns.forEach((column) => {
  1124. const value = row[column.prop]
  1125. // 如果值存在,添加到保存数据中(父项和子项都保存)
  1126. if (value !== undefined && value !== null && value !== '') {
  1127. saveData.push({
  1128. rowid: rowid,
  1129. parentId: String(parentId),
  1130. rkey: column.label, // 字段标签(对应 fixedFields)
  1131. rvalue: String(value), // 字段值
  1132. })
  1133. }
  1134. })
  1135. })
  1136. // 为每条数据添加公共字段
  1137. const finalSaveData = saveData.map((item) => {
  1138. const dataItem = {
  1139. ...item,
  1140. auditedUnitId: this.auditedUnitId || '',
  1141. surveyTemplateId: this.surveyTemplateId || '',
  1142. catalogId: this.catalogId || '',
  1143. }
  1144. // 如果有 uploadId(编辑模式),添加 uploadId 字段
  1145. if (this.uploadId) {
  1146. dataItem.uploadId = this.uploadId
  1147. }
  1148. // 添加 periodRecordId(动态表调查填报表格的行ID)
  1149. if (this.periodRecordId) {
  1150. dataItem.periodRecordId = this.periodRecordId
  1151. }
  1152. return dataItem
  1153. })
  1154. console.log('保存数据:', finalSaveData)
  1155. // 调用保存接口
  1156. const res = await saveSingleRecordSurvey(finalSaveData)
  1157. if (res && res.code === 200) {
  1158. Message.success('保存成功')
  1159. // 触发保存事件
  1160. this.$emit('save', finalSaveData)
  1161. // 触发刷新事件
  1162. this.$emit('refresh')
  1163. return true
  1164. } else {
  1165. Message.error(res?.message || '保存失败')
  1166. return false
  1167. }
  1168. } catch (err) {
  1169. console.error('保存失败', err)
  1170. Message.error(err?.message || '保存失败')
  1171. return false
  1172. }
  1173. },
  1174. // 保存前验证
  1175. validateBeforeSave() {
  1176. const errors = []
  1177. const categories = Array.isArray(this.fixedAssetsData)
  1178. ? this.fixedAssetsData
  1179. : []
  1180. categories.forEach((category) => {
  1181. const items = Array.isArray(category.items) ? category.items : []
  1182. items.forEach((item, index) => {
  1183. const rowNum = index + 1
  1184. // 必填校验
  1185. if (!item.name) {
  1186. errors.push(
  1187. `${category.category} 第${rowNum}行:项目名称不能为空`
  1188. )
  1189. }
  1190. if (!item.unit) {
  1191. errors.push(
  1192. `${category.category} 第${rowNum}行:计量单位不能为空`
  1193. )
  1194. }
  1195. if (
  1196. item.originalValue === '' ||
  1197. item.originalValue === undefined ||
  1198. item.originalValue === null
  1199. ) {
  1200. errors.push(
  1201. `${category.category} 第${rowNum}行:固定资产原值不能为空`
  1202. )
  1203. } else if (isNaN(Number(item.originalValue))) {
  1204. errors.push(
  1205. `${category.category} 第${rowNum}行:固定资产原值必须为数字`
  1206. )
  1207. }
  1208. if (!item.entryDate) {
  1209. errors.push(
  1210. `${category.category} 第${rowNum}行:入帐或竣工验收日期不能为空`
  1211. )
  1212. }
  1213. if (
  1214. item.depreciationPeriod === '' ||
  1215. item.depreciationPeriod === undefined ||
  1216. item.depreciationPeriod === null
  1217. ) {
  1218. errors.push(
  1219. `${category.category} 第${rowNum}行:折旧年限不能为空`
  1220. )
  1221. } else if (isNaN(Number(item.depreciationPeriod))) {
  1222. errors.push(
  1223. `${category.category} 第${rowNum}行:折旧年限必须为数字`
  1224. )
  1225. }
  1226. if (
  1227. item.depreciationExpense === '' ||
  1228. item.depreciationExpense === undefined ||
  1229. item.depreciationExpense === null
  1230. ) {
  1231. errors.push(`${category.category} 第${rowNum}行:折旧费不能为空`)
  1232. } else if (isNaN(Number(item.depreciationExpense))) {
  1233. errors.push(
  1234. `${category.category} 第${rowNum}行:折旧费必须为数字`
  1235. )
  1236. }
  1237. if (!item.fundSource) {
  1238. errors.push(
  1239. `${category.category} 第${rowNum}行:资金来源不能为空`
  1240. )
  1241. }
  1242. })
  1243. })
  1244. this.validationErrors = errors
  1245. return errors
  1246. },
  1247. isCategoryItem(item) {
  1248. if (!item) return false
  1249. const flag = item.isCategory
  1250. if (typeof flag === 'boolean') return flag
  1251. if (typeof flag === 'number') return flag !== 0
  1252. if (typeof flag === 'string') {
  1253. const normalized = flag.toLowerCase()
  1254. if (['1', 'true', 'yes', 'y'].includes(normalized)) return true
  1255. }
  1256. if (Array.isArray(item.children) && item.children.length > 0) {
  1257. return true
  1258. }
  1259. return false
  1260. },
  1261. // 规范化节点数据(保留所有原始字段)
  1262. normalizeItemNode(item) {
  1263. if (!item || typeof item !== 'object') return item
  1264. // 深拷贝保留所有原始字段
  1265. const cloned = this.deepClone(item)
  1266. // 确保 id 存在
  1267. cloned.id =
  1268. cloned.rowid ||
  1269. cloned.rowId ||
  1270. cloned.id ||
  1271. cloned.itemId ||
  1272. cloned.code ||
  1273. cloned.key ||
  1274. `auto-${this.autoIdSeed++}`
  1275. // 保留 rowid 字段(用于父子关系匹配)
  1276. if (!cloned.rowid && !cloned.rowId) {
  1277. cloned.rowid = cloned.id
  1278. } else {
  1279. cloned.rowid = cloned.rowid || cloned.rowId || cloned.id
  1280. }
  1281. // 规范化常用字段(如果不存在则设置默认值,但不覆盖已有值)
  1282. if (!cloned.itemName) {
  1283. cloned.itemName = cloned.name || cloned.title || cloned['项目'] || ''
  1284. }
  1285. if (cloned.unit === undefined) {
  1286. cloned.unit = cloned.unitName || cloned['单位'] || ''
  1287. }
  1288. if (cloned.originalValue === undefined) {
  1289. cloned.originalValue = cloned['原值'] || cloned.value || ''
  1290. }
  1291. if (cloned.entryDate === undefined) {
  1292. cloned.entryDate =
  1293. cloned['入帐或竣工验收日期'] || cloned.entry_date || ''
  1294. }
  1295. if (cloned.depreciationPeriod === undefined) {
  1296. cloned.depreciationPeriod = cloned['折旧年限'] || cloned.period || ''
  1297. }
  1298. if (cloned.depreciationExpense === undefined) {
  1299. cloned.depreciationExpense = cloned['折旧费'] || cloned.expense || ''
  1300. }
  1301. if (cloned.fundSource === undefined) {
  1302. cloned.fundSource = cloned.fund_source || cloned['资金来源'] || ''
  1303. }
  1304. if (cloned.remark === undefined) {
  1305. cloned.remark = cloned.note || cloned['备注'] || ''
  1306. }
  1307. if (!cloned.categorySeq) {
  1308. cloned.categorySeq =
  1309. cloned.category_code ||
  1310. cloned.seq ||
  1311. cloned.index ||
  1312. cloned['序号'] ||
  1313. cloned.id
  1314. }
  1315. // 如果已经是分类项,保持 isCategory 状态
  1316. if (cloned.isCategory === undefined) {
  1317. cloned.isCategory = false // 默认不是分类,后续会根据是否有子项来设置
  1318. }
  1319. // 处理 children
  1320. if (Array.isArray(cloned.children)) {
  1321. cloned.children = this.deepClone(cloned.children)
  1322. } else {
  1323. cloned.children = []
  1324. }
  1325. return cloned
  1326. },
  1327. // 规范化保存的数据
  1328. normalizeSavedData(saved) {
  1329. if (!saved) return null
  1330. const extractArray = (value) => {
  1331. if (!value) return null
  1332. if (Array.isArray(value)) return this.deepClone(value)
  1333. if (value && typeof value === 'object') {
  1334. if (Array.isArray(value.itemlist))
  1335. return this.deepClone(value.itemlist)
  1336. if (Array.isArray(value.items)) return this.deepClone(value.items)
  1337. if (Array.isArray(value.children))
  1338. return this.deepClone(value.children)
  1339. if (Array.isArray(value.data)) return this.deepClone(value.data)
  1340. if (Array.isArray(value.value)) return this.deepClone(value.value)
  1341. if (Array.isArray(value.records))
  1342. return this.deepClone(value.records)
  1343. if (Array.isArray(value.rows)) return this.deepClone(value.rows)
  1344. }
  1345. return null
  1346. }
  1347. let normalized = extractArray(saved)
  1348. // 检测三元组格式并转换
  1349. if (
  1350. Array.isArray(normalized) &&
  1351. normalized.length > 0 &&
  1352. normalized.every(
  1353. (it) =>
  1354. it &&
  1355. typeof it === 'object' &&
  1356. 'rkey' in it &&
  1357. 'rvalue' in it &&
  1358. ('rowid' in it || 'rowId' in it)
  1359. )
  1360. ) {
  1361. normalized = this.transformTripletArrayToRows(normalized)
  1362. }
  1363. if (!normalized && saved && typeof saved === 'object') {
  1364. const values = Object.values(saved)
  1365. if (
  1366. values.length > 0 &&
  1367. values.every(
  1368. (item) =>
  1369. item &&
  1370. typeof item === 'object' &&
  1371. (item.itemName || item.children || item.isCategory)
  1372. )
  1373. ) {
  1374. normalized = this.deepClone(values)
  1375. }
  1376. }
  1377. if (!normalized || normalized.length === 0) return null
  1378. const hasNestedChildren = normalized.some(
  1379. (item) =>
  1380. item && Array.isArray(item.children) && item.children.length > 0
  1381. )
  1382. if (hasNestedChildren) {
  1383. return normalized.map((item) => this.normalizeItemNode(item))
  1384. }
  1385. // 如果是扁平结构,尝试根据 parentId 组装
  1386. // 首先建立 rowid 到节点的映射(用于根据 parentid 查找父节点)
  1387. const itemsMapById = new Map() // 通过 id 查找节点
  1388. const itemsMapByRowid = new Map() // 通过 rowid 查找节点(用于父子关系)
  1389. normalized.forEach((raw) => {
  1390. const node = this.normalizeItemNode(raw)
  1391. // 获取节点的 id
  1392. const nodeId = node.id
  1393. // 获取节点的 rowid(可能用于作为父节点的标识)
  1394. const nodeRowid = raw.rowid || raw.rowId || raw.id || nodeId
  1395. itemsMapById.set(nodeId, node)
  1396. itemsMapByRowid.set(nodeRowid, node)
  1397. })
  1398. const roots = []
  1399. normalized.forEach((raw) => {
  1400. const nodeId =
  1401. raw.id ||
  1402. raw.itemId ||
  1403. raw.rowid ||
  1404. raw.rowId ||
  1405. raw.code ||
  1406. raw.key
  1407. const node = itemsMapById.get(nodeId)
  1408. if (!node) return
  1409. // 获取父引用字段:优先支持 parentId(按父项 id 关联),兼容 parentid(按父项 rowid 关联)
  1410. const parentid =
  1411. raw.parentid ??
  1412. raw.parentId ??
  1413. raw.parentID ??
  1414. raw.parentCode ??
  1415. raw.parent_code
  1416. const parentIdById = raw.parentId ?? raw.parentID
  1417. // 判定根节点:parentId 为 "-1" 或未提供父引用
  1418. const isRootById = parentIdById === '-1'
  1419. const hasAnyParentRef =
  1420. (parentIdById !== undefined &&
  1421. parentIdById !== null &&
  1422. parentIdById !== '') ||
  1423. (parentid !== undefined && parentid !== null && parentid !== '')
  1424. if (!hasAnyParentRef || isRootById || parentid === '-1') {
  1425. // 作为根节点(父项)
  1426. // 检查是否存在子项指向它(通过 id 或 rowid)
  1427. const hasChildren = normalized.some((item) => {
  1428. const childParentId = item.parentId ?? item.parentID
  1429. const childParentid =
  1430. item.parentid ?? item.parentId ?? item.parentID
  1431. const currentRowid = raw.rowid || raw.rowId || raw.id
  1432. return (
  1433. (childParentId &&
  1434. childParentId !== '-1' &&
  1435. childParentId === nodeId) ||
  1436. (childParentid &&
  1437. childParentid !== '-1' &&
  1438. childParentid === currentRowid)
  1439. )
  1440. })
  1441. if (hasChildren) {
  1442. node.isCategory = true
  1443. }
  1444. roots.push(node)
  1445. } else {
  1446. // 处理子项:优先使用 parentId -> idMap;否则使用 parentid(rowid) -> rowidMap
  1447. const parentNode =
  1448. (parentIdById &&
  1449. parentIdById !== '-1' &&
  1450. itemsMapById.get(parentIdById)) ||
  1451. (parentid && parentid !== '-1' && itemsMapByRowid.get(parentid))
  1452. if (parentNode) {
  1453. if (!Array.isArray(parentNode.children)) {
  1454. parentNode.children = []
  1455. }
  1456. parentNode.children.push(node)
  1457. // 父项标记为分类(不可操作)
  1458. parentNode.isCategory = true
  1459. } else {
  1460. // 找不到父节点,作为根节点
  1461. roots.push(node)
  1462. }
  1463. }
  1464. })
  1465. return roots.length > 0 ? roots : normalized
  1466. },
  1467. },
  1468. }
  1469. </script>
  1470. <style scoped lang="scss">
  1471. .fixed-assets-table-container {
  1472. // 分类行样式
  1473. ::v-deep .category-row {
  1474. background-color: #f5f7fa !important;
  1475. td {
  1476. background-color: #f5f7fa !important;
  1477. font-weight: bold;
  1478. }
  1479. .category-name {
  1480. color: #409eff;
  1481. font-weight: bold;
  1482. }
  1483. .category-seq {
  1484. color: #409eff;
  1485. font-weight: bold;
  1486. }
  1487. .category-value {
  1488. color: #606266;
  1489. font-weight: normal;
  1490. }
  1491. }
  1492. // 操作按钮样式
  1493. .operation-buttons {
  1494. display: flex;
  1495. justify-content: center;
  1496. gap: 5px;
  1497. .el-button {
  1498. padding: 5px;
  1499. min-width: 24px;
  1500. height: 24px;
  1501. border-radius: 50%;
  1502. background-color: #000;
  1503. color: #fff;
  1504. border: none;
  1505. &:hover {
  1506. background-color: #333;
  1507. }
  1508. i {
  1509. font-size: 14px;
  1510. }
  1511. }
  1512. }
  1513. // 输入框样式
  1514. ::v-deep .el-input__inner {
  1515. border: 1px solid #dcdfe6;
  1516. border-radius: 4px;
  1517. &:focus {
  1518. border-color: #409eff;
  1519. }
  1520. }
  1521. // 日期选择器样式
  1522. ::v-deep .el-date-editor {
  1523. width: 100%;
  1524. }
  1525. }
  1526. </style>