cleaning.vue.phase4 100 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458
  1. <template>
  2. <div :class="prefixCls" class="case-data-cleaning-page">
  3. <div class="page-header">
  4. <div class="page-header-main">
  5. <div class="page-title-area">
  6. <Button type="text" class="back-btn" @click="handleBack">←</Button>
  7. <div class="page-title-block">
  8. <div class="page-title">案件数据清洗</div>
  9. </div>
  10. </div>
  11. <div class="page-header-actions">
  12. <Button @click="toggleLeft">{{ leftCollapsed ? '展开左侧' : '收起左侧' }}</Button>
  13. <Button type="primary" :loading="importingData" @click="handleImportData">导入数据</Button>
  14. </div>
  15. </div>
  16. </div>
  17. <div class="main-layout" :style="{ gridTemplateColumns: layoutTemplateColumns }">
  18. <aside v-show="!leftCollapsed" class="left-panel">
  19. <div class="tree-header">
  20. <span class="label">文件分类</span>
  21. <div class="actions">
  22. <Button size="small" @click="sortMode = sortMode === 'asc' ? '' : 'asc'">升序</Button>
  23. <Button size="small" @click="sortMode = sortMode === 'desc' ? '' : 'desc'">降序</Button>
  24. </div>
  25. </div>
  26. <div class="tree-wrap">
  27. <BasicTree
  28. class="cleaning-file-tree"
  29. :tree-data="treeData"
  30. :tree-data-simple-mode="false"
  31. :field-names="{ key: 'key', title: 'title', children: 'children' }"
  32. :default-expand-all="true"
  33. v-model:selectedKeys="selectedKeys"
  34. @select="handleSelect"
  35. >
  36. <template #title="node">
  37. <div v-if="isTreeFileNode(node.raw)" class="tree-file-row" :title="getTreeFileName(node.raw)">
  38. <span class="tree-file-name">{{ getTreeFileName(node.raw) }}</span>
  39. </div>
  40. <div
  41. v-else
  42. class="tree-sheet-row"
  43. :class="{
  44. 'is-success': getSheetNodeVisualState(node.raw) === 'success',
  45. 'is-fail': getSheetNodeVisualState(node.raw) === 'fail',
  46. 'is-analyzing': getSheetNodeVisualState(node.raw) === 'analyzing',
  47. }"
  48. :title="getTreeSheetTitle(node.raw)"
  49. >
  50. <span class="tree-sheet-status">
  51. <span
  52. v-if="getSheetNodeVisualState(node.raw) === 'analyzing'"
  53. class="tree-sheet-status-spinner"
  54. ></span>
  55. <template v-else>
  56. {{ getSheetNodeVisualState(node.raw) === 'success' ? '✓' : '✕' }}
  57. </template>
  58. </span>
  59. <span class="tree-sheet-name">{{ getTreeSheetName(node.raw) }}</span>
  60. </div>
  61. </template>
  62. </BasicTree>
  63. </div>
  64. <div class="legend">
  65. <div v-for="item in iconTips" :key="item.key" class="legend-item">
  66. <span class="legend-icon" :class="`is-${item.key}`">
  67. <span v-if="item.key === 'matched' || item.key === 'unmatched'" class="legend-dot"></span>
  68. <template v-else>{{ item.icon }}</template>
  69. </span>
  70. <span class="legend-text">{{ item.label }}</span>
  71. </div>
  72. </div>
  73. </aside>
  74. <div v-show="!leftCollapsed" class="panel-resizer" @mousedown="startResize('left', $event)"></div>
  75. <section class="middle-panel">
  76. <div class="template-match-bar" :class="{ 'is-empty': !hasTemplateSelected }">
  77. <div class="template-match-main">
  78. <span class="template-match-icon"></span>
  79. <div class="template-match-copy">
  80. <div class="template-match-label">匹配模板</div>
  81. <div class="template-match-name">
  82. {{ currentTemplateDisplayName }}
  83. <span v-if="!hasTemplateSelected" class="template-match-badge is-pending">待配置</span>
  84. <span v-else-if="templateDetailFromMock" class="template-match-badge">示例</span>
  85. </div>
  86. </div>
  87. </div>
  88. <Button type="link" class="template-select-trigger" @click="openTemplateModal">选择模板</Button>
  89. </div>
  90. <template v-if="hasTemplateSelected">
  91. <div class="middle-panel-body">
  92. <FieldMappingCard />
  93. <div
  94. class="template-foot-panel"
  95. :class="{
  96. 'is-collapsed': showRecognitionPanel && !recognitionPanelExpanded,
  97. 'is-compact': !showRecognitionPanel,
  98. }"
  99. >
  100. <RecognitionRulePanel />
  101. <ChildTemplatePanel />
  102. </div>
  103. </div>
  104. </template>
  105. <MiddleEmptyState v-else />
  106. </section>
  107. <div class="panel-resizer" @mousedown="startResize('middle', $event)"></div>
  108. <section class="right-panel">
  109. <div class="panel-header">
  110. <div class="panel-title-wrap">
  111. <div class="panel-title">数据清洗区</div>
  112. </div>
  113. </div>
  114. <div class="toolbar toolbar--column-actions">
  115. <div class="toolbar-head">
  116. <span class="toolbar-label">列操作</span>
  117. </div>
  118. <div class="toolbar-actions">
  119. <Button size="small" type="primary" @click="openAddColumn">新增列</Button>
  120. <Button size="small" @click="openSplitColumn">拆分列</Button>
  121. <Button size="small" @click="openEditColumn">编辑列</Button>
  122. <Button size="small" @click="openDeleteColumn">删除列</Button>
  123. </div>
  124. <div class="toolbar-meta toolbar-meta--header-row">
  125. <span class="meta-label">表头行</span>
  126. <Input
  127. v-model:value="headerRowInput"
  128. size="small"
  129. class="meta-input"
  130. placeholder="输入行号"
  131. @pressEnter="applyHeaderRowInput"
  132. @blur="applyHeaderRowInput"
  133. />
  134. </div>
  135. </div>
  136. <div class="right-panel-body">
  137. <div ref="tableMockRef" class="table-mock">
  138. <div class="mock-head" :style="{ gridTemplateColumns: mockGridTemplate }">
  139. <span class="row-index-cell row-index-head">表头行</span>
  140. <span
  141. v-for="(header, colIndex) in rightTableHeaders"
  142. :key="header"
  143. :data-preview-col="colIndex"
  144. :class="{
  145. 'is-added': addedColumns.includes(header),
  146. active: selectedAddedColumn === header,
  147. 'is-current-col': currentColumn === colIndex,
  148. 'is-first-col': colIndex === 0,
  149. 'is-last-col': colIndex === rightTableHeaders.length - 1,
  150. }"
  151. @click="onClickHeader(header)"
  152. >
  153. <span>{{ header }}</span>
  154. <span
  155. v-if="getColumnRuleBadgeState(colIndex)"
  156. class="rule-badge"
  157. :class="`is-${getColumnRuleBadgeState(colIndex)}`"
  158. :title="getColumnRuleBadgeTitle(colIndex)"
  159. ></span>
  160. </span>
  161. </div>
  162. <div class="mock-match-row" :style="{ gridTemplateColumns: mockGridTemplate }">
  163. <span class="row-index-cell match-row-index">目标字段</span>
  164. <span
  165. v-for="(header, colIndex) in rightTableHeaders"
  166. :key="`mapping-${header}`"
  167. :data-preview-col="colIndex"
  168. :class="{
  169. 'is-current-col': currentColumn === colIndex,
  170. 'is-first-col': colIndex === 0,
  171. 'is-last-col': colIndex === rightTableHeaders.length - 1,
  172. 'is-empty': !getMatchedTargetFieldText(header),
  173. }"
  174. @click="onClickHeader(header)"
  175. >
  176. <span class="match-field-content">
  177. <span class="match-field-text">{{ getMatchedTargetFieldText(header) || '-' }}</span>
  178. <span class="match-status-group">
  179. <span
  180. class="match-status-badge"
  181. :class="getMatchedHeaderStatusClass(header)"
  182. :title="getMatchedHeaderStatusTitle(header) || undefined"
  183. >
  184. {{ getMatchedHeaderStatusText(header) }}
  185. </span>
  186. <span
  187. v-if="hasHeaderFormatError(header)"
  188. class="match-status-badge is-formatError"
  189. :title="getHeaderFormatErrorTitle(header) || undefined"
  190. >
  191. !
  192. </span>
  193. </span>
  194. </span>
  195. </span>
  196. </div>
  197. <div
  198. class="mock-row"
  199. :style="{ gridTemplateColumns: mockGridTemplate }"
  200. :class="{
  201. 'is-ignored-row': ignoredRowsSet.has(rowIndex + 1),
  202. 'is-current-row': selectedRowIndex === rowIndex,
  203. 'is-header-row': headerRowIndex === rowIndex,
  204. }"
  205. v-for="(row, rowIndex) in tablePreviewRows"
  206. :key="`row-${rowIndex}`"
  207. >
  208. <span class="row-index-cell" @click="selectedRowIndex = rowIndex">
  209. {{ rowIndex + 1 }}
  210. </span>
  211. <span
  212. v-for="(header, colIndex) in rightTableHeaders"
  213. :key="`${header}-${rowIndex}`"
  214. :data-preview-col="colIndex"
  215. :class="{
  216. 'is-current-col': currentColumn === colIndex,
  217. 'is-first-col': colIndex === 0,
  218. 'is-last-col': colIndex === rightTableHeaders.length - 1,
  219. }"
  220. @click="onClickCell(rowIndex, header, colIndex)"
  221. >
  222. {{ row[header] || '' }}
  223. </span>
  224. </div>
  225. </div>
  226. <CleaningRulePanel />
  227. </div>
  228. </section>
  229. </div>
  230. <AddColumnModal />
  231. <SplitColumnModal />
  232. <Modal v-model:open="deleteColumnDialogOpen" title="删除列" :width="520" :centered="true" @ok="confirmDeleteColumn">
  233. <div>确认删除列「{{ selectedAddedColumn || '-' }}」?</div>
  234. </Modal>
  235. <TemplateSelectModal />
  236. </div>
  237. </template>
  238. <script lang="ts" setup name="ViewsDataCleaning">
  239. import { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
  240. import { DownOutlined, UpOutlined } from '@ant-design/icons-vue';
  241. import { Button, Checkbox, Input, InputNumber, Modal, Popover, Radio, Select, Switch, Tooltip } from 'ant-design-vue';
  242. import { EmptyState } from '@/core/components/EmptyState';
  243. import { BasicTree } from '@/core/components/Tree';
  244. import { useDesign } from '@/core/hooks/web/useDesign';
  245. import { useGo, useQuery } from '@/core/hooks/web/usePage';
  246. import { PageEnum } from '@/core/enums/pageEnum';
  247. import { useMessage } from '@/core/hooks/web/useMessage';
  248. import type { FileInfo } from '@/types/api';
  249. import { tplCategoryList, tplCheckTableName, tplDetail, tplPage } from '@/plat/api/tpl/tplApi';
  250. import { rawPreview } from '@/case/api/govern/governApi';
  251. import type { TableInfo } from '@/plat/api/tpl/types/dto';
  252. import {
  253. FunAbs,
  254. FunDate,
  255. FunDateTime,
  256. FunExtra,
  257. FunMultiplier,
  258. FunRadix,
  259. FunRegular,
  260. FunReplace,
  261. FunSpExtra,
  262. FunTime,
  263. MappingEnum,
  264. RuleMapping,
  265. RuleSplit,
  266. } from '../../../dm/types/Func';
  267. import {
  268. buildRemoveSpecialRegex,
  269. groupColumnsFnValue,
  270. normalizeDecimalBaseLabel,
  271. runCleaningPreviewPipeline,
  272. type CleaningRuleKey as RulePipelineKey,
  273. } from './cleaningRuleUtils';
  274. import { resetImportProgressSession } from './importProgressSession';
  275. import { FileStatusEnum } from '../../types/enum';
  276. import { FILE_INFO_STORAGE_KEY, IMPORT_FILE_INFO_STORAGE_KEY, dateTypeOptions, decimalBaseOptions, defaultRightTableHeaders, delimiterOptions, iconTips, mockDataColumnWidth, mockIndexColumnWidth, replaceModeOptions, ruleExampleMap, ruleNameMap, rulesList } from './cleaning/constants';
  277. import type { CleaningRuleItem, CleaningRuleKey, FileTreeNode, FormatValidationType, GroupingColumnDraft, HeaderFormatValidationState, MappingTargetField, NodeCacheState, PreviewColumnSnapshot, PreviewFileDataset, PreviewStateSnapshot, RecognitionBasicInfoRow, RecognitionDirectionComparableMap, RecognitionDirectionField, RecognitionDirectionItem, RecognitionRuleTabKey, SelectedRuleItem, SheetNodeVisualState, TemplateListItem, TemplateTab, TreeFileInfo, TreeStoredColumnRule, TreeStoredFunRule, TreeTableRuleInfo } from './cleaning/types';
  278. import { createGroupingColumnDraft, getFilledGroupingFieldCount, normalizeGroupingColumnsDraft, normalizeSplitColumnMode, normalizeSplitColumnValue, resolveSplitColumnLength, splitSourceValueByConfig } from './cleaning/utils/columnSplit';
  279. import { getVisibleOriginOptionList, scrollSelectedTreeNodeIntoView } from './cleaning/utils/domScroll';
  280. import { buildFormatValidationTitle, isValidAcctCardNoValue, isValidDateOnlyValue, isValidDateTimeValue, isValidIdCardNumberValue, isValidNumberValue, isValidPhoneValue, isValidTimeOnlyValue, normalizeCallTypeCoreValue, normalizeFormatValidationType, normalizeLoanFlagCoreValue, shouldSkipFormatValidationByTarget } from './cleaning/utils/formatValidate';
  281. import { buildHeaderLabelsByRow, collectMatchedSheetsWithoutHeaderRow, pickPreviewRowByHeaders } from './cleaning/utils/headerRow';
  282. import { loadStoredFileList, mockFileList, mockTemplateList, mockTemplateMap } from './cleaning/utils/mockData';
  283. import { clearRecognitionBasicInfoFilledContent, hasOwnRecognitionBasicInfoConfig, hasRecognitionBasicInfoFilledContent, isSameRecognitionBasicInfoConfig, mergeRecognitionDirectionKeywords, normalizeRecognitionBasicInfoText, normalizeRecognitionDirectionGroup, parseRecognitionBasicInfoRows, parseRecognitionDirectionEntries, resolveRecognitionBasicInfoTargetField, resolveRecognitionDirectionTab, splitRecognitionDirectionKeywords } from './cleaning/utils/recognition';
  284. import { buildStoredColumnRulePayload, defaultRuleConfig, deserializeStoredRule, getSplitColumnRuleIdentity, isCleaningRuleEnabledInUi, sanitizeFileInfoTree, sanitizeSelectedRuleGroups, sanitizeSelectedRuleList, serializeStoredFunRules } from './cleaning/utils/ruleSerialize';
  285. import { buildTargetFieldsFromRuleList, buildTargetFieldsFromTemplateInfo, findTransAmountTableRule, findTransAmountTemplateFieldMeta, getOrderedTemplateFields, getRawRuleFlagValue, getRecordZfValue, getTableRuleRequiredFlag, getTableRuleTargetField, getTableRuleTargetKey, getTableRuleTargetLabel, getTemplateFieldRequiredFlag, getTemplateFieldTargetKey, getTemplateFieldTargetLabel, getZfFlag, isLoanFlagTargetField, isTransAmountTargetField, normalizeTableRuleList, pickFirstDefinedRuleFlagValue, resolveStoredZfFlag } from './cleaning/utils/tableRuleBuild';
  286. import { buildStoredLineNo, cloneDeepSafe, findBestMatchedHeader, getRequestErrorMessage, normalizeColumnNameForCompare, normalizeFieldMatchText, normalizeFormatValue, normalizeGeneratedColumnHeaderRowIndex, normalizeRecognitionConfigText, normalizeRuleText, normalizeSingleLineText, normalizeStoredLineNoToRowIndex, normalizeTemplateClassifyText, normalizeTemplateHeaders, normalizeTemplateId, normalizeTemplateNameKey, renameRecordKeys, resolveTemplateNameCheckResult, toRuleBoolean } from './cleaning/utils/text';
  287. import { buildTreeFromFlatList, findFirstSelectableKey, findTreeNodeByKey, getFileStatusVisualState, getTreeFileName, getTreeNodeKey, getTreeSheetName, getTreeSheetTitle, isFileRootItem, isTemplateMatchedFailStatus, isTreeFileNode, isTreeSheetNode, toTreeNodeDeep } from './cleaning/utils/treeUtils';
  288. import AddColumnModal from './cleaning/components/AddColumnModal.vue';
  289. import SplitColumnModal from './cleaning/components/SplitColumnModal.vue';
  290. import TemplateSelectModal from './cleaning/components/TemplateSelectModal.vue';
  291. import { CleaningContextKey } from './cleaning/context';
  292. import CleaningRulePanel from './cleaning/components/CleaningRulePanel.vue';
  293. import { useCleaningRules } from './cleaning/composables/useCleaningRules';
  294. import FieldMappingCard from './cleaning/components/FieldMappingCard.vue';
  295. import OriginFieldPopover from './cleaning/components/OriginFieldPopover.vue';
  296. import RecognitionRulePanel from './cleaning/components/RecognitionRulePanel.vue';
  297. import ChildTemplatePanel from './cleaning/components/ChildTemplatePanel.vue';
  298. import MiddleEmptyState from './cleaning/components/MiddleEmptyState.vue';
  299. import { useFieldMapping } from './cleaning/composables/useFieldMapping';
  300. import { useRecognitionRules } from './cleaning/composables/useRecognitionRules';
  301. import { useTemplateLibrary } from './cleaning/composables/useTemplateLibrary';
  302. const { prefixCls } = useDesign('case-data-cleaning');
  303. const go = useGo();
  304. const pageQuery = useQuery();
  305. const { createMessage } = useMessage();
  306. const importingData = ref(false);
  307. const leftCollapsed = ref(false);
  308. const leftPanelWidth = ref(300);
  309. const middlePanelWidth = ref(520);
  310. const minLeftWidth = 220;
  311. const maxLeftWidth = 520;
  312. const minMiddleWidth = 360;
  313. const maxMiddleWidth = 900;
  314. const minRightWidth = 560;
  315. const resizeState = ref<{ type: 'left' | 'middle' | ''; startX: number; startLeft: number; startMiddle: number }>({
  316. type: '',
  317. startX: 0,
  318. startLeft: 300,
  319. startMiddle: 520,
  320. });
  321. const pendingClientX = ref<number | null>(null);
  322. const rafId = ref<number | null>(null);
  323. const layoutTemplateColumns = computed(() => {
  324. if (leftCollapsed.value) {
  325. return `${middlePanelWidth.value}px 6px minmax(${minRightWidth}px, 1fr)`;
  326. }
  327. return `${leftPanelWidth.value}px 6px ${middlePanelWidth.value}px 6px minmax(${minRightWidth}px, 1fr)`;
  328. });
  329. const selectedKeys = ref<string[]>([]);
  330. const isTableRuleMatched = (
  331. item?: TreeTableRuleInfo | null,
  332. options: { index?: number; amountZfEnabled?: boolean } = {},
  333. ) => {
  334. if (!item) return false;
  335. if (
  336. options.amountZfEnabled &&
  337. options.index !== undefined &&
  338. isLoanFlagTargetField(getTableRuleTargetField(item, options.index))
  339. ) {
  340. return true;
  341. }
  342. const fileColIndex = Number(item.fileColIndex);
  343. return (Number.isInteger(fileColIndex) && fileColIndex >= 0) || !!normalizeRuleText(item.fileColName);
  344. };
  345. const isMatchSuccessful = (requiredTotal: number, requiredMatched: number, matchedCount: number) => {
  346. if (requiredTotal > 0) {
  347. return requiredMatched === requiredTotal;
  348. }
  349. return matchedCount > 0;
  350. };
  351. const getTableRuleMatchStats = (list?: TreeTableRuleInfo[] | null) => {
  352. const normalizedRules = normalizeTableRuleList(list);
  353. const amountZfEnabled = normalizedRules.some(
  354. (item, index) => isTransAmountTargetField(getTableRuleTargetField(item, index)) && getZfFlag(item.zf),
  355. );
  356. const normalizedRuleItems = normalizedRules.map((item, index) => ({ item, index }));
  357. const requiredRules = normalizedRuleItems.filter(({ item }) => getTableRuleRequiredFlag(item));
  358. const requiredTotal = requiredRules.length;
  359. const requiredMatched = requiredRules.filter(({ item, index }) =>
  360. isTableRuleMatched(item, { index, amountZfEnabled }),
  361. ).length;
  362. const matchedCount = normalizedRuleItems.filter(({ item, index }) =>
  363. isTableRuleMatched(item, { index, amountZfEnabled }),
  364. ).length;
  365. return { requiredTotal, requiredMatched, matchedCount };
  366. };
  367. const isSheetMatchComplete = (item?: TreeFileInfo | null) => {
  368. if (!isTreeSheetNode(item)) return false;
  369. const { requiredTotal, requiredMatched, matchedCount } = getTableRuleMatchStats(item?.tableRuleList);
  370. return isMatchSuccessful(requiredTotal, requiredMatched, matchedCount);
  371. };
  372. const resolveSelectableTreeKey = (key: string) => {
  373. const selectedNode = findTreeNodeByKey(treeData.value, key);
  374. if (!selectedNode) return '';
  375. if (selectedNode.selectable !== false) return selectedNode.key;
  376. return findFirstSelectableKey(selectedNode.children || []);
  377. };
  378. const rightTableHeaders = ref([...defaultRightTableHeaders]);
  379. const addedColumns = ref<string[]>([]);
  380. const selectedAddedColumn = ref('');
  381. const sourceTableRows = ref<Record<string, string>[]>([]);
  382. const tablePreviewRows = ref<Record<string, string>[]>(JSON.parse(JSON.stringify(sourceTableRows.value)));
  383. const currentColumn = ref<number | null>(null);
  384. const selectedRowIndex = ref<number | null>(null);
  385. const headerRowIndex = ref<number | null>(null);
  386. // Phase 3:清洗规则域下沉到 composable。页面同名解构接住,原有代码与模板零改动。
  387. const cleaningRules = useCleaningRules({ addedColumns, createMessage, currentColumn, headerRowIndex, rightTableHeaders, sourceTableRows, tablePreviewRows });
  388. const { absoluteValueChecked, activeNodeKey, activeNodeTableRuleMap, activeRules, activeTemplateItem, addedColumnConfig, applyTableRuleRequiredFlags, buildAmountZfHandledTargetKeySet, buildEffectiveFieldMapping, buildGeneratedColumnHeaderRowMap, buildRecognitionBasicInfoMatchedTargetKeySet, currentRuleTitle, dateConfigType, decimalEnabled, decimalSourceBase, decimalTargetBase, effectiveFieldMapping, extractDataAfter, extractDataBefore, extractDataMode, extractSegmentRow, extractSegmentValue, fieldMapping, fileList, fixedTargetFields, generatedColumnHeaderRowMap, getEnabledGroupingColumnsForHeader, getGeneratedColumnHeaderRowIndex, groupingColumnsDraft, groupingEnabled, ignoredRowsSet, magnificationValue, matchedTargetFieldsByHeader, omitLineInput, rebuildGeneratedColumnsInSourceRows, recognitionAmountZfEnabled, recognitionBasicInfoMatchedTargetKeySet, recognitionBasicInfoRows, recognitionDirectionFields, recognitionRuleHandledTargetKeySet, removeCustomChecked, removeCustomText, removeSpaceChecked, removeTabChecked, replaceEnabled, replaceMode, replaceSource, replaceTarget, resolveTemplateFieldMeta, runRulePipeline, selectRules, selectedRules, selectedTemplateId, setGeneratedColumnCell, sortMode, splitColumnConfig, syncExtractDataMode, templateDetail, treeData } = cleaningRules;
  389. const initialNodeStatusSignatureMap = new Map<string, string>();
  390. const analyzingNodeKeys = ref<string[]>([]);
  391. const setNodeAnalyzing = (nodeKey: string, analyzing: boolean) => {
  392. if (!nodeKey) return;
  393. if (analyzing) {
  394. if (!analyzingNodeKeys.value.includes(nodeKey)) {
  395. analyzingNodeKeys.value = [...analyzingNodeKeys.value, nodeKey];
  396. }
  397. return;
  398. }
  399. if (analyzingNodeKeys.value.includes(nodeKey)) {
  400. analyzingNodeKeys.value = analyzingNodeKeys.value.filter((item) => item !== nodeKey);
  401. }
  402. };
  403. const buildNodeStatusSignature = (state: NodeCacheState) =>
  404. JSON.stringify({
  405. selectedTemplateId: state.selectedTemplateId,
  406. fieldMapping: state.fieldMapping,
  407. rightTableHeaders: state.rightTableHeaders,
  408. addedColumns: state.addedColumns,
  409. headerRowIndex: state.headerRowIndex,
  410. selectRules: state.selectRules.map((rules) =>
  411. rules.map((rule) => ({
  412. key: rule.key,
  413. name: rule.name,
  414. enabled: rule.enabled,
  415. rules: rule.rules,
  416. })),
  417. ),
  418. selectedRules: state.selectedRules.map((rule) => ({
  419. key: rule.key,
  420. name: rule.name,
  421. enabled: rule.enabled,
  422. rules: rule.rules,
  423. })),
  424. addedColumnConfig: state.addedColumnConfig,
  425. splitColumnConfig: state.splitColumnConfig,
  426. generatedColumnHeaderRowMap: state.generatedColumnHeaderRowMap,
  427. recognitionDirectionFields: (state.recognitionDirectionFields || []).map((item) => ({
  428. key: item.key,
  429. label: item.label,
  430. tab: item.tab,
  431. entries: item.entries.map((entry) => ({
  432. label: entry.label,
  433. value: entry.value,
  434. })),
  435. })),
  436. recognitionBasicInfoBaseFuncRegx: state.recognitionBasicInfoBaseFuncRegx,
  437. recognitionBasicInfoManualVisible: state.recognitionBasicInfoManualVisible,
  438. recognitionBasicInfoRows: state.recognitionBasicInfoRows.map((item) => ({
  439. headerMatch: item.headerMatch,
  440. targetField: item.targetField,
  441. endFlag: item.endFlag,
  442. })),
  443. recognitionAmountZfEnabled: state.recognitionAmountZfEnabled,
  444. childTemplateName: state.childTemplateName,
  445. });
  446. const applyNodeCacheState = (state: NodeCacheState) => {
  447. selectedTemplateId.value = state.selectedTemplateId;
  448. fieldMapping.value = cloneDeepSafe(state.fieldMapping);
  449. rightTableHeaders.value = [...state.rightTableHeaders];
  450. addedColumns.value = [...state.addedColumns];
  451. selectedAddedColumn.value = state.selectedAddedColumn;
  452. sourceTableRows.value = cloneDeepSafe(state.sourceTableRows);
  453. selectRules.value = sanitizeSelectedRuleGroups(cloneDeepSafe(state.selectRules));
  454. selectedRules.value = sanitizeSelectedRuleList(cloneDeepSafe(state.selectedRules));
  455. activeRules.value = state.activeRules;
  456. currentColumn.value = state.currentColumn;
  457. selectedRowIndex.value = state.selectedRowIndex;
  458. headerRowIndex.value = state.headerRowIndex;
  459. headerRowInput.value = state.headerRowInput;
  460. currentRuleTitle.value = state.currentRuleTitle;
  461. omitLineInput.value = state.omitLineInput;
  462. dateConfigType.value = state.dateConfigType;
  463. extractDataBefore.value = state.extractDataBefore;
  464. extractDataAfter.value = state.extractDataAfter;
  465. syncExtractDataMode();
  466. extractSegmentValue.value = state.extractSegmentValue;
  467. extractSegmentRow.value = state.extractSegmentRow;
  468. absoluteValueChecked.value = state.absoluteValueChecked;
  469. decimalEnabled.value = state.decimalEnabled;
  470. decimalSourceBase.value = state.decimalSourceBase;
  471. decimalTargetBase.value = state.decimalTargetBase;
  472. replaceEnabled.value = state.replaceEnabled;
  473. replaceMode.value = state.replaceMode;
  474. replaceSource.value = state.replaceSource;
  475. replaceTarget.value = state.replaceTarget;
  476. removeSpaceChecked.value = state.removeSpaceChecked;
  477. removeTabChecked.value = state.removeTabChecked;
  478. removeCustomChecked.value = state.removeCustomChecked;
  479. removeCustomText.value = state.removeCustomText;
  480. groupingEnabled.value = state.groupingEnabled;
  481. groupingColumnsDraft.value = normalizeGroupingColumnsDraft(cloneDeepSafe(state.groupingColumnsDraft));
  482. magnificationValue.value = state.magnificationValue;
  483. addedColumnConfig.value = cloneDeepSafe(state.addedColumnConfig);
  484. splitColumnConfig.value = cloneDeepSafe(state.splitColumnConfig);
  485. generatedColumnHeaderRowMap.value = buildGeneratedColumnHeaderRowMap(
  486. state.addedColumns,
  487. state.sourceTableRows,
  488. state.generatedColumnHeaderRowMap,
  489. state.headerRowIndex,
  490. );
  491. recognitionDirectionFields.value = cloneDeepSafe(state.recognitionDirectionFields || []);
  492. recognitionBasicInfoBaseFuncRegx.value = state.recognitionBasicInfoBaseFuncRegx;
  493. recognitionBasicInfoManualVisible.value = state.recognitionBasicInfoManualVisible;
  494. recognitionBasicInfoExpanded.value = state.recognitionBasicInfoExpanded;
  495. recognitionBasicInfoRows.value = cloneDeepSafe(state.recognitionBasicInfoRows);
  496. recognitionAmountZfEnabled.value = !!state.recognitionAmountZfEnabled;
  497. childTemplateName.value = state.childTemplateName;
  498. if (!selectedRules.value.some((item) => item.active) && currentColumn.value !== null) {
  499. const currentColumnRules = selectRules.value[currentColumn.value] || [];
  500. if (!currentColumnRules.some((item) => item.active)) {
  501. currentRuleTitle.value = '';
  502. }
  503. }
  504. runRulePipeline();
  505. };
  506. const resetNodeWorkState = () => {
  507. selectedTemplateId.value = 0;
  508. templateDetail.value = null;
  509. templateDetailFromMock.value = false;
  510. activeTemplateItem.value = null;
  511. fieldMapping.value = {};
  512. activeMappingTargetKey.value = '';
  513. rightTableHeaders.value = [];
  514. addedColumns.value = [];
  515. selectedAddedColumn.value = '';
  516. sourceTableRows.value = [];
  517. selectRules.value = [];
  518. selectedRules.value = [];
  519. activeRules.value = 0;
  520. currentColumn.value = null;
  521. selectedRowIndex.value = null;
  522. headerRowIndex.value = null;
  523. headerRowInput.value = '';
  524. currentRuleTitle.value = '';
  525. omitLineInput.value = '';
  526. dateConfigType.value = 1;
  527. extractDataMode.value = 'before';
  528. extractDataBefore.value = '';
  529. extractDataAfter.value = '';
  530. extractSegmentValue.value = '';
  531. extractSegmentRow.value = 1;
  532. absoluteValueChecked.value = false;
  533. decimalEnabled.value = false;
  534. decimalSourceBase.value = '10进制';
  535. decimalTargetBase.value = '16进制';
  536. replaceEnabled.value = true;
  537. replaceMode.value = 0;
  538. replaceSource.value = '';
  539. replaceTarget.value = '';
  540. removeSpaceChecked.value = false;
  541. removeTabChecked.value = false;
  542. removeCustomChecked.value = false;
  543. removeCustomText.value = '';
  544. groupingEnabled.value = false;
  545. groupingColumnsDraft.value = normalizeGroupingColumnsDraft();
  546. magnificationValue.value = 1;
  547. addedColumnConfig.value = {};
  548. splitColumnConfig.value = {};
  549. generatedColumnHeaderRowMap.value = {};
  550. recognitionDirectionFields.value = [];
  551. recognitionBasicInfoBaseFuncRegx.value = '';
  552. recognitionBasicInfoManualVisible.value = false;
  553. recognitionBasicInfoExpanded.value = false;
  554. recognitionBasicInfoRows.value = [];
  555. recognitionAmountZfEnabled.value = false;
  556. childTemplateName.value = '';
  557. runRulePipeline();
  558. };
  559. const switchNodeState = async (nextKey: string) => {
  560. if (!nextKey) return;
  561. if (activeNodeKey.value === nextKey) return;
  562. saveCurrentNodeState();
  563. setNodeAnalyzing(nextKey, true);
  564. activeNodeKey.value = nextKey;
  565. const currentNode = findTreeNodeByKey(treeData.value, nextKey)?.raw || null;
  566. structureSyncSuspended.value = true;
  567. try {
  568. const cached = nodeStateCache.get(nextKey);
  569. if (cached) {
  570. applyNodeCacheState(cached);
  571. await loadTemplateDetail(cached.selectedTemplateId, { silent: true, requestDetail: false });
  572. touchNodeCache(nextKey, cached);
  573. return;
  574. }
  575. resetNodeWorkState();
  576. const shouldLoadNodeTemplate = !isTemplateMatchedFailStatus(currentNode?.fileStatus);
  577. await loadTemplateDetail(shouldLoadNodeTemplate ? currentNode?.templateId : undefined, {
  578. silent: true,
  579. requestDetail: false,
  580. });
  581. await applyNodePreset(currentNode);
  582. const initialState = buildNodeCacheState();
  583. touchNodeCache(nextKey, initialState);
  584. if (!initialNodeStatusSignatureMap.has(nextKey)) {
  585. initialNodeStatusSignatureMap.set(nextKey, buildNodeStatusSignature(initialState));
  586. }
  587. } finally {
  588. structureSyncSuspended.value = false;
  589. syncActiveNodeStructure();
  590. setNodeAnalyzing(nextKey, false);
  591. }
  592. };
  593. const structureSyncSuspended = ref(false);
  594. const persistFileListToSessionStorage = () => {
  595. try {
  596. sessionStorage.setItem(FILE_INFO_STORAGE_KEY, JSON.stringify(sanitizeFileInfoTree(fileList.value)));
  597. } catch (error) {
  598. // 忽略序列化异常,避免影响当前清洗编辑流程。
  599. }
  600. };
  601. const applyPreviewDataset = (dataset: PreviewFileDataset) => {
  602. rightTableHeaders.value = [...dataset.headers];
  603. addedColumns.value = [];
  604. generatedColumnHeaderRowMap.value = {};
  605. selectedAddedColumn.value = '';
  606. sourceTableRows.value = dataset.rows.length ? dataset.rows : [];
  607. tablePreviewRows.value = cloneDeepSafe(sourceTableRows.value);
  608. return true;
  609. };
  610. const loadPreviewDatasetFromFile = async (item?: TreeFileInfo | null): Promise<PreviewFileDataset | null> => {
  611. // 从 DuckDB 动态表 raw_{sheetId} 查询预览数据(sheet 节点 id 即 sheetId),
  612. // 替代原 Electron 端本地文件读取
  613. if (!isTreeSheetNode(item) || item?.id === null || item?.id === undefined) return null;
  614. try {
  615. const payload = (await rawPreview({
  616. sheetId: item.id,
  617. headers: normalizeTemplateHeaders(item?.header),
  618. headerRowIndex: normalizeStoredLineNoToRowIndex(item?.lineNo),
  619. limit: 2000,
  620. })) as PreviewFileDataset | null;
  621. if (!payload?.headers?.length) return null;
  622. return {
  623. headers: payload.headers.map((header) => normalizeRuleText(header)).filter(Boolean),
  624. rows: Array.isArray(payload.rows) ? payload.rows : [],
  625. };
  626. } catch (error) {
  627. console.error('读取预览数据失败', item?.id, error);
  628. return null;
  629. }
  630. };
  631. const applyPresetHeadersFromNode = (item?: TreeFileInfo | null) => {
  632. const headers = normalizeTemplateHeaders(item?.header);
  633. if (!headers.length) return false;
  634. return applyPreviewDataset({
  635. headers,
  636. rows: [],
  637. });
  638. };
  639. const insertHeaderAt = (header: string, targetIndex?: number | null) => {
  640. if (!header) return;
  641. if (!rightTableHeaders.value.includes(header)) {
  642. const maxIndex = rightTableHeaders.value.length;
  643. const nextIndex =
  644. typeof targetIndex === 'number' && Number.isInteger(targetIndex)
  645. ? Math.min(Math.max(targetIndex, 0), maxIndex)
  646. : maxIndex;
  647. rightTableHeaders.value.splice(nextIndex, 0, header);
  648. }
  649. if (!addedColumns.value.includes(header)) {
  650. addedColumns.value.push(header);
  651. }
  652. if (!Object.prototype.hasOwnProperty.call(generatedColumnHeaderRowMap.value, header)) {
  653. generatedColumnHeaderRowMap.value[header] = headerRowIndex.value;
  654. }
  655. sourceTableRows.value = sourceTableRows.value.map((row) => ({
  656. ...row,
  657. [header]: String(row[header] ?? ''),
  658. }));
  659. };
  660. const buildColumnRulePayload = (header: string): TreeStoredColumnRule | null => {
  661. const splitConfig = splitColumnConfig.value[header];
  662. if (splitConfig) {
  663. const splitNameIndex = rightTableHeaders.value.findIndex((item) => item === splitConfig.targetName1);
  664. const splitNameIndex2 = rightTableHeaders.value.findIndex((item) => item === splitConfig.targetName2);
  665. const splitMode = normalizeSplitColumnMode(splitConfig.delimiterType);
  666. const splitValue = normalizeSplitColumnValue(splitConfig.delimiterType, splitConfig.delimiterValue);
  667. const splitLength = resolveSplitColumnLength(splitConfig.delimiterType, splitConfig.delimiterValue);
  668. const splitRule = new RuleSplit();
  669. splitRule.refIndex = rightTableHeaders.value.findIndex((item) => item === splitConfig.source);
  670. splitRule.mappingEnum = splitMode === 'length' ? MappingEnum.SP_LEN : MappingEnum.SP_CHAR;
  671. splitRule.splitNameIndex = splitNameIndex >= 0 ? splitNameIndex : undefined;
  672. splitRule.splitNameIndex2 = splitNameIndex2 >= 0 ? splitNameIndex2 : undefined;
  673. splitRule.len = splitMode === 'length' && splitLength > 0 ? splitLength : undefined;
  674. splitRule.splitChar = splitMode === 'special' && splitValue ? splitValue : undefined;
  675. return buildStoredColumnRulePayload(splitRule);
  676. }
  677. const addedConfig = addedColumnConfig.value[header];
  678. if (!addedConfig) return null;
  679. const mappingRule = new RuleMapping();
  680. mappingRule.refIndex = addedConfig.contentFields
  681. .map((field) => rightTableHeaders.value.findIndex((item) => item === field))
  682. .filter((index) => index >= 0);
  683. mappingRule.mappingEnum =
  684. addedConfig.mode === 'custom'
  685. ? addedConfig.customContent
  686. ? MappingEnum.NEW_DATA
  687. : MappingEnum.BLANK
  688. : mappingRule.refIndex.length
  689. ? MappingEnum.REF
  690. : MappingEnum.BLANK;
  691. mappingRule.content = addedConfig.mode === 'custom' ? addedConfig.customContent || '' : '';
  692. return buildStoredColumnRulePayload(mappingRule);
  693. };
  694. const restoreStoredColumnRules = (list?: TreeTableRuleInfo[] | null) => {
  695. const headerSnapshot = [...rightTableHeaders.value];
  696. const normalizedRules = normalizeTableRuleList(list);
  697. const splitRuleGroups = normalizedRules.reduce((acc, rule) => {
  698. const header = normalizeRuleText(rule.fileColName);
  699. const columnRule = rule.columnRule;
  700. const className = normalizeRuleText(columnRule?.['@class']);
  701. const mode = normalizeRuleText(columnRule?.mode);
  702. if (!header || (!className.endsWith('RuleSplit') && mode !== 'split')) {
  703. return acc;
  704. }
  705. const fileColIndex = Number(rule.fileColIndex);
  706. const identity = getSplitColumnRuleIdentity(columnRule);
  707. const prev = acc.get(identity) || [];
  708. prev.push({
  709. header,
  710. fileColIndex: Number.isInteger(fileColIndex) && fileColIndex >= 0 ? fileColIndex : Number.MAX_SAFE_INTEGER,
  711. });
  712. acc.set(identity, prev);
  713. return acc;
  714. }, new Map<string, Array<{ header: string; fileColIndex: number }>>());
  715. normalizedRules.forEach((rule) => {
  716. const header = normalizeRuleText(rule.fileColName);
  717. if (!header) return;
  718. const columnRule = rule.columnRule;
  719. const fileColIndex = Number(rule.fileColIndex);
  720. const targetIndex = Number.isInteger(fileColIndex) && fileColIndex >= 0 ? fileColIndex : undefined;
  721. if (columnRule) {
  722. insertHeaderAt(header, targetIndex);
  723. }
  724. const className = normalizeRuleText(columnRule?.['@class']);
  725. const mode = normalizeRuleText(columnRule?.mode);
  726. if (className.endsWith('RuleSplit') || mode === 'split') {
  727. const refIndex = Number(columnRule?.refIndex);
  728. const source =
  729. Number.isInteger(refIndex) && refIndex >= 0
  730. ? headerSnapshot[refIndex] || rightTableHeaders.value[refIndex] || ''
  731. : '';
  732. if (!source) return;
  733. const splitChar = String(columnRule?.splitChar ?? '');
  734. const splitLength = Number(columnRule?.len);
  735. const splitMappingEnum = Number(columnRule?.mappingEnum);
  736. const isLengthSplit =
  737. splitMappingEnum === MappingEnum.SP_LEN ||
  738. (!Number.isInteger(splitMappingEnum) && Number.isInteger(splitLength) && splitLength > 0);
  739. const splitNameIndex = Number(columnRule?.splitNameIndex);
  740. const splitNameIndex2 = Number(columnRule?.splitNameIndex2);
  741. const groupedTargetItems = (splitRuleGroups.get(getSplitColumnRuleIdentity(columnRule)) || [])
  742. .slice()
  743. .sort((a, b) => a.fileColIndex - b.fileColIndex);
  744. const groupedTargets = groupedTargetItems.map((item) => item.header).filter(Boolean);
  745. const uniqueTargets = Array.from(new Set(groupedTargets));
  746. const resolveGroupedTargetByIndex = (index: number) =>
  747. Number.isInteger(index) && index >= 0
  748. ? groupedTargetItems.find((item) => item.fileColIndex === index)?.header || ''
  749. : '';
  750. let targetName1 = resolveGroupedTargetByIndex(splitNameIndex);
  751. let targetName2 = resolveGroupedTargetByIndex(splitNameIndex2);
  752. if (!targetName1 && !targetName2) {
  753. targetName1 = uniqueTargets[0] || header;
  754. targetName2 = uniqueTargets.find((item) => item !== targetName1) || '';
  755. }
  756. if (targetName1 && targetName2 && targetName1 === targetName2) {
  757. if (Number.isInteger(fileColIndex) && fileColIndex === splitNameIndex2) {
  758. targetName1 = '';
  759. } else {
  760. targetName2 = '';
  761. }
  762. }
  763. splitColumnConfig.value[header] = {
  764. source,
  765. delimiterType: isLengthSplit ? 'length' : 'special',
  766. delimiterValue: isLengthSplit ? String(splitLength > 0 ? splitLength : '') : splitChar,
  767. targetName1,
  768. targetName2,
  769. };
  770. return;
  771. }
  772. if (className.endsWith('RuleMapping') || mode === 'ref' || mode === 'custom') {
  773. const refIndexList = Array.isArray(columnRule?.refIndex)
  774. ? columnRule.refIndex.map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0)
  775. : [];
  776. const mappingEnum = Number(columnRule?.mappingEnum);
  777. const isRefMapping = mappingEnum === MappingEnum.REF || (!Number.isFinite(mappingEnum) && mode === 'ref');
  778. addedColumnConfig.value[header] = {
  779. mode: isRefMapping ? 'ref' : 'custom',
  780. contentFields: refIndexList
  781. .map((index) => headerSnapshot[index] || rightTableHeaders.value[index])
  782. .filter(Boolean),
  783. customContent: String(columnRule?.content ?? ''),
  784. };
  785. }
  786. });
  787. };
  788. const restoreStoredFunRules = (list?: TreeTableRuleInfo[] | null) => {
  789. selectedRules.value = [];
  790. selectRules.value = rightTableHeaders.value.map(() => []);
  791. const sharedRuleKeySet = new Set<string>();
  792. const mergeColumnRule = (
  793. columnRules: SelectedRuleItem[],
  794. nextRule: SelectedRuleItem,
  795. rawRule: TreeStoredFunRule,
  796. ) => {
  797. if (nextRule.key !== 'extractData') {
  798. columnRules.push(nextRule);
  799. return;
  800. }
  801. const lastRule = columnRules[columnRules.length - 1];
  802. if (!lastRule || lastRule.key !== 'extractData') {
  803. columnRules.push(nextRule);
  804. return;
  805. }
  806. const direction = normalizeRuleText(rawRule?.metaDirection) || (rawRule?.opened === false ? 'after' : 'before');
  807. if (direction === 'after') {
  808. lastRule.rules.nextModel = nextRule.rules.nextModel;
  809. } else {
  810. lastRule.rules.beforeModel = nextRule.rules.beforeModel;
  811. }
  812. };
  813. normalizeTableRuleList(list).forEach((tableRule) => {
  814. const header = normalizeRuleText(tableRule.fileColName);
  815. const columnIndex = header ? rightTableHeaders.value.findIndex((item) => item === header) : -1;
  816. const columnRules: SelectedRuleItem[] = [];
  817. (tableRule.funList || [])
  818. .slice()
  819. .sort((a, b) => Number(a?.sort ?? 0) - Number(b?.sort ?? 0))
  820. .forEach((rule) => {
  821. const nextRule = deserializeStoredRule(rule);
  822. if (!nextRule) return;
  823. if (nextRule.key === 'ignoreLine') {
  824. if (!sharedRuleKeySet.has(nextRule.key)) {
  825. selectedRules.value.push(nextRule);
  826. sharedRuleKeySet.add(nextRule.key);
  827. }
  828. return;
  829. }
  830. mergeColumnRule(columnRules, nextRule, rule);
  831. });
  832. if (columnIndex >= 0 && columnRules.length) {
  833. selectRules.value[columnIndex] = sanitizeSelectedRuleList(columnRules);
  834. }
  835. });
  836. selectedRules.value = sanitizeSelectedRuleList(selectedRules.value);
  837. };
  838. const restoreFileLevelRules = (item?: TreeFileInfo | null) => {
  839. const ignoreLines = Array.isArray(item?.ignoreLines)
  840. ? item.ignoreLines.map((line) => Number(line)).filter((line) => Number.isInteger(line) && line > 0)
  841. : [];
  842. if (ignoreLines.length) {
  843. const existingIgnoreRule = selectedRules.value.find((rule) => rule.key === 'ignoreLine');
  844. const nextIgnoreRules = {
  845. ...defaultRuleConfig(),
  846. ...(existingIgnoreRule?.rules || {}),
  847. omitLine: ignoreLines.join(','),
  848. };
  849. if (existingIgnoreRule) {
  850. existingIgnoreRule.rules = nextIgnoreRules;
  851. existingIgnoreRule.enabled = true;
  852. } else {
  853. selectedRules.value = [
  854. ...selectedRules.value,
  855. {
  856. name: ruleNameMap.ignoreLine,
  857. key: 'ignoreLine',
  858. active: selectedRules.value.length === 0,
  859. enabled: true,
  860. rules: nextIgnoreRules,
  861. },
  862. ];
  863. }
  864. }
  865. };
  866. const buildTableRuleListFromState = (list?: TreeTableRuleInfo[] | null) => {
  867. const previousRules = normalizeTableRuleList(list);
  868. const currentTemplateTableId = normalizeTemplateId(
  869. templateDetail.value?.id ??
  870. activeTemplateItem.value?.id ??
  871. selectedTemplateId.value ??
  872. activeNodeInfo.value?.templateId,
  873. );
  874. const previousRuleMap = previousRules.reduce(
  875. (acc, item, index) => {
  876. const key = getTableRuleTargetKey(item, index);
  877. if (key) acc[key] = item;
  878. return acc;
  879. },
  880. {} as Record<string, TreeTableRuleInfo>,
  881. );
  882. const sharedFunRules = serializeStoredFunRules(selectedRules.value);
  883. const directionConfMap = recognitionDirectionFields.value.reduce(
  884. (acc, item) => {
  885. acc[item.key] = buildRecognitionDirectionConf(item.entries);
  886. return acc;
  887. },
  888. {} as Record<string, string>,
  889. );
  890. return fixedTargetFields.value.map((field, index) => {
  891. const previous = previousRuleMap[field.key];
  892. const requiredMarker = getTableRuleRequiredFlag(previous, field.required) || field.required;
  893. const mappedHeader = normalizeRuleText(effectiveFieldMapping.value[field.key]);
  894. const matchedByRecognitionRule = isTargetHandledByRecognitionRule(field.key);
  895. const fileColIndex = mappedHeader ? rightTableHeaders.value.findIndex((item) => item === mappedHeader) : -1;
  896. const columnFunRules =
  897. fileColIndex >= 0 ? serializeStoredFunRules(selectRules.value[fileColIndex] || [], sharedFunRules.length) : [];
  898. const templateField = resolveTemplateFieldMeta(field, index);
  899. const templateFieldRecord = (templateField || {}) as Record<string, any>;
  900. // 交易金额字段以「正负识别」开关为准(开关本身已从节点/模板的 zf 初始化);
  901. // 其余字段沿用模板字段 / 上一条规则的 zf 并兜底 0,保证每条规则都带整数 zf
  902. // ——后端 TableRuleDTO.zf 是 Integer,清洗器 `rule.getZf() == 1` 会自动拆箱,缺字段必 NPE。
  903. const zf = isTransAmountTargetField(field)
  904. ? recognitionAmountZfEnabled.value
  905. ? 1
  906. : 0
  907. : resolveStoredZfFlag(templateFieldRecord, previous);
  908. const requiredFlagValue = pickFirstDefinedRuleFlagValue(
  909. getRawRuleFlagValue(templateFieldRecord, 'required'),
  910. getRawRuleFlagValue(previous, 'required'),
  911. requiredMarker ? 1 : 0,
  912. );
  913. const matchedFlagValue =
  914. matchedByRecognitionRule || mappedHeader
  915. ? 1
  916. : pickFirstDefinedRuleFlagValue(
  917. getRawRuleFlagValue(templateFieldRecord, 'matched'),
  918. getRawRuleFlagValue(previous, 'matched'),
  919. requiredMarker ? 1 : 0,
  920. );
  921. const matchedRequiredFlagValue = pickFirstDefinedRuleFlagValue(getRawRuleFlagValue(previous, 'matchedRequired'));
  922. const directionConf =
  923. directionConfMap[field.key] ||
  924. normalizeRecognitionConfigText(previous?.directionConf) ||
  925. normalizeRecognitionConfigText(templateFieldRecord.directionConf ?? templateFieldRecord.direction_conf);
  926. const fileRuleList = [...(index === 0 ? sharedFunRules : []), ...columnFunRules];
  927. return {
  928. id: previous?.id ?? null,
  929. tableId: currentTemplateTableId || normalizeTemplateId(previous?.tableId) || null,
  930. fieldNameCn: field.label,
  931. fieldNameEn: field.key,
  932. fieldType: normalizeRuleText(templateField?.fieldType) || normalizeRuleText(previous?.fieldType) || 'string',
  933. required: requiredFlagValue,
  934. fieldLen: previous?.fieldLen ?? null,
  935. matched: matchedFlagValue,
  936. matchedRequired: matchedRequiredFlagValue,
  937. directionConf: directionConf || null,
  938. zf,
  939. fieldSort: index + 1,
  940. fileColIndex: fileColIndex >= 0 ? fileColIndex : null,
  941. fileColName: mappedHeader || null,
  942. funList: fileRuleList,
  943. columnRule: mappedHeader ? buildColumnRulePayload(mappedHeader) : null,
  944. } as TreeTableRuleInfo;
  945. });
  946. };
  947. const syncNodeStructure = (node?: TreeFileInfo | null) => {
  948. if (!isTreeSheetNode(node)) return;
  949. const templateId = normalizeTemplateId(
  950. templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value ?? node.templateId,
  951. );
  952. const mainId = getSelectedTemplateMainId(node.mainId);
  953. const tableName =
  954. normalizeRuleText(templateDetail.value?.tableNameCn) ||
  955. normalizeRuleText(activeTemplateItem.value?.tableNameCn) ||
  956. normalizeRuleText(node.tableName);
  957. const baseHeaders = rightTableHeaders.value.filter((header) => !addedColumns.value.includes(header));
  958. const existingHeaders = normalizeTemplateHeaders(node.header);
  959. const hasRealHeaderConfig =
  960. headerRowIndex.value !== null ||
  961. !!existingHeaders.length ||
  962. addedColumns.value.length > 0 ||
  963. Object.values(effectiveFieldMapping.value).some(Boolean) ||
  964. baseHeaders.length > 0;
  965. const shouldPersistRecognitionBasicInfo = hasRecognitionBasicInfoFilledContent(recognitionBasicInfoRows.value);
  966. const ignoreLines = Array.from(ignoredRowsSet.value).sort((a, b) => a - b);
  967. const groupColumns = buildEnabledGroupingColumnsPayload();
  968. node.templateId = templateId || node.templateId || null;
  969. node.mainId = mainId || null;
  970. node.tableName = tableName || node.tableName || null;
  971. if (headerRowIndex.value !== null) {
  972. node.lineNo = buildStoredLineNo(headerRowIndex.value);
  973. }
  974. if (hasRealHeaderConfig) {
  975. node.header = baseHeaders.length ? [...baseHeaders] : [];
  976. }
  977. node.ignoreLines = ignoreLines;
  978. node.groupColumns = groupColumns;
  979. node.tableRuleList = buildTableRuleListFromState(node.tableRuleList);
  980. node.childTemplateName = normalizeRuleText(childTemplateName.value) || null;
  981. node.hasNew = !!node.childTemplateName;
  982. if (shouldPersistRecognitionBasicInfo) {
  983. node.funcRegx = buildRecognitionBasicInfoFuncRegx();
  984. } else if (Object.prototype.hasOwnProperty.call(node, 'funcRegx')) {
  985. node.funcRegx = null;
  986. }
  987. persistFileListToSessionStorage();
  988. };
  989. const syncActiveNodeStructure = () => {
  990. if (structureSyncSuspended.value) return;
  991. syncNodeStructure(activeNodeInfo.value);
  992. };
  993. const hasHeaderFormatError = (header: string) => !!headerFormatValidationMap.value[header]?.invalidTargets.length;
  994. const getHeaderFormatErrorTitle = (header: string) => headerFormatValidationMap.value[header]?.title || '';
  995. const getMatchedTargetFieldText = (header: string) =>
  996. matchedTargetFieldsByHeader.value[header]?.map((item) => item.label).join('、') || '';
  997. const hasMatchedTargetField = (header: string) => !!matchedTargetFieldsByHeader.value[header]?.length;
  998. const getMatchedHeaderStatusClass = (header: string) => (hasMatchedTargetField(header) ? 'is-success' : 'is-fail');
  999. const getMatchedHeaderStatusText = (header: string) => (hasMatchedTargetField(header) ? '✓' : '✕');
  1000. const getMatchedHeaderStatusTitle = (header: string) => (hasMatchedTargetField(header) ? '已匹配' : '未匹配');
  1001. const remapGroupingColumnRules = (
  1002. list: Array<Partial<GroupingColumnDraft> | Record<string, any>> | undefined,
  1003. keyMap: Record<string, string>,
  1004. removedHeaders: Set<string> = new Set(),
  1005. ) =>
  1006. normalizeGroupingColumnsDraft(list).map((item) => {
  1007. const nextHeader = removedHeaders.has(item.header) ? '' : keyMap[item.header] || item.header;
  1008. return createGroupingColumnDraft({
  1009. ...item,
  1010. header: nextHeader,
  1011. title: nextHeader || '',
  1012. });
  1013. });
  1014. const remapSelectRuleHeaderRefs = (
  1015. groups: SelectedRuleItem[][],
  1016. keyMap: Record<string, string>,
  1017. removedHeaders: Set<string> = new Set(),
  1018. ) =>
  1019. groups.map((rules) =>
  1020. rules.map((rule) => {
  1021. if (rule.key !== 'groupingColumns') {
  1022. return rule;
  1023. }
  1024. return {
  1025. ...rule,
  1026. rules: {
  1027. ...rule.rules,
  1028. groupColumns: remapGroupingColumnRules(rule.rules?.groupColumns, keyMap, removedHeaders),
  1029. },
  1030. };
  1031. }),
  1032. );
  1033. const syncHeaderRowInput = () => {
  1034. headerRowInput.value = headerRowIndex.value === null ? '' : String(headerRowIndex.value + 1);
  1035. };
  1036. const applyHeaderRow = (rowIndex: number, options: { silent?: boolean; autoMatch?: boolean } = {}) => {
  1037. const { silent = false, autoMatch = true } = options;
  1038. if (rowIndex < 0 || rowIndex >= sourceTableRows.value.length) {
  1039. syncHeaderRowInput();
  1040. if (!silent) {
  1041. createMessage.warning('请输入正确的表头行号');
  1042. }
  1043. return;
  1044. }
  1045. const previousHeaders = [...rightTableHeaders.value];
  1046. const headerSourceRow = sourceTableRows.value[rowIndex] || ({} as Record<string, string>);
  1047. const nextHeaders = buildHeaderLabelsByRow(previousHeaders, headerSourceRow, addedColumns.value);
  1048. if (
  1049. headerRowIndex.value === rowIndex &&
  1050. nextHeaders.length === previousHeaders.length &&
  1051. nextHeaders.every((header, index) => header === previousHeaders[index])
  1052. ) {
  1053. selectedRowIndex.value = rowIndex;
  1054. syncHeaderRowInput();
  1055. return;
  1056. }
  1057. const headerKeyMap = previousHeaders.reduce(
  1058. (acc, header, index) => {
  1059. acc[header] = nextHeaders[index];
  1060. return acc;
  1061. },
  1062. {} as Record<string, string>,
  1063. );
  1064. const fixedHeaderSet = new Set(addedColumns.value);
  1065. sourceTableRows.value = sourceTableRows.value.map((row, index) => {
  1066. const nextRow: Record<string, string> = {};
  1067. previousHeaders.forEach((header, colIndex) => {
  1068. const nextHeader = nextHeaders[colIndex];
  1069. if (fixedHeaderSet.has(header)) {
  1070. nextRow[nextHeader] = String(row[header] ?? '');
  1071. return;
  1072. }
  1073. nextRow[nextHeader] = index === rowIndex ? nextHeader : String(row[header] ?? '');
  1074. });
  1075. return nextRow;
  1076. });
  1077. rightTableHeaders.value = nextHeaders;
  1078. generatedColumnHeaderRowMap.value = addedColumns.value.reduce(
  1079. (acc, item) => {
  1080. const nextKey = headerKeyMap[item] || item;
  1081. acc[nextKey] = getGeneratedColumnHeaderRowIndex(item);
  1082. return acc;
  1083. },
  1084. {} as Record<string, number | null>,
  1085. );
  1086. addedColumns.value = addedColumns.value.map((item) => headerKeyMap[item] || item);
  1087. selectedAddedColumn.value = selectedAddedColumn.value ? headerKeyMap[selectedAddedColumn.value] || '' : '';
  1088. fieldMapping.value = Object.entries(fieldMapping.value).reduce(
  1089. (acc, [key, value]) => {
  1090. acc[key] = headerKeyMap[value] || value;
  1091. return acc;
  1092. },
  1093. {} as Record<string, string>,
  1094. );
  1095. addedColumnConfig.value = Object.entries(renameRecordKeys(addedColumnConfig.value, headerKeyMap)).reduce(
  1096. (acc, [key, value]) => {
  1097. acc[key] = {
  1098. ...value,
  1099. contentFields: value.contentFields.map((field) => headerKeyMap[field] || field),
  1100. };
  1101. return acc;
  1102. },
  1103. {} as Record<string, { mode: 'ref' | 'custom'; contentFields: string[]; customContent: string }>,
  1104. );
  1105. splitColumnConfig.value = Object.entries(renameRecordKeys(splitColumnConfig.value, headerKeyMap)).reduce(
  1106. (acc, [key, value]) => {
  1107. acc[key] = {
  1108. ...value,
  1109. source: headerKeyMap[value.source] || value.source,
  1110. targetName1: headerKeyMap[value.targetName1] || value.targetName1,
  1111. targetName2: headerKeyMap[value.targetName2] || value.targetName2,
  1112. };
  1113. return acc;
  1114. },
  1115. {} as Record<
  1116. string,
  1117. { source: string; delimiterType: string; delimiterValue: string; targetName1: string; targetName2: string }
  1118. >,
  1119. );
  1120. selectRules.value = remapSelectRuleHeaderRefs(selectRules.value, headerKeyMap);
  1121. groupingColumnsDraft.value = remapGroupingColumnRules(groupingColumnsDraft.value, headerKeyMap);
  1122. if (splitColumnForm.value.source) {
  1123. splitColumnForm.value.source = headerKeyMap[splitColumnForm.value.source] || splitColumnForm.value.source;
  1124. }
  1125. headerRowIndex.value = rowIndex;
  1126. selectedRowIndex.value = rowIndex;
  1127. syncHeaderRowInput();
  1128. rebuildGeneratedColumnsInSourceRows();
  1129. runRulePipeline();
  1130. if (autoMatch) {
  1131. autoMatchFields(false);
  1132. }
  1133. if (!silent) {
  1134. createMessage.success(`已将第${rowIndex + 1}行设为表头行`);
  1135. }
  1136. };
  1137. const applyHeaderRowInput = () => {
  1138. const rawValue = headerRowInput.value.trim();
  1139. if (!rawValue) {
  1140. syncHeaderRowInput();
  1141. return;
  1142. }
  1143. const rowNumber = Number(rawValue);
  1144. if (!Number.isInteger(rowNumber)) {
  1145. syncHeaderRowInput();
  1146. createMessage.warning('请输入正确的表头行号');
  1147. return;
  1148. }
  1149. applyHeaderRow(rowNumber - 1);
  1150. };
  1151. const ensureCurrentHeaderRowConfigured = (warningText = '请先填写表头所在行,再进行模板匹配') => {
  1152. if (headerRowIndex.value !== null) return true;
  1153. const rawValue = headerRowInput.value.trim();
  1154. if (!rawValue) {
  1155. createMessage.warning(warningText);
  1156. return false;
  1157. }
  1158. const rowNumber = Number(rawValue);
  1159. if (!Number.isInteger(rowNumber) || rowNumber <= 0 || rowNumber > sourceTableRows.value.length) {
  1160. createMessage.warning('请输入正确的表头行号');
  1161. return false;
  1162. }
  1163. applyHeaderRow(rowNumber - 1, { silent: true, autoMatch: false });
  1164. return headerRowIndex.value !== null;
  1165. };
  1166. const mockGridTemplate = computed(() =>
  1167. [`${mockIndexColumnWidth}px`, ...rightTableHeaders.value.map(() => `${mockDataColumnWidth}px`)].join(' '),
  1168. );
  1169. const getColumnRuleBadgeState = (colIndex: number) => {
  1170. if (colIndex < 0) return false;
  1171. const rules = sanitizeSelectedRuleList(selectRules.value[colIndex] || []);
  1172. const hasEnabledRules = rules.some((rule) => rule && rule.key && rule.enabled);
  1173. if (hasEnabledRules) return 'active';
  1174. return rules.some((rule) => rule && rule.key) ? 'draft' : '';
  1175. };
  1176. const getColumnRuleBadgeTitle = (colIndex: number) => {
  1177. const state = getColumnRuleBadgeState(colIndex);
  1178. if (state === 'active') return '该列已启用清洗流程';
  1179. if (state === 'draft') return '该列已添加清洗规则,待启用';
  1180. return '';
  1181. };
  1182. const addColumnDialogOpen = ref(false);
  1183. const splitColumnDialogOpen = ref(false);
  1184. const deleteColumnDialogOpen = ref(false);
  1185. const addColumnMode = ref<'add' | 'edit'>('add');
  1186. const addColumnForm = ref<{
  1187. name: string;
  1188. mode: 'ref' | 'custom';
  1189. contentFields: string[];
  1190. customContent: string;
  1191. }>({
  1192. name: '',
  1193. mode: 'ref',
  1194. contentFields: [],
  1195. customContent: '',
  1196. });
  1197. const selectedSourceField = ref('');
  1198. const selectedContentIndex = ref(-1);
  1199. const draggingContentIndex = ref<number | null>(null);
  1200. const dragOverContentIndex = ref<number | null>(null);
  1201. const availableSourceHeaders = computed(() =>
  1202. rightTableHeaders.value.filter((item) => item && !addedColumns.value.includes(item)),
  1203. );
  1204. const hasDuplicateHeaderName = (name: string, excludedRawNames: string[] = []) => {
  1205. const normalizedName = normalizeColumnNameForCompare(name);
  1206. if (!normalizedName) return false;
  1207. return rightTableHeaders.value.some(
  1208. (header) => !excludedRawNames.includes(header) && normalizeColumnNameForCompare(header) === normalizedName,
  1209. );
  1210. };
  1211. const splitColumnForm = ref({
  1212. source: '',
  1213. targetName1: '',
  1214. targetName2: '',
  1215. delimiterType: 'length',
  1216. delimiterValue: '',
  1217. });
  1218. const mappingPopoverOpenKey = ref<string>('');
  1219. const originFilterKeyword = ref('');
  1220. const resolveTargetKeyByTableRule = (rule: TreeTableRuleInfo) => {
  1221. const fieldNameEn = normalizeRuleText(rule.fieldNameEn);
  1222. if (fieldNameEn) {
  1223. const matchedByKey = fixedTargetFields.value.find((item) => item.key === fieldNameEn);
  1224. if (matchedByKey) return matchedByKey.key;
  1225. }
  1226. const fieldNameCn = normalizeRuleText(rule.fieldNameCn);
  1227. if (fieldNameCn) {
  1228. const matchedByLabel =
  1229. fixedTargetFields.value.find((item) => item.label === fieldNameCn) ||
  1230. fixedTargetFields.value.find((item) => item.label.includes(fieldNameCn) || fieldNameCn.includes(item.label));
  1231. if (matchedByLabel) return matchedByLabel.key;
  1232. }
  1233. return '';
  1234. };
  1235. const applyTableRuleMapping = (list?: TreeTableRuleInfo[] | null) => {
  1236. const tableRules = normalizeTableRuleList(list);
  1237. if (!tableRules.length || !fixedTargetFields.value.length) return false;
  1238. const headerSet = new Set(rightTableHeaders.value);
  1239. const nextMapping = tableRules.reduce(
  1240. (acc, item) => {
  1241. const targetKey = resolveTargetKeyByTableRule(item);
  1242. const sourceField = normalizeRuleText(item.fileColName);
  1243. if (!targetKey || !sourceField || !headerSet.has(sourceField)) return acc;
  1244. acc[targetKey] = sourceField;
  1245. return acc;
  1246. },
  1247. {} as Record<string, string>,
  1248. );
  1249. if (!Object.keys(nextMapping).length) return false;
  1250. fieldMapping.value = { ...fieldMapping.value, ...nextMapping };
  1251. return true;
  1252. };
  1253. const isTargetMatchedByRecognitionBasicInfo = (targetKey: string) =>
  1254. recognitionBasicInfoMatchedTargetKeySet.value.has(targetKey);
  1255. const isTargetHandledByRecognitionRule = (targetKey: string) =>
  1256. recognitionRuleHandledTargetKeySet.value.has(targetKey);
  1257. const closeMappingPopoverPreview = () => {
  1258. mappingPopoverOpenKey.value = '';
  1259. originFilterKeyword.value = '';
  1260. clearOriginHoverPreviewState();
  1261. };
  1262. const childTemplateName = ref('');
  1263. const showDuplicateChildTemplateWarning = (names: string[]) => {
  1264. const previewNames = names.slice(0, 3).join('、');
  1265. const suffix = names.length > 3 ? ` 等 ${names.length} 个名称` : '';
  1266. Modal.warning({
  1267. title: '子模板名称重复',
  1268. content: `当前清洗结果中存在重复的子模板名称:${previewNames}${suffix},请修改后再导入数据。`,
  1269. centered: true,
  1270. });
  1271. };
  1272. const collectChildTemplateNames = (nodes: TreeFileInfo[] = []) => {
  1273. const names: string[] = [];
  1274. const walk = (items: TreeFileInfo[] = []) => {
  1275. items.forEach((item) => {
  1276. const childName = normalizeRuleText(item.childTemplateName);
  1277. if (childName) {
  1278. names.push(childName);
  1279. }
  1280. if (Array.isArray(item.children) && item.children.length) {
  1281. walk(item.children);
  1282. }
  1283. });
  1284. };
  1285. walk(nodes);
  1286. return names;
  1287. };
  1288. const findDuplicateChildTemplateNames = (names: string[]) => {
  1289. const duplicateNameMap = new Map<string, string>();
  1290. const counter = new Map<string, number>();
  1291. names.forEach((name) => {
  1292. const key = normalizeTemplateNameKey(name);
  1293. if (!key) return;
  1294. const nextCount = (counter.get(key) || 0) + 1;
  1295. counter.set(key, nextCount);
  1296. if (nextCount > 1 && !duplicateNameMap.has(key)) {
  1297. duplicateNameMap.set(key, name);
  1298. }
  1299. });
  1300. return Array.from(duplicateNameMap.values());
  1301. };
  1302. const ensureChildTemplateNamesAvailable = async (nodes: TreeFileInfo[] = []) => {
  1303. const childTemplateNames = collectChildTemplateNames(nodes);
  1304. if (!childTemplateNames.length) return true;
  1305. const duplicateNames = findDuplicateChildTemplateNames(childTemplateNames);
  1306. if (duplicateNames.length) {
  1307. showDuplicateChildTemplateWarning(duplicateNames);
  1308. return false;
  1309. }
  1310. const uniqueNameMap = new Map<string, string>();
  1311. childTemplateNames.forEach((name) => {
  1312. const key = normalizeTemplateNameKey(name);
  1313. if (key && !uniqueNameMap.has(key)) {
  1314. uniqueNameMap.set(key, name);
  1315. }
  1316. });
  1317. for (const name of uniqueNameMap.values()) {
  1318. const available = await checkTemplateNameAvailable(name);
  1319. if (!available) {
  1320. showChildTemplateExistsWarning(name);
  1321. return false;
  1322. }
  1323. }
  1324. return true;
  1325. };
  1326. const recognitionBasicInfoBaseFuncRegx = ref('');
  1327. const recognitionBasicInfoManualVisible = ref(false);
  1328. // Phase 4:识别规则域下沉到 composable。页面同名解构接住,原有代码与模板零改动。
  1329. const recognitionApi = useRecognitionRules({ absoluteValueChecked, activeNodeKey, activeRules, addedColumnConfig, addedColumns, buildGeneratedColumnHeaderRowMap, childTemplateName, createMessage, currentColumn, currentRuleTitle, dateConfigType, decimalEnabled, decimalSourceBase, decimalTargetBase, extractDataAfter, extractDataBefore, extractSegmentRow, extractSegmentValue, fieldMapping, fixedTargetFields, generatedColumnHeaderRowMap, groupingColumnsDraft, groupingEnabled, headerRowIndex, magnificationValue, mappingPopoverOpenKey, omitLineInput, originFilterKeyword, recognitionAmountZfEnabled, recognitionBasicInfoBaseFuncRegx, recognitionBasicInfoManualVisible, recognitionBasicInfoRows, recognitionDirectionFields, removeCustomChecked, removeCustomText, removeSpaceChecked, removeTabChecked, replaceEnabled, replaceMode, replaceSource, replaceTarget, rightTableHeaders, runRulePipeline, selectRules, selectedAddedColumn, selectedRowIndex, selectedRules, selectedTemplateId, sourceTableRows, splitColumnConfig, syncActiveNodeStructure });
  1330. const { activeRecognitionRule, buildNodeCacheState, headerRowInput, nodeStateCache, recognitionBasicInfoExpanded, recognitionPanelExpanded, recognitionRuleTabs, saveCurrentNodeState, showRecognitionPanel, touchNodeCache } = recognitionApi;
  1331. const requiredTotal = computed(() => fixedTargetFields.value.filter((item) => isMappingTargetRequired(item)).length);
  1332. const requiredMatchedCount = computed(
  1333. () => fixedTargetFields.value.filter((item) => isMappingTargetRequired(item) && isTargetMatched(item.key)).length,
  1334. );
  1335. const matchedFieldCount = computed(() => fixedTargetFields.value.filter((item) => isTargetMatched(item.key)).length);
  1336. const buildRecognitionDirectionConf = (entries?: RecognitionDirectionItem[]) => {
  1337. if (!entries?.length) return '';
  1338. const payload = entries.reduce(
  1339. (acc, item) => {
  1340. const label = normalizeRecognitionConfigText(item.label);
  1341. if (!label) return acc;
  1342. acc[label] = normalizeRecognitionConfigText(item.value);
  1343. return acc;
  1344. },
  1345. {} as Record<string, string>,
  1346. );
  1347. return Object.keys(payload).length ? JSON.stringify(payload) : '';
  1348. };
  1349. const applyRecognitionDirectionState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
  1350. recognitionDirectionFields.value = resolveRecognitionDirectionFields(node, options);
  1351. };
  1352. const resolveRecognitionBasicInfoSource = (
  1353. node?: TreeFileInfo | null,
  1354. options: { preferTemplate?: boolean } = {},
  1355. ) => {
  1356. const templateFuncRegx = normalizeRecognitionConfigText(
  1357. templateDetail.value?.funcRegx || activeTemplateItem.value?.funcRegx,
  1358. );
  1359. const hasNodeFuncRegx = hasOwnRecognitionBasicInfoConfig(node);
  1360. const nodeFuncRegx = normalizeRecognitionConfigText(node?.funcRegx);
  1361. if (options.preferTemplate) {
  1362. return templateFuncRegx;
  1363. }
  1364. if (hasNodeFuncRegx) {
  1365. return nodeFuncRegx;
  1366. }
  1367. return templateFuncRegx;
  1368. };
  1369. const applyRecognitionBasicInfoState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
  1370. const templateId = normalizeTemplateId(
  1371. node?.templateId ?? templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
  1372. );
  1373. const parentTemplateId = getSelectedTemplateMainId(node?.mainId);
  1374. const parentTemplate = parentTemplateId
  1375. ? resolveTemplateInfoById(parentTemplateId)
  1376. : resolveParentTemplateInfoById(templateId);
  1377. const source = resolveRecognitionBasicInfoSource(node, options);
  1378. const hasNodeFuncRegx =
  1379. hasOwnRecognitionBasicInfoConfig(node) &&
  1380. !isSameRecognitionBasicInfoConfig(
  1381. node?.funcRegx,
  1382. templateDetail.value?.funcRegx || activeTemplateItem.value?.funcRegx,
  1383. ) &&
  1384. !isSameRecognitionBasicInfoConfig(node?.funcRegx, parentTemplate?.funcRegx);
  1385. const shouldClearFilledContent = options.preferTemplate || !hasNodeFuncRegx;
  1386. recognitionBasicInfoBaseFuncRegx.value = source;
  1387. const parsedRows = parseRecognitionBasicInfoRows(source, parentTemplate?.funcRegx);
  1388. recognitionBasicInfoRows.value = shouldClearFilledContent
  1389. ? clearRecognitionBasicInfoFilledContent(parsedRows)
  1390. : parsedRows;
  1391. recognitionBasicInfoExpanded.value = !!recognitionBasicInfoRows.value.length;
  1392. };
  1393. const resolveRecognitionAmountZfEnabled = (
  1394. node?: TreeFileInfo | null,
  1395. options: { preferTemplate?: boolean } = {},
  1396. ) => {
  1397. const tableRule = findTransAmountTableRule(node?.tableRuleList);
  1398. const tableRuleZf = getRecordZfValue(tableRule);
  1399. const templateField = findTransAmountTemplateFieldMeta(templateDetail.value || activeTemplateItem.value);
  1400. const templateZf = getRecordZfValue(templateField);
  1401. if (options.preferTemplate) {
  1402. return templateZf !== undefined
  1403. ? getZfFlag(templateZf)
  1404. : tableRuleZf !== undefined
  1405. ? getZfFlag(tableRuleZf)
  1406. : false;
  1407. }
  1408. return tableRuleZf !== undefined
  1409. ? getZfFlag(tableRuleZf)
  1410. : templateZf !== undefined
  1411. ? getZfFlag(templateZf)
  1412. : false;
  1413. };
  1414. const applyRecognitionAmountZfState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
  1415. recognitionAmountZfEnabled.value = resolveRecognitionAmountZfEnabled(node, options);
  1416. };
  1417. const buildRecognitionBasicInfoFuncRegx = () => {
  1418. if (!recognitionBasicInfoRows.value.length) {
  1419. return recognitionBasicInfoBaseFuncRegx.value;
  1420. }
  1421. if (!hasRecognitionBasicInfoFilledContent(recognitionBasicInfoRows.value)) {
  1422. return '';
  1423. }
  1424. const payload = recognitionBasicInfoRows.value.reduce(
  1425. (acc, item) => {
  1426. const targetField = resolveRecognitionBasicInfoTargetField(item.targetField);
  1427. if (!targetField) return acc;
  1428. acc[targetField] = [
  1429. normalizeRecognitionBasicInfoText(item.headerMatch),
  1430. normalizeRecognitionBasicInfoText(item.endFlag),
  1431. ];
  1432. return acc;
  1433. },
  1434. {} as Record<string, string[]>,
  1435. );
  1436. return Object.keys(payload).length ? JSON.stringify(payload) : '';
  1437. };
  1438. const applyNodePreset = async (item?: TreeFileInfo | null) => {
  1439. const previewDataset = await loadPreviewDatasetFromFile(item);
  1440. const hasPresetHeaders = previewDataset ? applyPreviewDataset(previewDataset) : applyPresetHeadersFromNode(item);
  1441. restoreStoredColumnRules(item?.tableRuleList);
  1442. applyRecognitionDirectionState(item);
  1443. applyRecognitionBasicInfoState(item);
  1444. applyRecognitionAmountZfState(item);
  1445. childTemplateName.value = normalizeRuleText(item?.childTemplateName);
  1446. const headerRow = normalizeStoredLineNoToRowIndex(item?.lineNo);
  1447. if (headerRow !== null && !hasPresetHeaders) {
  1448. applyHeaderRow(headerRow, { silent: true, autoMatch: false });
  1449. } else {
  1450. headerRowIndex.value = headerRow;
  1451. selectedRowIndex.value = headerRow;
  1452. syncHeaderRowInput();
  1453. }
  1454. if (!applyTableRuleMapping(item?.tableRuleList)) {
  1455. autoMatchFields(false);
  1456. }
  1457. restoreStoredFunRules(item?.tableRuleList);
  1458. restoreFileLevelRules(item);
  1459. runRulePipeline();
  1460. };
  1461. const DEFAULT_TEMPLATE_CATEGORY = '全部分类';
  1462. const templateModalOpen = ref(false);
  1463. const activeTemplateTab = ref<TemplateTab>('template');
  1464. const activeCategory = ref(DEFAULT_TEMPLATE_CATEGORY);
  1465. const templateKeyword = ref('');
  1466. const templateSource = ref<number>(1);
  1467. const modalTemplateDetail = ref<TableInfo | null>(null);
  1468. const templatePreviewLoading = ref(false);
  1469. let templatePreviewRequestToken = 0;
  1470. const categories = computed(() => {
  1471. const set = new Set<string>(
  1472. templateCategoryOptions.value.length ? templateCategoryOptions.value : [DEFAULT_TEMPLATE_CATEGORY],
  1473. );
  1474. templateList.value.forEach((t) => set.add(t.classify || FALLBACK_TEMPLATE_CATEGORY));
  1475. return Array.from(set);
  1476. });
  1477. const showTemplateTabSwitch = computed(
  1478. () => activeCategory.value === DEFAULT_TEMPLATE_CATEGORY || pluginCategorySet.value.has(activeCategory.value),
  1479. );
  1480. const resolveNodeTargetFields = (item?: TreeFileInfo | null, preferredTemplateId?: string | number | null) => {
  1481. const tableRules = normalizeTableRuleList(item?.tableRuleList);
  1482. const templateId = normalizeTemplateId(preferredTemplateId ?? item?.templateId);
  1483. const template = (templateId && (getTemplateListItemById(templateId) || mockTemplateMap[templateId])) || null;
  1484. const baseFields = tableRules.length
  1485. ? buildTargetFieldsFromRuleList(tableRules)
  1486. : buildTargetFieldsFromTemplateInfo(template);
  1487. return applyTableRuleRequiredFlags(baseFields, tableRules);
  1488. };
  1489. const getRequiredMatchStats = (
  1490. fields: MappingTargetField[],
  1491. mapping: Record<string, string>,
  1492. recognitionRows: RecognitionBasicInfoRow[] = [],
  1493. amountZfEnabled = false,
  1494. ) => {
  1495. const matchedByRecognitionBasicInfo = buildRecognitionBasicInfoMatchedTargetKeySet(recognitionRows, fields);
  1496. const handledTargetKeySet = new Set(matchedByRecognitionBasicInfo);
  1497. buildAmountZfHandledTargetKeySet(fields, amountZfEnabled).forEach((key) => {
  1498. handledTargetKeySet.add(key);
  1499. });
  1500. const effectiveMapping = buildEffectiveFieldMapping(mapping, handledTargetKeySet);
  1501. const requiredFields = fields.filter((item) => item.required);
  1502. const requiredTotal = requiredFields.length;
  1503. const requiredMatched = requiredFields.filter(
  1504. (item) => handledTargetKeySet.has(item.key) || !!effectiveMapping[item.key],
  1505. ).length;
  1506. const matchedCount = fields.filter(
  1507. (item) => handledTargetKeySet.has(item.key) || !!effectiveMapping[item.key],
  1508. ).length;
  1509. return { requiredTotal, requiredMatched, matchedCount };
  1510. };
  1511. const getSheetNodeVisualState = (item?: TreeFileInfo | null): SheetNodeVisualState => {
  1512. if (!isTreeSheetNode(item)) return 'fail';
  1513. const nodeKey = getTreeNodeKey(item);
  1514. if (analyzingNodeKeys.value.includes(nodeKey)) {
  1515. return 'analyzing';
  1516. }
  1517. const defaultState = getFileStatusVisualState(item.fileStatus) || (isSheetMatchComplete(item) ? 'success' : 'fail');
  1518. const initialSignature = initialNodeStatusSignatureMap.get(nodeKey);
  1519. if (activeNodeKey.value === nodeKey) {
  1520. const currentState = buildNodeCacheState();
  1521. const hasEditedState = !initialSignature || buildNodeStatusSignature(currentState) !== initialSignature;
  1522. if (!hasEditedState) {
  1523. return defaultState;
  1524. }
  1525. return isMatchSuccessful(requiredTotal.value, requiredMatchedCount.value, matchedFieldCount.value)
  1526. ? 'success'
  1527. : 'fail';
  1528. }
  1529. const cached = nodeStateCache.get(nodeKey);
  1530. if (cached) {
  1531. const hasEditedState = !initialSignature || buildNodeStatusSignature(cached) !== initialSignature;
  1532. if (!hasEditedState) {
  1533. return defaultState;
  1534. }
  1535. const {
  1536. requiredTotal: cachedRequiredTotal,
  1537. requiredMatched: cachedRequiredMatched,
  1538. matchedCount: cachedMatchedCount,
  1539. } = getRequiredMatchStats(
  1540. resolveNodeTargetFields(item, cached.selectedTemplateId),
  1541. cached.fieldMapping,
  1542. cached.recognitionBasicInfoRows,
  1543. cached.recognitionAmountZfEnabled,
  1544. );
  1545. return isMatchSuccessful(cachedRequiredTotal, cachedRequiredMatched, cachedMatchedCount) ? 'success' : 'fail';
  1546. }
  1547. return defaultState;
  1548. };
  1549. const syncModalTemplateItem = (preferredId?: string | number | null) => {
  1550. const currentBaseList = filteredTemplateList.value;
  1551. if (activeTemplateTab.value === 'plugin') {
  1552. const currentPluginId = modalTemplateItem.value?.id;
  1553. const matchedPlugin = currentBaseList.find((item) => String(item.id) === String(currentPluginId)) || null;
  1554. modalTemplateItem.value = matchedPlugin;
  1555. return modalTemplateItem.value;
  1556. }
  1557. const preferredTemplateId = normalizeTemplateId(preferredId);
  1558. const currentTemplateId = normalizeTemplateId(modalTemplateItem.value?.id);
  1559. if (preferredTemplateId) {
  1560. const matched = currentBaseList.find((item) => normalizeTemplateId(item.id) === preferredTemplateId) || null;
  1561. if (matched) {
  1562. modalTemplateItem.value = matched;
  1563. return matched;
  1564. }
  1565. }
  1566. if (currentTemplateId) {
  1567. const currentMatched = currentBaseList.find((item) => normalizeTemplateId(item.id) === currentTemplateId) || null;
  1568. if (currentMatched) {
  1569. modalTemplateItem.value = currentMatched;
  1570. return currentMatched;
  1571. }
  1572. }
  1573. modalTemplateItem.value = currentBaseList[0] || null;
  1574. return modalTemplateItem.value;
  1575. };
  1576. const resolveTemplateInfoById = (templateId?: string | number | null) => {
  1577. const id = normalizeTemplateId(templateId);
  1578. if (!id) return null;
  1579. const activeDetail = normalizeTemplateId(templateDetail.value?.id) === id ? templateDetail.value : null;
  1580. const activeItem = normalizeTemplateId(activeTemplateItem.value?.id) === id ? activeTemplateItem.value : null;
  1581. const cachedDetail = templateDetailCache.get(id) || null;
  1582. const listItem = getTemplateListItemById(id) || mockTemplateMap[id] || null;
  1583. return (
  1584. mergeTemplateInfo(activeItem || listItem || activeDetail, activeDetail || cachedDetail) ||
  1585. activeDetail ||
  1586. cachedDetail ||
  1587. activeItem ||
  1588. listItem ||
  1589. null
  1590. );
  1591. };
  1592. const resolveParentTemplateInfoById = (templateId?: string | number | null) => {
  1593. const template = resolveTemplateInfoById(templateId);
  1594. const mainId = normalizeTemplateId((template as TableInfo | null)?.mainId);
  1595. return (mainId && resolveTemplateInfoById(mainId)) || template;
  1596. };
  1597. const getSelectedTemplateMainId = (fallbackMainId?: string | number | null) => {
  1598. if (templateDetail.value && Object.prototype.hasOwnProperty.call(templateDetail.value, 'mainId')) {
  1599. return normalizeTemplateId(templateDetail.value.mainId);
  1600. }
  1601. if (activeTemplateItem.value && Object.prototype.hasOwnProperty.call(activeTemplateItem.value, 'mainId')) {
  1602. return normalizeTemplateId(activeTemplateItem.value.mainId);
  1603. }
  1604. return normalizeTemplateId(fallbackMainId);
  1605. };
  1606. const syncModalTemplatePreviewState = (
  1607. template?: TemplateListItem | null,
  1608. options: { preferAppliedDetail?: boolean } = {},
  1609. ) => {
  1610. modalTemplateItem.value = template || null;
  1611. modalTemplateDetail.value = null;
  1612. templatePreviewLoading.value = false;
  1613. templatePreviewRequestToken += 1;
  1614. const id = normalizeTemplateId(template?.id);
  1615. if (!id || Number(template?.id) < 0) return null;
  1616. if (options.preferAppliedDetail && normalizeTemplateId(templateDetail.value?.id) === id && templateDetail.value) {
  1617. modalTemplateDetail.value = templateDetail.value;
  1618. return modalTemplateDetail.value;
  1619. }
  1620. const cachedDetail = templateDetailCache.get(id);
  1621. if (cachedDetail) {
  1622. modalTemplateDetail.value = cachedDetail;
  1623. return modalTemplateDetail.value;
  1624. }
  1625. return null;
  1626. };
  1627. const loadTemplatePreviewDetail = async (
  1628. template?: TemplateListItem | null,
  1629. options: { preferAppliedDetail?: boolean } = {},
  1630. ) => {
  1631. const existingDetail = syncModalTemplatePreviewState(template, options);
  1632. if (existingDetail) {
  1633. return existingDetail;
  1634. }
  1635. const id = normalizeTemplateId(template?.id);
  1636. if (!id || Number(template?.id) < 0) return null;
  1637. const requestToken = ++templatePreviewRequestToken;
  1638. templatePreviewLoading.value = true;
  1639. try {
  1640. const detail = await tplDetail({ id });
  1641. if (requestToken !== templatePreviewRequestToken) return null;
  1642. if (detail) {
  1643. templateDetailCache.set(id, detail);
  1644. modalTemplateDetail.value = detail;
  1645. mergeTemplateListItem(mergeTemplateInfo(template, detail));
  1646. modalTemplateItem.value = getTemplateListItemById(id) || template || null;
  1647. }
  1648. return modalTemplateDetail.value;
  1649. } catch (error) {
  1650. if (requestToken === templatePreviewRequestToken) {
  1651. modalTemplateDetail.value = null;
  1652. }
  1653. return null;
  1654. } finally {
  1655. if (requestToken === templatePreviewRequestToken) {
  1656. templatePreviewLoading.value = false;
  1657. }
  1658. }
  1659. };
  1660. const closeTemplateModal = () => {
  1661. templateModalOpen.value = false;
  1662. templatePreviewRequestToken += 1;
  1663. templatePreviewLoading.value = false;
  1664. modalTemplateDetail.value = null;
  1665. modalTemplateItem.value = null;
  1666. };
  1667. const loadTemplateDetail = async (
  1668. templateId?: string | number | null,
  1669. options: { silent?: boolean; requestDetail?: boolean } = {},
  1670. ) => {
  1671. const id = normalizeTemplateId(templateId ?? selectedTemplateId.value);
  1672. if (!id) {
  1673. syncSelectedTemplateState(id);
  1674. return templateDetail.value;
  1675. }
  1676. if (!templateListLoaded.value && !templateListLoading.value) {
  1677. await loadTemplateList({ silent: true, preferredId: id });
  1678. }
  1679. const { matchedListItem, cachedDetail } = syncSelectedTemplateState(id);
  1680. if (!id || !options.requestDetail || cachedDetail) {
  1681. return templateDetail.value;
  1682. }
  1683. try {
  1684. const resp = await tplDetail({ id });
  1685. const detail = resp || null;
  1686. if (detail) {
  1687. templateDetailCache.set(id, detail);
  1688. }
  1689. templateDetail.value = detail
  1690. ? mergeTemplateInfo(matchedListItem, detail)
  1691. : matchedListItem || mockTemplateMap[id] || null;
  1692. templateDetailFromMock.value = !detail && !matchedListItem && !!mockTemplateMap[id];
  1693. if (templateDetail.value) {
  1694. mergeTemplateListItem(templateDetail.value);
  1695. activeTemplateItem.value = getTemplateListItemById(id) || (templateDetail.value as TemplateListItem);
  1696. }
  1697. } catch (error) {
  1698. templateDetail.value = matchedListItem || mockTemplateMap[id] || null;
  1699. templateDetailFromMock.value = !matchedListItem && !!mockTemplateMap[id];
  1700. activeTemplateItem.value = (matchedListItem || (templateDetail.value as TemplateListItem | null)) ?? null;
  1701. if (!options.silent) {
  1702. createMessage.warning('模板详情接口暂不可用,已使用当前列表匹配结果');
  1703. }
  1704. }
  1705. return templateDetail.value;
  1706. };
  1707. const openTemplateModal = async () => {
  1708. templateModalOpen.value = true;
  1709. activeTemplateTab.value = 'template';
  1710. activeCategory.value = DEFAULT_TEMPLATE_CATEGORY;
  1711. templateKeyword.value = '';
  1712. templateSource.value = 0;
  1713. await Promise.all([
  1714. loadTemplateCategories(),
  1715. loadTemplateList({ silent: true, preferredId: selectedTemplateId.value }),
  1716. ]);
  1717. const previewTarget = syncModalTemplateItem(selectedTemplateId.value);
  1718. await loadTemplatePreviewDetail(previewTarget, { preferAppliedDetail: true });
  1719. };
  1720. watch([activeCategory, templateSource, templateKeyword, activeTemplateTab], async () => {
  1721. if (!templateModalOpen.value) return;
  1722. if (!showTemplateTabSwitch.value) {
  1723. activeTemplateTab.value = 'template';
  1724. }
  1725. const previewTarget = syncModalTemplateItem(
  1726. activeTemplateTab.value === 'plugin' ? undefined : selectedTemplateId.value,
  1727. );
  1728. await loadTemplatePreviewDetail(previewTarget, {
  1729. preferAppliedDetail:
  1730. activeTemplateTab.value !== 'plugin' && normalizeTemplateId(previewTarget?.id) === selectedTemplateId.value,
  1731. });
  1732. });
  1733. watch(activeTemplateTab, (tab) => {
  1734. if (!templateModalOpen.value) return;
  1735. if (tab === 'plugin') {
  1736. modalTemplateDetail.value = null;
  1737. templatePreviewLoading.value = false;
  1738. templatePreviewRequestToken += 1;
  1739. }
  1740. });
  1741. watch(categories, (nextCategories) => {
  1742. if (!nextCategories.includes(activeCategory.value)) {
  1743. activeCategory.value = DEFAULT_TEMPLATE_CATEGORY;
  1744. }
  1745. });
  1746. const activeNodeInfo = computed(() => {
  1747. return activeNodeKey.value ? findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw || null : null;
  1748. });
  1749. // Phase 4:模板域(匹配条 / 创建子模板 / 空态)下沉到 composable。页面同名解构接住,原有代码与模板零改动。
  1750. const templateApi = useTemplateLibrary({ DEFAULT_TEMPLATE_CATEGORY, activeCategory, activeNodeInfo, activeTemplateItem, activeTemplateTab, addedColumnConfig, addedColumns, childTemplateName, createMessage, fieldMapping, headerRowIndex, recognitionAmountZfEnabled, recognitionBasicInfoBaseFuncRegx, recognitionBasicInfoManualVisible, recognitionBasicInfoRows, recognitionDirectionFields, rightTableHeaders, selectRules, selectedRules, selectedTemplateId, splitColumnConfig, syncActiveNodeStructure, templateDetail, templateKeyword, templateSource });
  1751. const { FALLBACK_TEMPLATE_CATEGORY, checkTemplateNameAvailable, currentTemplateDisplayName, currentTemplateName, filteredTemplateList, getTemplateListItemById, hasTemplateSelected, isSystemTemplateRecord, loadTemplateCategories, loadTemplateList, mergeTemplateInfo, mergeTemplateListItem, modalTemplateItem, pluginCategorySet, showChildTemplateExistsWarning, syncSelectedTemplateState, templateCategoryOptions, templateDetailCache, templateDetailFromMock, templateList, templateListLoaded, templateListLoading, templateSearchBaseList } = templateApi;
  1752. // Phase 4:字段映射域下沉到 composable。页面同名解构接住,原有代码与模板零改动。
  1753. const mappingApi = useFieldMapping({ activeNodeInfo, activeNodeKey, activeNodeTableRuleMap, activeTemplateItem, addedColumns, buildRecognitionDirectionConf, createMessage, currentColumn, effectiveFieldMapping, fieldMapping, fixedTargetFields, headerRowIndex, ignoredRowsSet, isTargetHandledByRecognitionRule, mappingPopoverOpenKey, matchedTargetFieldsByHeader, originFilterKeyword, recognitionDirectionFields, resolveTemplateFieldMeta, rightTableHeaders, selectedAddedColumn, selectedRowIndex, sourceTableRows, tablePreviewRows, templateDetail, treeData });
  1754. const { activeMappingTargetKey, autoMatchFields, clearOriginHoverPreviewState, focusPreviewColumn, headerFormatValidationMap, isMappingTargetRequired, isTargetMatched, resolveRecognitionDirectionFields, tableMockRef } = mappingApi;
  1755. const activeRecognitionRuleDesc = computed(() => {
  1756. const templateName = currentTemplateName.value === '-' ? '当前模板' : currentTemplateName.value;
  1757. const ruleLabel = activeRecognitionRule.value || recognitionRuleTabs.value[0] || '识别规则';
  1758. return `围绕「${templateName}」预置 ${ruleLabel} 的识别配置,后续可结合导入样本继续沉淀成子模板。`;
  1759. });
  1760. watch(
  1761. recognitionRuleTabs,
  1762. (tabs) => {
  1763. if (!tabs.length) {
  1764. activeRecognitionRule.value = '';
  1765. return;
  1766. }
  1767. if (!tabs.includes(activeRecognitionRule.value)) {
  1768. activeRecognitionRule.value = tabs[0];
  1769. }
  1770. },
  1771. { immediate: true },
  1772. );
  1773. watch(
  1774. fixedTargetFields,
  1775. (fields) => {
  1776. if (activeMappingTargetKey.value && !fields.some((item) => item.key === activeMappingTargetKey.value)) {
  1777. activeMappingTargetKey.value = '';
  1778. }
  1779. },
  1780. { deep: true },
  1781. );
  1782. const startResize = (type: 'left' | 'middle', event: MouseEvent) => {
  1783. event.preventDefault();
  1784. resizeState.value = {
  1785. type,
  1786. startX: event.clientX,
  1787. startLeft: leftPanelWidth.value,
  1788. startMiddle: middlePanelWidth.value,
  1789. };
  1790. document.body.classList.add('is-resizing-layout');
  1791. document.body.style.cursor = 'col-resize';
  1792. };
  1793. const applyResize = () => {
  1794. if (!resizeState.value.type || pendingClientX.value === null) return;
  1795. const deltaX = pendingClientX.value - resizeState.value.startX;
  1796. if (resizeState.value.type === 'left' && !leftCollapsed.value) {
  1797. const next = resizeState.value.startLeft + deltaX;
  1798. leftPanelWidth.value = Math.min(maxLeftWidth, Math.max(minLeftWidth, next));
  1799. }
  1800. if (resizeState.value.type === 'middle') {
  1801. const next = resizeState.value.startMiddle + deltaX;
  1802. middlePanelWidth.value = Math.min(maxMiddleWidth, Math.max(minMiddleWidth, next));
  1803. }
  1804. rafId.value = null;
  1805. };
  1806. const onResizeMouseMove = (event: MouseEvent) => {
  1807. if (!resizeState.value.type) return;
  1808. event.preventDefault();
  1809. pendingClientX.value = event.clientX;
  1810. if (rafId.value === null) {
  1811. rafId.value = window.requestAnimationFrame(applyResize);
  1812. }
  1813. };
  1814. const stopResize = () => {
  1815. resizeState.value.type = '';
  1816. resizeState.value.startX = 0;
  1817. pendingClientX.value = null;
  1818. if (rafId.value !== null) {
  1819. window.cancelAnimationFrame(rafId.value);
  1820. rafId.value = null;
  1821. }
  1822. document.body.classList.remove('is-resizing-layout');
  1823. document.body.style.cursor = '';
  1824. };
  1825. const openAddColumn = () => {
  1826. addColumnMode.value = 'add';
  1827. addColumnDialogOpen.value = true;
  1828. addColumnForm.value = { name: '', mode: 'ref', contentFields: [], customContent: '' };
  1829. selectedSourceField.value = availableSourceHeaders.value[0] || '';
  1830. selectedContentIndex.value = -1;
  1831. draggingContentIndex.value = null;
  1832. dragOverContentIndex.value = null;
  1833. };
  1834. const openSplitColumn = () => {
  1835. if (!availableSourceHeaders.value.length) {
  1836. createMessage.warning('当前没有可供拆分的原始列');
  1837. return;
  1838. }
  1839. splitColumnDialogOpen.value = true;
  1840. splitColumnForm.value = {
  1841. source: availableSourceHeaders.value[0] || '',
  1842. targetName1: '',
  1843. targetName2: '',
  1844. delimiterType: 'length',
  1845. delimiterValue: '',
  1846. };
  1847. };
  1848. const openEditColumn = () => {
  1849. if (!selectedAddedColumn.value) {
  1850. createMessage.warning('请先选中一个新增列');
  1851. return;
  1852. }
  1853. addColumnMode.value = 'edit';
  1854. addColumnDialogOpen.value = true;
  1855. const config = addedColumnConfig.value[selectedAddedColumn.value];
  1856. addColumnForm.value = {
  1857. name: selectedAddedColumn.value,
  1858. mode: config?.mode || 'ref',
  1859. contentFields: (config?.contentFields || []).filter((field) => availableSourceHeaders.value.includes(field)),
  1860. customContent: config?.customContent || '',
  1861. };
  1862. selectedSourceField.value = availableSourceHeaders.value[0] || '';
  1863. selectedContentIndex.value = -1;
  1864. draggingContentIndex.value = null;
  1865. dragOverContentIndex.value = null;
  1866. };
  1867. const openDeleteColumn = () => {
  1868. if (!selectedAddedColumn.value) {
  1869. createMessage.warning('请先选中一个新增列');
  1870. return;
  1871. }
  1872. deleteColumnDialogOpen.value = true;
  1873. };
  1874. const buildEnabledGroupingColumnsPayload = () =>
  1875. rightTableHeaders.value.reduce(
  1876. (acc, header) => {
  1877. if (!addedColumns.value.includes(header)) {
  1878. return acc;
  1879. }
  1880. const groupColumns = getEnabledGroupingColumnsForHeader(header);
  1881. if (!groupColumns.length) {
  1882. return acc;
  1883. }
  1884. acc.push({
  1885. targetHeader: header,
  1886. groupFlag: true,
  1887. groupColumns: cloneDeepSafe(groupColumns),
  1888. });
  1889. return acc;
  1890. },
  1891. [] as Array<{
  1892. targetHeader: string;
  1893. groupFlag: boolean;
  1894. groupColumns: Array<Record<string, any>>;
  1895. }>,
  1896. );
  1897. const confirmDeleteColumn = () => {
  1898. const name = selectedAddedColumn.value;
  1899. if (!name) return;
  1900. rightTableHeaders.value = rightTableHeaders.value.filter((h) => h !== name);
  1901. addedColumns.value = addedColumns.value.filter((h) => h !== name);
  1902. delete addedColumnConfig.value[name];
  1903. const deletedSplitConfig = splitColumnConfig.value[name];
  1904. delete splitColumnConfig.value[name];
  1905. if (deletedSplitConfig) {
  1906. [deletedSplitConfig.targetName1, deletedSplitConfig.targetName2].filter(Boolean).forEach((targetName) => {
  1907. const targetConfig = splitColumnConfig.value[targetName];
  1908. if (targetName === name || !targetConfig) return;
  1909. splitColumnConfig.value[targetName] = {
  1910. ...targetConfig,
  1911. targetName1: targetConfig.targetName1 === name ? '' : targetConfig.targetName1,
  1912. targetName2: targetConfig.targetName2 === name ? '' : targetConfig.targetName2,
  1913. };
  1914. });
  1915. }
  1916. delete generatedColumnHeaderRowMap.value[name];
  1917. sourceTableRows.value.forEach((row) => {
  1918. delete row[name];
  1919. });
  1920. Object.keys(fieldMapping.value).forEach((key) => {
  1921. if (fieldMapping.value[key] === name) {
  1922. fieldMapping.value[key] = '';
  1923. }
  1924. });
  1925. selectRules.value = remapSelectRuleHeaderRefs(selectRules.value, {}, new Set([name]));
  1926. groupingColumnsDraft.value = remapGroupingColumnRules(groupingColumnsDraft.value, {}, new Set([name]));
  1927. selectedAddedColumn.value = addedColumns.value[0] || '';
  1928. currentColumn.value = selectedAddedColumn.value
  1929. ? rightTableHeaders.value.findIndex((h) => h === selectedAddedColumn.value)
  1930. : null;
  1931. selectedRowIndex.value = null;
  1932. runRulePipeline();
  1933. deleteColumnDialogOpen.value = false;
  1934. createMessage.success('删除列成功');
  1935. };
  1936. const onClickHeader = (header: string) => {
  1937. const index = rightTableHeaders.value.findIndex((h) => h === header);
  1938. if (index < 0) return;
  1939. closeMappingPopoverPreview();
  1940. focusPreviewColumn(header, index, { resetSelectedRow: true });
  1941. };
  1942. const onClickCell = (rowIndex: number, header: string, colIndex: number) => {
  1943. closeMappingPopoverPreview();
  1944. selectedRowIndex.value = rowIndex;
  1945. focusPreviewColumn(header, colIndex, { resetSelectedRow: false });
  1946. };
  1947. const toggleLeft = () => {
  1948. leftCollapsed.value = !leftCollapsed.value;
  1949. };
  1950. const handleSelect = (keys: (string | number)[]) => {
  1951. const rawKey = keys[0] ? String(keys[0]) : '';
  1952. const nextKey = rawKey ? resolveSelectableTreeKey(rawKey) : '';
  1953. selectedKeys.value = nextKey ? [nextKey] : [];
  1954. if (nextKey) {
  1955. switchNodeState(nextKey);
  1956. }
  1957. };
  1958. const handleImportData = async () => {
  1959. saveCurrentNodeState();
  1960. syncActiveNodeStructure();
  1961. const sanitizedFileInfos = sanitizeFileInfoTree(fileList.value, { keepRuleKey: false });
  1962. if (!sanitizedFileInfos.length) {
  1963. createMessage.warning('未找到可导入的数据,请先上传并完成模板匹配');
  1964. return;
  1965. }
  1966. const matchedSheetsWithoutHeaderRow = collectMatchedSheetsWithoutHeaderRow(sanitizedFileInfos);
  1967. if (matchedSheetsWithoutHeaderRow.length) {
  1968. const previewNames = matchedSheetsWithoutHeaderRow.slice(0, 3).join('、');
  1969. const suffix =
  1970. matchedSheetsWithoutHeaderRow.length > 3 ? ` 等 ${matchedSheetsWithoutHeaderRow.length} 个工作表` : '';
  1971. createMessage.warning(`已匹配模板的工作表必须先填写表头所在行:${previewNames}${suffix}`);
  1972. return;
  1973. }
  1974. try {
  1975. const childTemplateNameValid = await ensureChildTemplateNamesAvailable(sanitizedFileInfos);
  1976. if (!childTemplateNameValid) {
  1977. return;
  1978. }
  1979. } catch (error) {
  1980. createMessage.error(getRequestErrorMessage(error, '模板名称校验失败'));
  1981. return;
  1982. }
  1983. sessionStorage.setItem(FILE_INFO_STORAGE_KEY, JSON.stringify(sanitizedFileInfos));
  1984. importingData.value = true;
  1985. try {
  1986. const resolveMappedHeaderByTargetField = (item?: TreeFileInfo | null, targetField?: string) => {
  1987. const normalizedTarget = normalizeFieldMatchText(targetField);
  1988. if (!normalizedTarget) return '';
  1989. const matchedRule = normalizeTableRuleList(item?.tableRuleList).find((rule, index) => {
  1990. const targetKey = normalizeFieldMatchText(getTableRuleTargetKey(rule, index));
  1991. const targetLabel = normalizeFieldMatchText(getTableRuleTargetLabel(rule, index));
  1992. return normalizedTarget === targetKey || normalizedTarget === targetLabel;
  1993. });
  1994. return normalizeRecognitionConfigText(matchedRule?.fileColName);
  1995. };
  1996. const buildImportRecognitionBasicInfoFuncRegx = (item?: TreeFileInfo | null) => {
  1997. const source = normalizeRecognitionConfigText(item?.funcRegx);
  1998. if (!source) return '';
  1999. const rows = parseRecognitionBasicInfoRows(source);
  2000. if (!rows.length) return source;
  2001. if (!hasRecognitionBasicInfoFilledContent(rows)) return '';
  2002. const payload = rows.reduce(
  2003. (acc, row) => {
  2004. const targetField = resolveRecognitionBasicInfoTargetField(row.targetField);
  2005. const headerMatch = normalizeRecognitionBasicInfoText(row.headerMatch);
  2006. const endFlag = normalizeRecognitionBasicInfoText(row.endFlag);
  2007. const mappedHeader = resolveMappedHeaderByTargetField(item, targetField);
  2008. const nextKey = headerMatch || mappedHeader || targetField;
  2009. if (!nextKey) return acc;
  2010. acc[nextKey] = [headerMatch, endFlag];
  2011. return acc;
  2012. },
  2013. {} as Record<string, string[]>,
  2014. );
  2015. return Object.keys(payload).length ? JSON.stringify(payload) : '';
  2016. };
  2017. const buildImportFileInfoPayload = (item?: TreeFileInfo | null): TreeFileInfo | null => {
  2018. if (!item) return null;
  2019. const nextItem = cloneDeepSafe(item) as TreeFileInfo;
  2020. const children = Array.isArray(nextItem.children)
  2021. ? nextItem.children
  2022. .map((child) => buildImportFileInfoPayload(child))
  2023. .filter((child): child is TreeFileInfo => Boolean(child))
  2024. : nextItem.children;
  2025. if (isFileRootItem(nextItem)) {
  2026. const rootPayload = {
  2027. ...nextItem,
  2028. children,
  2029. } as TreeFileInfo & { childTemplateName?: string | null };
  2030. delete rootPayload.childTemplateName;
  2031. return rootPayload;
  2032. }
  2033. const childName = normalizeRuleText(nextItem.childTemplateName);
  2034. const hasNew = !!childName || !!nextItem.hasNew;
  2035. const importTemplateId = normalizeTemplateId(
  2036. nextItem.templateId ?? templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
  2037. );
  2038. const currentTemplate = resolveTemplateInfoById(importTemplateId);
  2039. const parentTemplateId = normalizeTemplateId(nextItem.mainId) || normalizeTemplateId(currentTemplate?.mainId);
  2040. const parentTemplate = (parentTemplateId && resolveTemplateInfoById(parentTemplateId)) || currentTemplate;
  2041. const currentFuncRegx = normalizeRecognitionConfigText(nextItem.funcRegx);
  2042. const importFuncRegx = currentFuncRegx ? buildImportRecognitionBasicInfoFuncRegx(nextItem) : '';
  2043. const sheetPayload = {
  2044. ...nextItem,
  2045. tableName: hasNew ? childName : nextItem.tableName,
  2046. tableNameEn: hasNew
  2047. ? normalizeRuleText(parentTemplate?.tableNameEn) ||
  2048. normalizeRuleText(currentTemplate?.tableNameEn) ||
  2049. normalizeRuleText(nextItem.tableNameEn) ||
  2050. null
  2051. : nextItem.tableNameEn,
  2052. hasNew,
  2053. classify: hasNew
  2054. ? normalizeRuleText(parentTemplate?.classify) ||
  2055. normalizeRuleText(currentTemplate?.classify) ||
  2056. normalizeRuleText(nextItem.classify) ||
  2057. null
  2058. : nextItem.classify,
  2059. funcRegx: importFuncRegx || null,
  2060. children,
  2061. } as TreeFileInfo & { childTemplateName?: string | null };
  2062. return sheetPayload;
  2063. };
  2064. const importFileInfos = sanitizedFileInfos
  2065. .map((item) => buildImportFileInfoPayload(item))
  2066. .filter((item): item is TreeFileInfo => Boolean(item));
  2067. sessionStorage.setItem(IMPORT_FILE_INFO_STORAGE_KEY, JSON.stringify(importFileInfos));
  2068. resetImportProgressSession();
  2069. await go(PageEnum.BASE_CLEAN_PROGRESS);
  2070. } catch {
  2071. createMessage.error('导入数据失败');
  2072. } finally {
  2073. importingData.value = false;
  2074. }
  2075. };
  2076. const getFocusedNodeKeyFromQuery = () => {
  2077. const rawFocusKey = pageQuery.value?.focusKey;
  2078. return Array.isArray(rawFocusKey) ? normalizeRuleText(rawFocusKey[0]) : normalizeRuleText(rawFocusKey);
  2079. };
  2080. const handleBack = () => {
  2081. // 返回上一级:文件上传页
  2082. go(PageEnum.BASE_UPLOAD);
  2083. };
  2084. const loadFileTree = async () => {
  2085. const storedList = loadStoredFileList();
  2086. fileList.value = storedList.length ? storedList : sanitizeFileInfoTree(cloneDeepSafe(mockFileList));
  2087. if (!treeData.value.length) return;
  2088. const focusedKey = resolveSelectableTreeKey(getFocusedNodeKeyFromQuery());
  2089. const initialKey =
  2090. focusedKey ||
  2091. (selectedKeys.value.length
  2092. ? resolveSelectableTreeKey(String(selectedKeys.value[0]))
  2093. : findFirstSelectableKey(treeData.value));
  2094. if (!initialKey) return;
  2095. selectedKeys.value = [initialKey];
  2096. await switchNodeState(initialKey);
  2097. if (focusedKey) {
  2098. scrollSelectedTreeNodeIntoView();
  2099. }
  2100. };
  2101. const initializePage = async () => {
  2102. void Promise.all([loadTemplateCategories(), loadTemplateList({ silent: true })]);
  2103. runRulePipeline();
  2104. await loadFileTree();
  2105. };
  2106. onMounted(() => {
  2107. initializePage();
  2108. window.addEventListener('mousemove', onResizeMouseMove);
  2109. window.addEventListener('mouseup', stopResize);
  2110. });
  2111. onBeforeUnmount(() => {
  2112. saveCurrentNodeState();
  2113. window.removeEventListener('mousemove', onResizeMouseMove);
  2114. window.removeEventListener('mouseup', stopResize);
  2115. document.body.classList.remove('is-resizing-layout');
  2116. document.body.style.cursor = '';
  2117. });
  2118. // Phase 2:把 Modal 子组件需要的页面级共享状态/方法 provide 下去(子组件内部按名取用)
  2119. provide(CleaningContextKey, { DEFAULT_TEMPLATE_CATEGORY, FALLBACK_TEMPLATE_CATEGORY, activeCategory, activeNodeInfo, activeNodeKey, activeTemplateItem, activeTemplateTab, addColumnDialogOpen, addColumnForm, addColumnMode, addedColumnConfig, addedColumns, applyHeaderRow, applyPresetHeadersFromNode, applyPreviewDataset, applyRecognitionAmountZfState, applyRecognitionBasicInfoState, applyRecognitionDirectionState, autoMatchFields, availableSourceHeaders, categories, childTemplateName, cleaningRules, closeTemplateModal, createMessage, currentColumn, dragOverContentIndex, draggingContentIndex, fieldMapping, filteredTemplateList, fixedTargetFields, generatedColumnHeaderRowMap, getGeneratedColumnHeaderRowIndex, groupingColumnsDraft, hasDuplicateHeaderName, headerRowIndex, headerRowInput, isSystemTemplateRecord, isTargetHandledByRecognitionRule, loadPreviewDatasetFromFile, loadTemplateDetail, loadTemplatePreviewDetail, mappingApi, mappingPopoverOpenKey, mergeTemplateInfo, modalTemplateDetail, modalTemplateItem, nodeStateCache, openTemplateModal, originFilterKeyword, recognitionAmountZfEnabled, recognitionApi, recognitionBasicInfoRows, remapGroupingColumnRules, remapSelectRuleHeaderRefs, resetNodeWorkState, rightTableHeaders, runRulePipeline, saveCurrentNodeState, selectRules, selectedAddedColumn, selectedContentIndex, selectedRowIndex, selectedSourceField, selectedTemplateId, setGeneratedColumnCell, showTemplateTabSwitch, sourceTableRows, splitColumnConfig, splitColumnDialogOpen, splitColumnForm, structureSyncSuspended, syncHeaderRowInput, syncModalTemplatePreviewState, templateApi, templateDetailCache, templateKeyword, templateModalOpen, templatePreviewLoading, templateSearchBaseList, templateSource });
  2120. </script>
  2121. <style lang="less">
  2122. // 样式已按域拆分到 ./cleaning/styles/ 下,由此处引入(保持全局,非 scoped)。
  2123. @import './cleaning/styles/index.less';
  2124. </style>