cleaning.vue 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326
  1. <template>
  2. <div :class="prefixCls" class="case-data-cleaning-page">
  3. <!-- 清洗流程公共顶栏:品牌 / 案件 / 模板库 / 下载管理 / 窗口控制 -->
  4. <CaseFlowTopBar />
  5. <div class="page-body">
  6. <div class="page-header">
  7. <div class="page-header-main">
  8. <div class="page-title-area">
  9. <Button type="text" class="back-btn" @click="handleBack">←</Button>
  10. <div class="page-title-block">
  11. <div class="page-title">案件数据清洗</div>
  12. </div>
  13. </div>
  14. <div class="page-header-actions">
  15. <Button @click="toggleLeft">{{ leftCollapsed ? '展开左侧' : '收起左侧' }}</Button>
  16. <Button type="primary" :loading="importingData" @click="handleImportData">导入数据</Button>
  17. </div>
  18. </div>
  19. </div>
  20. <div class="main-layout" :style="{ gridTemplateColumns: layoutTemplateColumns }">
  21. <FileTreePanel />
  22. <div v-show="!leftCollapsed" class="panel-resizer" @mousedown="startResize('left', $event)"></div>
  23. <section class="middle-panel">
  24. <div class="template-match-bar" :class="{ 'is-empty': !hasTemplateSelected }">
  25. <div class="template-match-main">
  26. <span class="template-match-icon"></span>
  27. <div class="template-match-copy">
  28. <div class="template-match-label">匹配模板</div>
  29. <div class="template-match-name">
  30. {{ currentTemplateDisplayName }}
  31. <span v-if="!hasTemplateSelected" class="template-match-badge is-pending">待配置</span>
  32. <span v-else-if="templateDetailFromMock" class="template-match-badge">示例</span>
  33. </div>
  34. </div>
  35. </div>
  36. <Button type="link" class="template-select-trigger" @click="openTemplateModal">选择模板</Button>
  37. </div>
  38. <template v-if="hasTemplateSelected">
  39. <div class="middle-panel-body">
  40. <FieldMappingCard />
  41. <div
  42. class="template-foot-panel"
  43. :class="{
  44. 'is-collapsed': showRecognitionPanel && !recognitionPanelExpanded,
  45. 'is-compact': !showRecognitionPanel,
  46. }"
  47. >
  48. <RecognitionRulePanel />
  49. <ChildTemplatePanel />
  50. </div>
  51. </div>
  52. </template>
  53. <MiddleEmptyState v-else />
  54. </section>
  55. <div class="panel-resizer" @mousedown="startResize('middle', $event)"></div>
  56. <section class="right-panel">
  57. <div class="panel-header">
  58. <div class="panel-title-wrap">
  59. <div class="panel-title">数据清洗区</div>
  60. </div>
  61. </div>
  62. <div class="toolbar toolbar--column-actions">
  63. <div class="toolbar-head">
  64. <span class="toolbar-label">列操作</span>
  65. </div>
  66. <div class="toolbar-actions">
  67. <Button size="small" type="primary" @click="openAddColumn">新增列</Button>
  68. <Button size="small" @click="openSplitColumn">拆分列</Button>
  69. <Button size="small" @click="openEditColumn">编辑列</Button>
  70. <Button size="small" @click="openDeleteColumn">删除列</Button>
  71. </div>
  72. <div class="toolbar-meta toolbar-meta--header-row">
  73. <span class="meta-label">表头行</span>
  74. <Input
  75. v-model:value="headerRowInput"
  76. size="small"
  77. class="meta-input"
  78. placeholder="输入行号"
  79. @pressEnter="applyHeaderRowInput"
  80. @blur="applyHeaderRowInput"
  81. />
  82. </div>
  83. </div>
  84. <div class="right-panel-body">
  85. <DataPreviewTable />
  86. <CleaningRulePanel />
  87. </div>
  88. </section>
  89. </div>
  90. </div>
  91. <AddColumnModal />
  92. <SplitColumnModal />
  93. <Modal v-model:open="deleteColumnDialogOpen" title="删除列" :width="520" :centered="true" @ok="confirmDeleteColumn">
  94. <div>确认删除列「{{ selectedAddedColumn || '-' }}」?</div>
  95. </Modal>
  96. <TemplateSelectModal />
  97. </div>
  98. </template>
  99. <script lang="ts" setup name="ViewsDataCleaning">
  100. import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
  101. import { Button, Input, Modal } from 'ant-design-vue';
  102. import { useDesign } from '@/core/hooks/web/useDesign';
  103. import { useGo, useQuery } from '@/core/hooks/web/usePage';
  104. import { PageEnum } from '@/core/enums/pageEnum';
  105. import { useMessage } from '@/core/hooks/web/useMessage';
  106. import { tplDetail } from '@/plat/api/tpl/tplApi';
  107. import type { TableInfo } from '@/plat/api/tpl/types/dto';
  108. import { resetImportProgressSession } from './importProgressSession';
  109. import { FILE_INFO_STORAGE_KEY, IMPORT_FILE_INFO_STORAGE_KEY } from './cleaning/constants';
  110. import type { PreviewFileDataset, TemplateListItem, TemplateTab, TreeFileInfo } from './cleaning/types';
  111. import { collectMatchedSheetsWithoutHeaderRow } from './cleaning/utils/headerRow';
  112. import { mockTemplateMap } from './cleaning/utils/mockData';
  113. import {
  114. hasRecognitionBasicInfoFilledContent,
  115. normalizeRecognitionBasicInfoText,
  116. parseRecognitionBasicInfoRows,
  117. resolveRecognitionBasicInfoTargetField,
  118. } from './cleaning/utils/recognition';
  119. import { sanitizeFileInfoTree, sanitizeSelectedRuleList } from './cleaning/utils/ruleSerialize';
  120. import {
  121. getTableRuleTargetKey,
  122. getTableRuleTargetLabel,
  123. normalizeTableRuleList,
  124. } from './cleaning/utils/tableRuleBuild';
  125. import {
  126. buildStoredLineNo,
  127. cloneDeepSafe,
  128. getRequestErrorMessage,
  129. normalizeFieldMatchText,
  130. normalizeRecognitionConfigText,
  131. normalizeRuleText,
  132. normalizeTemplateHeaders,
  133. normalizeTemplateId,
  134. } from './cleaning/utils/text';
  135. import {
  136. findFirstSelectableKey,
  137. findRootByNodeId,
  138. findTreeNodeByKey,
  139. isFileRootItem,
  140. isTemplateMatchedFailStatus,
  141. isTreeSheetNode,
  142. } from './cleaning/utils/treeUtils';
  143. import AddColumnModal from './cleaning/components/AddColumnModal.vue';
  144. import SplitColumnModal from './cleaning/components/SplitColumnModal.vue';
  145. import TemplateSelectModal from './cleaning/components/TemplateSelectModal.vue';
  146. import { CleaningContextKey } from './cleaning/context';
  147. import CleaningRulePanel from './cleaning/components/CleaningRulePanel.vue';
  148. import { useCleaningRules } from './cleaning/composables/useCleaningRules';
  149. import FieldMappingCard from './cleaning/components/FieldMappingCard.vue';
  150. import RecognitionRulePanel from './cleaning/components/RecognitionRulePanel.vue';
  151. import ChildTemplatePanel from './cleaning/components/ChildTemplatePanel.vue';
  152. import MiddleEmptyState from './cleaning/components/MiddleEmptyState.vue';
  153. import { useFieldMapping } from './cleaning/composables/useFieldMapping';
  154. import { useRecognitionRules } from './cleaning/composables/useRecognitionRules';
  155. import { useTemplateLibrary } from './cleaning/composables/useTemplateLibrary';
  156. import { useCaseDataCleaningShared } from './cleaning/composables/useCaseDataCleaningShared';
  157. import { importFileList } from '@/case/api/govern/governApi';
  158. import FileTreePanel from './cleaning/components/FileTreePanel.vue';
  159. import DataPreviewTable from './cleaning/components/DataPreviewTable.vue';
  160. import { useCaseDataCleaningActions } from './cleaning/composables/useCaseDataCleaningActions';
  161. import CaseFlowTopBar from './components/CaseFlowTopBar.vue';
  162. const { prefixCls } = useDesign('case-data-cleaning');
  163. const go = useGo();
  164. const pageQuery = useQuery();
  165. const { createMessage } = useMessage();
  166. const importingData = ref(false);
  167. const leftCollapsed = ref(false);
  168. const leftPanelWidth = ref(300);
  169. const middlePanelWidth = ref(520);
  170. const minLeftWidth = 220;
  171. const maxLeftWidth = 520;
  172. const minMiddleWidth = 360;
  173. const maxMiddleWidth = 900;
  174. const minRightWidth = 560;
  175. const resizeState = ref<{ type: 'left' | 'middle' | ''; startX: number; startLeft: number; startMiddle: number }>({
  176. type: '',
  177. startX: 0,
  178. startLeft: 300,
  179. startMiddle: 520,
  180. });
  181. const pendingClientX = ref<number | null>(null);
  182. const rafId = ref<number | null>(null);
  183. const layoutTemplateColumns = computed(() => {
  184. if (leftCollapsed.value) {
  185. return `${middlePanelWidth.value}px 6px minmax(${minRightWidth}px, 1fr)`;
  186. }
  187. return `${leftPanelWidth.value}px 6px ${middlePanelWidth.value}px 6px minmax(${minRightWidth}px, 1fr)`;
  188. });
  189. const resolveSelectableTreeKey = (key: string) => {
  190. const selectedNode = findTreeNodeByKey(treeData.value, key);
  191. if (!selectedNode) return '';
  192. if (selectedNode.selectable !== false) return selectedNode.key;
  193. return findFirstSelectableKey(selectedNode.children || []);
  194. };
  195. const switchNodeState = async (nextKey: string) => {
  196. if (!nextKey) return;
  197. if (activeNodeKey.value === nextKey) return;
  198. saveCurrentNodeState();
  199. setNodeAnalyzing(nextKey, true);
  200. activeNodeKey.value = nextKey;
  201. const currentNode = findTreeNodeByKey(treeData.value, nextKey)?.raw || null;
  202. structureSyncSuspended.value = true;
  203. try {
  204. const cached = nodeStateCache.get(nextKey);
  205. if (cached) {
  206. applyNodeCacheState(cached);
  207. await loadTemplateDetail(cached.selectedTemplateId, { silent: true, requestDetail: false });
  208. touchNodeCache(nextKey, cached);
  209. return;
  210. }
  211. resetNodeWorkState();
  212. const shouldLoadNodeTemplate = !isTemplateMatchedFailStatus(currentNode?.fileStatus);
  213. await loadTemplateDetail(shouldLoadNodeTemplate ? currentNode?.templateId : undefined, {
  214. silent: true,
  215. requestDetail: false,
  216. });
  217. await applyNodePreset(currentNode);
  218. const initialState = buildNodeCacheState();
  219. touchNodeCache(nextKey, initialState);
  220. if (!initialNodeStatusSignatureMap.has(nextKey)) {
  221. initialNodeStatusSignatureMap.set(nextKey, buildNodeStatusSignature(initialState));
  222. }
  223. } finally {
  224. structureSyncSuspended.value = false;
  225. syncActiveNodeStructure();
  226. setNodeAnalyzing(nextKey, false);
  227. }
  228. };
  229. const applyPreviewDataset = (dataset: PreviewFileDataset) => {
  230. rightTableHeaders.value = [...dataset.headers];
  231. addedColumns.value = [];
  232. generatedColumnHeaderRowMap.value = {};
  233. selectedAddedColumn.value = '';
  234. sourceTableRows.value = dataset.rows.length ? dataset.rows : [];
  235. tablePreviewRows.value = cloneDeepSafe(sourceTableRows.value);
  236. return true;
  237. };
  238. const syncNodeStructure = (node?: TreeFileInfo | null) => {
  239. if (!isTreeSheetNode(node)) return;
  240. const templateId = normalizeTemplateId(
  241. templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value ?? node.templateId,
  242. );
  243. const mainId = getSelectedTemplateMainId(node.mainId);
  244. const tableName =
  245. normalizeRuleText(templateDetail.value?.tableNameCn) ||
  246. normalizeRuleText(activeTemplateItem.value?.tableNameCn) ||
  247. normalizeRuleText(node.tableName);
  248. const baseHeaders = rightTableHeaders.value.filter((header) => !addedColumns.value.includes(header));
  249. const existingHeaders = normalizeTemplateHeaders(node.header);
  250. const hasRealHeaderConfig =
  251. headerRowIndex.value !== null ||
  252. !!existingHeaders.length ||
  253. addedColumns.value.length > 0 ||
  254. Object.values(effectiveFieldMapping.value).some(Boolean) ||
  255. baseHeaders.length > 0;
  256. const shouldPersistRecognitionBasicInfo = hasRecognitionBasicInfoFilledContent(recognitionBasicInfoRows.value);
  257. const ignoreLines = Array.from(ignoredRowsSet.value).sort((a, b) => a - b);
  258. const groupColumns = buildEnabledGroupingColumnsPayload();
  259. node.templateId = templateId || node.templateId || null;
  260. node.mainId = mainId || null;
  261. node.tableName = tableName || node.tableName || null;
  262. if (headerRowIndex.value !== null) {
  263. node.lineNo = buildStoredLineNo(headerRowIndex.value);
  264. }
  265. if (hasRealHeaderConfig) {
  266. node.header = baseHeaders.length ? [...baseHeaders] : [];
  267. }
  268. node.ignoreLines = ignoreLines;
  269. node.groupColumns = groupColumns;
  270. node.tableRuleList = buildTableRuleListFromState(node.tableRuleList);
  271. node.childTemplateName = normalizeRuleText(childTemplateName.value) || null;
  272. node.hasNew = !!node.childTemplateName;
  273. if (shouldPersistRecognitionBasicInfo) {
  274. node.funcRegx = buildRecognitionBasicInfoFuncRegx();
  275. } else if (Object.prototype.hasOwnProperty.call(node, 'funcRegx')) {
  276. node.funcRegx = null;
  277. }
  278. persistFileListToSessionStorage();
  279. };
  280. const hasMatchedTargetField = (header: string) => !!matchedTargetFieldsByHeader.value[header]?.length;
  281. const getColumnRuleBadgeState = (colIndex: number) => {
  282. if (colIndex < 0) return false;
  283. const rules = sanitizeSelectedRuleList(selectRules.value[colIndex] || []);
  284. const hasEnabledRules = rules.some((rule) => rule && rule.key && rule.enabled);
  285. if (hasEnabledRules) return 'active';
  286. return rules.some((rule) => rule && rule.key) ? 'draft' : '';
  287. };
  288. const isTargetHandledByRecognitionRule = (targetKey: string) =>
  289. recognitionRuleHandledTargetKeySet.value.has(targetKey);
  290. const requiredTotal = computed(() => fixedTargetFields.value.filter((item) => isMappingTargetRequired(item)).length);
  291. const DEFAULT_TEMPLATE_CATEGORY = '全部分类';
  292. const templateModalOpen = ref(false);
  293. const activeTemplateTab = ref<TemplateTab>('template');
  294. const activeCategory = ref(DEFAULT_TEMPLATE_CATEGORY);
  295. const templateKeyword = ref('');
  296. const templateSource = ref<number>(1);
  297. const modalTemplateDetail = ref<TableInfo | null>(null);
  298. const templatePreviewLoading = ref(false);
  299. let templatePreviewRequestToken = 0;
  300. const categories = computed(() => {
  301. const set = new Set<string>(
  302. templateCategoryOptions.value.length ? templateCategoryOptions.value : [DEFAULT_TEMPLATE_CATEGORY],
  303. );
  304. templateList.value.forEach((t) => set.add(t.classify || FALLBACK_TEMPLATE_CATEGORY));
  305. return Array.from(set);
  306. });
  307. const showTemplateTabSwitch = computed(
  308. () => activeCategory.value === DEFAULT_TEMPLATE_CATEGORY || pluginCategorySet.value.has(activeCategory.value),
  309. );
  310. const syncModalTemplateItem = (preferredId?: string | number | null) => {
  311. const currentBaseList = filteredTemplateList.value;
  312. if (activeTemplateTab.value === 'plugin') {
  313. const currentPluginId = modalTemplateItem.value?.id;
  314. const matchedPlugin = currentBaseList.find((item) => String(item.id) === String(currentPluginId)) || null;
  315. modalTemplateItem.value = matchedPlugin;
  316. return modalTemplateItem.value;
  317. }
  318. const preferredTemplateId = normalizeTemplateId(preferredId);
  319. const currentTemplateId = normalizeTemplateId(modalTemplateItem.value?.id);
  320. if (preferredTemplateId) {
  321. const matched = currentBaseList.find((item) => normalizeTemplateId(item.id) === preferredTemplateId) || null;
  322. if (matched) {
  323. modalTemplateItem.value = matched;
  324. return matched;
  325. }
  326. }
  327. if (currentTemplateId) {
  328. const currentMatched = currentBaseList.find((item) => normalizeTemplateId(item.id) === currentTemplateId) || null;
  329. if (currentMatched) {
  330. modalTemplateItem.value = currentMatched;
  331. return currentMatched;
  332. }
  333. }
  334. modalTemplateItem.value = currentBaseList[0] || null;
  335. return modalTemplateItem.value;
  336. };
  337. const resolveTemplateInfoById = (templateId?: string | number | null) => {
  338. const id = normalizeTemplateId(templateId);
  339. if (!id) return null;
  340. const activeDetail = normalizeTemplateId(templateDetail.value?.id) === id ? templateDetail.value : null;
  341. const activeItem = normalizeTemplateId(activeTemplateItem.value?.id) === id ? activeTemplateItem.value : null;
  342. const cachedDetail = templateDetailCache.get(id) || null;
  343. const listItem = getTemplateListItemById(id) || mockTemplateMap[id] || null;
  344. return (
  345. mergeTemplateInfo(activeItem || listItem || activeDetail, activeDetail || cachedDetail) ||
  346. activeDetail ||
  347. cachedDetail ||
  348. activeItem ||
  349. listItem ||
  350. null
  351. );
  352. };
  353. const syncModalTemplatePreviewState = (
  354. template?: TemplateListItem | null,
  355. options: { preferAppliedDetail?: boolean } = {},
  356. ) => {
  357. modalTemplateItem.value = template || null;
  358. modalTemplateDetail.value = null;
  359. templatePreviewLoading.value = false;
  360. templatePreviewRequestToken += 1;
  361. const id = normalizeTemplateId(template?.id);
  362. if (!id || Number(template?.id) < 0) return null;
  363. if (options.preferAppliedDetail && normalizeTemplateId(templateDetail.value?.id) === id && templateDetail.value) {
  364. modalTemplateDetail.value = templateDetail.value;
  365. return modalTemplateDetail.value;
  366. }
  367. const cachedDetail = templateDetailCache.get(id);
  368. if (cachedDetail) {
  369. modalTemplateDetail.value = cachedDetail;
  370. return modalTemplateDetail.value;
  371. }
  372. return null;
  373. };
  374. const loadTemplatePreviewDetail = async (
  375. template?: TemplateListItem | null,
  376. options: { preferAppliedDetail?: boolean } = {},
  377. ) => {
  378. const existingDetail = syncModalTemplatePreviewState(template, options);
  379. if (existingDetail) {
  380. return existingDetail;
  381. }
  382. const id = normalizeTemplateId(template?.id);
  383. if (!id || Number(template?.id) < 0) return null;
  384. const requestToken = ++templatePreviewRequestToken;
  385. templatePreviewLoading.value = true;
  386. try {
  387. const detail = await tplDetail({ id });
  388. if (requestToken !== templatePreviewRequestToken) return null;
  389. if (detail) {
  390. templateDetailCache.set(id, detail);
  391. modalTemplateDetail.value = detail;
  392. mergeTemplateListItem(mergeTemplateInfo(template, detail));
  393. modalTemplateItem.value = getTemplateListItemById(id) || template || null;
  394. }
  395. return modalTemplateDetail.value;
  396. } catch (error) {
  397. if (requestToken === templatePreviewRequestToken) {
  398. modalTemplateDetail.value = null;
  399. }
  400. return null;
  401. } finally {
  402. if (requestToken === templatePreviewRequestToken) {
  403. templatePreviewLoading.value = false;
  404. }
  405. }
  406. };
  407. const closeTemplateModal = () => {
  408. templateModalOpen.value = false;
  409. templatePreviewRequestToken += 1;
  410. templatePreviewLoading.value = false;
  411. modalTemplateDetail.value = null;
  412. modalTemplateItem.value = null;
  413. };
  414. watch([activeCategory, templateSource, templateKeyword, activeTemplateTab], async () => {
  415. if (!templateModalOpen.value) return;
  416. if (!showTemplateTabSwitch.value) {
  417. activeTemplateTab.value = 'template';
  418. }
  419. const previewTarget = syncModalTemplateItem(
  420. activeTemplateTab.value === 'plugin' ? undefined : selectedTemplateId.value,
  421. );
  422. await loadTemplatePreviewDetail(previewTarget, {
  423. preferAppliedDetail:
  424. activeTemplateTab.value !== 'plugin' && normalizeTemplateId(previewTarget?.id) === selectedTemplateId.value,
  425. });
  426. });
  427. watch(activeTemplateTab, (tab) => {
  428. if (!templateModalOpen.value) return;
  429. if (tab === 'plugin') {
  430. modalTemplateDetail.value = null;
  431. templatePreviewLoading.value = false;
  432. templatePreviewRequestToken += 1;
  433. }
  434. });
  435. const activeNodeInfo = computed(() => {
  436. return activeNodeKey.value ? findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw || null : null;
  437. });
  438. // Phase 5:底层共享状态收进唯一 store。页面同名解构接住,原有代码与模板零改动。
  439. const caseDataCleaningShared = useCaseDataCleaningShared({
  440. activeNodeInfo,
  441. applyPreviewDataset,
  442. createMessage,
  443. getColumnRuleBadgeState,
  444. hasMatchedTargetField,
  445. pageQuery,
  446. requiredTotal,
  447. resolveSelectableTreeKey,
  448. resolveTemplateInfoById,
  449. switchNodeState,
  450. syncNodeStructure,
  451. });
  452. const {
  453. addColumnDialogOpen,
  454. addColumnForm,
  455. addColumnMode,
  456. addedColumns,
  457. analyzingNodeKeys,
  458. applyPresetHeadersFromNode,
  459. availableSourceHeaders,
  460. buildRecognitionDirectionConf,
  461. childTemplateName,
  462. collectChildTemplateNames,
  463. currentColumn,
  464. deleteColumnDialogOpen,
  465. dragOverContentIndex,
  466. draggingContentIndex,
  467. findDuplicateChildTemplateNames,
  468. getColumnRuleBadgeTitle,
  469. getFocusedNodeKeyFromQuery,
  470. getMatchedHeaderStatusClass,
  471. getMatchedHeaderStatusText,
  472. getMatchedHeaderStatusTitle,
  473. handleSelect,
  474. hasDuplicateHeaderName,
  475. headerRowIndex,
  476. initialNodeStatusSignatureMap,
  477. isMatchSuccessful,
  478. isSheetMatchComplete,
  479. loadPreviewDatasetFromFile,
  480. mappingPopoverOpenKey,
  481. mockGridTemplate,
  482. openAddColumn,
  483. openDeleteColumn,
  484. openSplitColumn,
  485. originFilterKeyword,
  486. recognitionBasicInfoBaseFuncRegx,
  487. recognitionBasicInfoManualVisible,
  488. remapGroupingColumnRules,
  489. remapSelectRuleHeaderRefs,
  490. resolveParentTemplateInfoById,
  491. rightTableHeaders,
  492. selectedAddedColumn,
  493. selectedContentIndex,
  494. selectedKeys,
  495. selectedRowIndex,
  496. selectedSourceField,
  497. setNodeAnalyzing,
  498. showDuplicateChildTemplateWarning,
  499. sourceTableRows,
  500. splitColumnDialogOpen,
  501. splitColumnForm,
  502. structureSyncSuspended,
  503. syncActiveNodeStructure,
  504. tablePreviewRows,
  505. } = caseDataCleaningShared;
  506. // 既有域:cleaningRules
  507. const cleaningRules = useCleaningRules({
  508. addedColumns,
  509. createMessage,
  510. currentColumn,
  511. headerRowIndex,
  512. rightTableHeaders,
  513. sourceTableRows,
  514. tablePreviewRows,
  515. });
  516. const {
  517. absoluteValueChecked,
  518. activeNodeKey,
  519. activeNodeTableRuleMap,
  520. activeRules,
  521. activeTemplateItem,
  522. addedColumnConfig,
  523. applyTableRuleRequiredFlags,
  524. buildAmountZfHandledTargetKeySet,
  525. buildEffectiveFieldMapping,
  526. buildGeneratedColumnHeaderRowMap,
  527. buildRecognitionBasicInfoMatchedTargetKeySet,
  528. currentRuleTitle,
  529. dateConfigType,
  530. decimalEnabled,
  531. decimalSourceBase,
  532. decimalTargetBase,
  533. effectiveFieldMapping,
  534. extractDataAfter,
  535. extractDataBefore,
  536. extractDataMode,
  537. extractSegmentRow,
  538. extractSegmentValue,
  539. fieldMapping,
  540. fileList,
  541. fixedTargetFields,
  542. generatedColumnHeaderRowMap,
  543. getEnabledGroupingColumnsForHeader,
  544. getGeneratedColumnHeaderRowIndex,
  545. groupingColumnsDraft,
  546. groupingEnabled,
  547. ignoredRowsSet,
  548. magnificationValue,
  549. matchedTargetFieldsByHeader,
  550. omitLineInput,
  551. rebuildGeneratedColumnsInSourceRows,
  552. recognitionAmountZfEnabled,
  553. recognitionBasicInfoMatchedTargetKeySet,
  554. recognitionBasicInfoRows,
  555. recognitionDirectionFields,
  556. recognitionRuleHandledTargetKeySet,
  557. removeCustomChecked,
  558. removeCustomText,
  559. removeSpaceChecked,
  560. removeTabChecked,
  561. replaceEnabled,
  562. replaceMode,
  563. replaceSource,
  564. replaceTarget,
  565. resolveTemplateFieldMeta,
  566. runRulePipeline,
  567. selectRules,
  568. selectedRules,
  569. selectedTemplateId,
  570. setGeneratedColumnCell,
  571. sortMode,
  572. splitColumnConfig,
  573. syncExtractDataMode,
  574. templateDetail,
  575. treeData,
  576. } = cleaningRules;
  577. // 既有域:recognitionApi
  578. const recognitionApi = useRecognitionRules({
  579. absoluteValueChecked,
  580. activeNodeKey,
  581. activeRules,
  582. addedColumnConfig,
  583. addedColumns,
  584. buildGeneratedColumnHeaderRowMap,
  585. childTemplateName,
  586. createMessage,
  587. currentColumn,
  588. currentRuleTitle,
  589. dateConfigType,
  590. decimalEnabled,
  591. decimalSourceBase,
  592. decimalTargetBase,
  593. extractDataAfter,
  594. extractDataBefore,
  595. extractSegmentRow,
  596. extractSegmentValue,
  597. fieldMapping,
  598. fixedTargetFields,
  599. generatedColumnHeaderRowMap,
  600. groupingColumnsDraft,
  601. groupingEnabled,
  602. headerRowIndex,
  603. magnificationValue,
  604. mappingPopoverOpenKey,
  605. omitLineInput,
  606. originFilterKeyword,
  607. recognitionAmountZfEnabled,
  608. recognitionBasicInfoBaseFuncRegx,
  609. recognitionBasicInfoManualVisible,
  610. recognitionBasicInfoRows,
  611. recognitionDirectionFields,
  612. removeCustomChecked,
  613. removeCustomText,
  614. removeSpaceChecked,
  615. removeTabChecked,
  616. replaceEnabled,
  617. replaceMode,
  618. replaceSource,
  619. replaceTarget,
  620. rightTableHeaders,
  621. runRulePipeline,
  622. selectRules,
  623. selectedAddedColumn,
  624. selectedRowIndex,
  625. selectedRules,
  626. selectedTemplateId,
  627. sourceTableRows,
  628. splitColumnConfig,
  629. syncActiveNodeStructure,
  630. });
  631. const {
  632. activeRecognitionRule,
  633. buildNodeCacheState,
  634. headerRowInput,
  635. nodeStateCache,
  636. recognitionBasicInfoExpanded,
  637. recognitionPanelExpanded,
  638. recognitionRuleTabs,
  639. saveCurrentNodeState,
  640. showRecognitionPanel,
  641. touchNodeCache,
  642. } = recognitionApi;
  643. // 既有域:templateApi
  644. const templateApi = useTemplateLibrary({
  645. DEFAULT_TEMPLATE_CATEGORY,
  646. activeCategory,
  647. activeNodeInfo,
  648. activeTemplateItem,
  649. activeTemplateTab,
  650. addedColumnConfig,
  651. addedColumns,
  652. childTemplateName,
  653. createMessage,
  654. fieldMapping,
  655. headerRowIndex,
  656. recognitionAmountZfEnabled,
  657. recognitionBasicInfoBaseFuncRegx,
  658. recognitionBasicInfoManualVisible,
  659. recognitionBasicInfoRows,
  660. recognitionDirectionFields,
  661. rightTableHeaders,
  662. selectRules,
  663. selectedRules,
  664. selectedTemplateId,
  665. splitColumnConfig,
  666. syncActiveNodeStructure,
  667. templateDetail,
  668. templateKeyword,
  669. templateSource,
  670. });
  671. const {
  672. FALLBACK_TEMPLATE_CATEGORY,
  673. checkTemplateNameAvailable,
  674. currentTemplateDisplayName,
  675. currentTemplateName,
  676. filteredTemplateList,
  677. getTemplateListItemById,
  678. hasTemplateSelected,
  679. isSystemTemplateRecord,
  680. loadTemplateCategories,
  681. loadTemplateList,
  682. mergeTemplateInfo,
  683. mergeTemplateListItem,
  684. modalTemplateItem,
  685. pluginCategorySet,
  686. showChildTemplateExistsWarning,
  687. syncSelectedTemplateState,
  688. templateCategoryOptions,
  689. templateDetailCache,
  690. templateDetailFromMock,
  691. templateList,
  692. templateListLoaded,
  693. templateListLoading,
  694. templateSearchBaseList,
  695. } = templateApi;
  696. // categories 依赖 templateApi 解构出的 ref,必须在解构之后注册,否则 setup 期首次求值触发 TDZ 报错
  697. watch(categories, (nextCategories) => {
  698. if (!nextCategories.includes(activeCategory.value)) {
  699. activeCategory.value = DEFAULT_TEMPLATE_CATEGORY;
  700. }
  701. });
  702. // 既有域:mappingApi
  703. const mappingApi = useFieldMapping({
  704. activeNodeInfo,
  705. activeNodeKey,
  706. activeNodeTableRuleMap,
  707. activeTemplateItem,
  708. addedColumns,
  709. buildRecognitionDirectionConf,
  710. createMessage,
  711. currentColumn,
  712. effectiveFieldMapping,
  713. fieldMapping,
  714. fixedTargetFields,
  715. headerRowIndex,
  716. ignoredRowsSet,
  717. isTargetHandledByRecognitionRule,
  718. mappingPopoverOpenKey,
  719. matchedTargetFieldsByHeader,
  720. originFilterKeyword,
  721. recognitionDirectionFields,
  722. resolveTemplateFieldMeta,
  723. rightTableHeaders,
  724. selectedAddedColumn,
  725. selectedRowIndex,
  726. sourceTableRows,
  727. tablePreviewRows,
  728. templateDetail,
  729. treeData,
  730. });
  731. const {
  732. activeMappingTargetKey,
  733. autoMatchFields,
  734. clearOriginHoverPreviewState,
  735. focusPreviewColumn,
  736. headerFormatValidationMap,
  737. isMappingTargetRequired,
  738. isTargetMatched,
  739. resolveRecognitionDirectionFields,
  740. tableMockRef,
  741. } = mappingApi;
  742. // 待人工确认工作清单(智能清洗入口):清单数据、单文件定位、仅清洗当前表模式
  743. const loadFallbackTree = async (): Promise<TreeFileInfo[]> => {
  744. const focusedKey = getFocusedNodeKeyFromQuery();
  745. const batchId = String(pageQuery.value?.batchId || '');
  746. if (!focusedKey && !batchId) return [];
  747. try {
  748. const roots = (await importFileList(batchId ? ({ batchId } as any) : {})) as unknown as TreeFileInfo[];
  749. const list = Array.isArray(roots) ? roots : [];
  750. if (!list.length) return [];
  751. if (focusedKey) {
  752. const root = findRootByNodeId(list, focusedKey);
  753. // 只装 focusKey 所属文件:清洗页保持「单文件树」语义(与重新清洗入口一致),
  754. // 也避免「导入数据」把全案件的表一起洗掉
  755. return root ? sanitizeFileInfoTree(cloneDeepSafe([root])) : [];
  756. }
  757. return sanitizeFileInfoTree(cloneDeepSafe(list));
  758. } catch (error) {
  759. return [];
  760. }
  761. };
  762. // Phase 5:页面动作域。页面同名解构接住,原有代码与模板零改动。
  763. const cleaningActions = useCaseDataCleaningActions({
  764. loadFallbackTree,
  765. DEFAULT_TEMPLATE_CATEGORY,
  766. absoluteValueChecked,
  767. activeCategory,
  768. activeMappingTargetKey,
  769. activeNodeInfo,
  770. activeNodeKey,
  771. activeRecognitionRule,
  772. activeRules,
  773. activeTemplateItem,
  774. activeTemplateTab,
  775. addColumnDialogOpen,
  776. addColumnForm,
  777. addColumnMode,
  778. addedColumnConfig,
  779. addedColumns,
  780. analyzingNodeKeys,
  781. applyPresetHeadersFromNode,
  782. applyPreviewDataset,
  783. applyTableRuleRequiredFlags,
  784. autoMatchFields,
  785. availableSourceHeaders,
  786. buildAmountZfHandledTargetKeySet,
  787. buildEffectiveFieldMapping,
  788. buildGeneratedColumnHeaderRowMap,
  789. buildNodeCacheState,
  790. buildRecognitionBasicInfoMatchedTargetKeySet,
  791. buildRecognitionDirectionConf,
  792. checkTemplateNameAvailable,
  793. childTemplateName,
  794. clearOriginHoverPreviewState,
  795. collectChildTemplateNames,
  796. createMessage,
  797. currentColumn,
  798. currentRuleTitle,
  799. currentTemplateName,
  800. dateConfigType,
  801. decimalEnabled,
  802. decimalSourceBase,
  803. decimalTargetBase,
  804. deleteColumnDialogOpen,
  805. dragOverContentIndex,
  806. draggingContentIndex,
  807. effectiveFieldMapping,
  808. extractDataAfter,
  809. extractDataBefore,
  810. extractDataMode,
  811. extractSegmentRow,
  812. extractSegmentValue,
  813. fieldMapping,
  814. fileList,
  815. findDuplicateChildTemplateNames,
  816. fixedTargetFields,
  817. focusPreviewColumn,
  818. generatedColumnHeaderRowMap,
  819. getEnabledGroupingColumnsForHeader,
  820. getFocusedNodeKeyFromQuery,
  821. getGeneratedColumnHeaderRowIndex,
  822. getTemplateListItemById,
  823. groupingColumnsDraft,
  824. groupingEnabled,
  825. headerFormatValidationMap,
  826. headerRowIndex,
  827. headerRowInput,
  828. initialNodeStatusSignatureMap,
  829. isMappingTargetRequired,
  830. isMatchSuccessful,
  831. isSheetMatchComplete,
  832. isTargetHandledByRecognitionRule,
  833. isTargetMatched,
  834. loadPreviewDatasetFromFile,
  835. loadTemplateCategories,
  836. loadTemplateList,
  837. loadTemplatePreviewDetail,
  838. magnificationValue,
  839. mappingPopoverOpenKey,
  840. matchedTargetFieldsByHeader,
  841. mergeTemplateInfo,
  842. mergeTemplateListItem,
  843. nodeStateCache,
  844. omitLineInput,
  845. originFilterKeyword,
  846. rebuildGeneratedColumnsInSourceRows,
  847. recognitionAmountZfEnabled,
  848. recognitionBasicInfoBaseFuncRegx,
  849. recognitionBasicInfoExpanded,
  850. recognitionBasicInfoManualVisible,
  851. recognitionBasicInfoMatchedTargetKeySet,
  852. recognitionBasicInfoRows,
  853. recognitionDirectionFields,
  854. recognitionRuleTabs,
  855. remapGroupingColumnRules,
  856. remapSelectRuleHeaderRefs,
  857. removeCustomChecked,
  858. removeCustomText,
  859. removeSpaceChecked,
  860. removeTabChecked,
  861. replaceEnabled,
  862. replaceMode,
  863. replaceSource,
  864. replaceTarget,
  865. requiredTotal,
  866. resolveParentTemplateInfoById,
  867. resolveRecognitionDirectionFields,
  868. resolveSelectableTreeKey,
  869. resolveTemplateFieldMeta,
  870. resolveTemplateInfoById,
  871. rightTableHeaders,
  872. runRulePipeline,
  873. selectRules,
  874. selectedAddedColumn,
  875. selectedContentIndex,
  876. selectedKeys,
  877. selectedRowIndex,
  878. selectedRules,
  879. selectedSourceField,
  880. selectedTemplateId,
  881. showChildTemplateExistsWarning,
  882. showDuplicateChildTemplateWarning,
  883. sourceTableRows,
  884. splitColumnConfig,
  885. splitColumnForm,
  886. switchNodeState,
  887. syncExtractDataMode,
  888. syncModalTemplateItem,
  889. syncSelectedTemplateState,
  890. templateDetail,
  891. templateDetailCache,
  892. templateDetailFromMock,
  893. templateKeyword,
  894. templateListLoaded,
  895. templateListLoading,
  896. templateModalOpen,
  897. templateSource,
  898. treeData,
  899. });
  900. const {
  901. applyHeaderRow,
  902. applyHeaderRowInput,
  903. applyNodeCacheState,
  904. applyNodePreset,
  905. applyRecognitionAmountZfState,
  906. applyRecognitionBasicInfoState,
  907. applyRecognitionDirectionState,
  908. buildEnabledGroupingColumnsPayload,
  909. buildNodeStatusSignature,
  910. buildRecognitionBasicInfoFuncRegx,
  911. buildTableRuleListFromState,
  912. confirmDeleteColumn,
  913. ensureChildTemplateNamesAvailable,
  914. getHeaderFormatErrorTitle,
  915. getMatchedTargetFieldText,
  916. getSelectedTemplateMainId,
  917. getSheetNodeVisualState,
  918. hasHeaderFormatError,
  919. initializePage,
  920. loadTemplateDetail,
  921. onClickCell,
  922. onClickHeader,
  923. openEditColumn,
  924. openTemplateModal,
  925. persistFileListToSessionStorage,
  926. resetNodeWorkState,
  927. syncHeaderRowInput,
  928. } = cleaningActions;
  929. watch(
  930. recognitionRuleTabs,
  931. (tabs) => {
  932. if (!tabs.length) {
  933. activeRecognitionRule.value = '';
  934. return;
  935. }
  936. if (!tabs.includes(activeRecognitionRule.value)) {
  937. activeRecognitionRule.value = tabs[0];
  938. }
  939. },
  940. { immediate: true },
  941. );
  942. watch(
  943. fixedTargetFields,
  944. (fields) => {
  945. if (activeMappingTargetKey.value && !fields.some((item) => item.key === activeMappingTargetKey.value)) {
  946. activeMappingTargetKey.value = '';
  947. }
  948. },
  949. { deep: true },
  950. );
  951. const startResize = (type: 'left' | 'middle', event: MouseEvent) => {
  952. event.preventDefault();
  953. resizeState.value = {
  954. type,
  955. startX: event.clientX,
  956. startLeft: leftPanelWidth.value,
  957. startMiddle: middlePanelWidth.value,
  958. };
  959. document.body.classList.add('is-resizing-layout');
  960. document.body.style.cursor = 'col-resize';
  961. };
  962. const applyResize = () => {
  963. if (!resizeState.value.type || pendingClientX.value === null) return;
  964. const deltaX = pendingClientX.value - resizeState.value.startX;
  965. if (resizeState.value.type === 'left' && !leftCollapsed.value) {
  966. const next = resizeState.value.startLeft + deltaX;
  967. leftPanelWidth.value = Math.min(maxLeftWidth, Math.max(minLeftWidth, next));
  968. }
  969. if (resizeState.value.type === 'middle') {
  970. const next = resizeState.value.startMiddle + deltaX;
  971. middlePanelWidth.value = Math.min(maxMiddleWidth, Math.max(minMiddleWidth, next));
  972. }
  973. rafId.value = null;
  974. };
  975. const onResizeMouseMove = (event: MouseEvent) => {
  976. if (!resizeState.value.type) return;
  977. event.preventDefault();
  978. pendingClientX.value = event.clientX;
  979. if (rafId.value === null) {
  980. rafId.value = window.requestAnimationFrame(applyResize);
  981. }
  982. };
  983. const stopResize = () => {
  984. resizeState.value.type = '';
  985. resizeState.value.startX = 0;
  986. pendingClientX.value = null;
  987. if (rafId.value !== null) {
  988. window.cancelAnimationFrame(rafId.value);
  989. rafId.value = null;
  990. }
  991. document.body.classList.remove('is-resizing-layout');
  992. document.body.style.cursor = '';
  993. };
  994. const toggleLeft = () => {
  995. leftCollapsed.value = !leftCollapsed.value;
  996. };
  997. const handleImportData = async () => {
  998. saveCurrentNodeState();
  999. syncActiveNodeStructure();
  1000. const sanitizedFileInfos = sanitizeFileInfoTree(fileList.value, { keepRuleKey: false });
  1001. if (!sanitizedFileInfos.length) {
  1002. createMessage.warning('未找到可导入的数据,请先上传并完成模板匹配');
  1003. return;
  1004. }
  1005. // 只有文件根、没有任何工作表时,后端不会产生清洗任务;不要跳到进度页
  1006. if (!sanitizedFileInfos.some((item) => (item.children || []).length > 0)) {
  1007. createMessage.warning('未找到可清洗文件,请先上传并匹配文件');
  1008. return;
  1009. }
  1010. const matchedSheetsWithoutHeaderRow = collectMatchedSheetsWithoutHeaderRow(sanitizedFileInfos);
  1011. if (matchedSheetsWithoutHeaderRow.length) {
  1012. const previewNames = matchedSheetsWithoutHeaderRow.slice(0, 3).join('、');
  1013. const suffix =
  1014. matchedSheetsWithoutHeaderRow.length > 3 ? ` 等 ${matchedSheetsWithoutHeaderRow.length} 个工作表` : '';
  1015. createMessage.warning(`已匹配模板的工作表必须先填写表头所在行:${previewNames}${suffix}`);
  1016. return;
  1017. }
  1018. try {
  1019. const childTemplateNameValid = await ensureChildTemplateNamesAvailable(sanitizedFileInfos);
  1020. if (!childTemplateNameValid) {
  1021. return;
  1022. }
  1023. } catch (error) {
  1024. createMessage.error(getRequestErrorMessage(error, '模板名称校验失败'));
  1025. return;
  1026. }
  1027. sessionStorage.setItem(FILE_INFO_STORAGE_KEY, JSON.stringify(sanitizedFileInfos));
  1028. importingData.value = true;
  1029. try {
  1030. const resolveMappedHeaderByTargetField = (item?: TreeFileInfo | null, targetField?: string) => {
  1031. const normalizedTarget = normalizeFieldMatchText(targetField);
  1032. if (!normalizedTarget) return '';
  1033. const matchedRule = normalizeTableRuleList(item?.tableRuleList).find((rule, index) => {
  1034. const targetKey = normalizeFieldMatchText(getTableRuleTargetKey(rule, index));
  1035. const targetLabel = normalizeFieldMatchText(getTableRuleTargetLabel(rule, index));
  1036. return normalizedTarget === targetKey || normalizedTarget === targetLabel;
  1037. });
  1038. return normalizeRecognitionConfigText(matchedRule?.fileColName);
  1039. };
  1040. const buildImportRecognitionBasicInfoFuncRegx = (item?: TreeFileInfo | null) => {
  1041. const source = normalizeRecognitionConfigText(item?.funcRegx);
  1042. if (!source) return '';
  1043. const rows = parseRecognitionBasicInfoRows(source);
  1044. if (!rows.length) return source;
  1045. if (!hasRecognitionBasicInfoFilledContent(rows)) return '';
  1046. const payload = rows.reduce(
  1047. (acc, row) => {
  1048. const targetField = resolveRecognitionBasicInfoTargetField(row.targetField);
  1049. const headerMatch = normalizeRecognitionBasicInfoText(row.headerMatch);
  1050. const endFlag = normalizeRecognitionBasicInfoText(row.endFlag);
  1051. const mappedHeader = resolveMappedHeaderByTargetField(item, targetField);
  1052. const nextKey = headerMatch || mappedHeader || targetField;
  1053. if (!nextKey) return acc;
  1054. acc[nextKey] = [headerMatch, endFlag];
  1055. return acc;
  1056. },
  1057. {} as Record<string, string[]>,
  1058. );
  1059. return Object.keys(payload).length ? JSON.stringify(payload) : '';
  1060. };
  1061. const buildImportFileInfoPayload = (item?: TreeFileInfo | null): TreeFileInfo | null => {
  1062. if (!item) return null;
  1063. const nextItem = cloneDeepSafe(item) as TreeFileInfo;
  1064. const children = Array.isArray(nextItem.children)
  1065. ? nextItem.children
  1066. .map((child) => buildImportFileInfoPayload(child))
  1067. .filter((child): child is TreeFileInfo => Boolean(child))
  1068. : nextItem.children;
  1069. if (isFileRootItem(nextItem)) {
  1070. const rootPayload = {
  1071. ...nextItem,
  1072. children,
  1073. } as TreeFileInfo & { childTemplateName?: string | null };
  1074. delete rootPayload.childTemplateName;
  1075. return rootPayload;
  1076. }
  1077. const childName = normalizeRuleText(nextItem.childTemplateName);
  1078. const hasNew = !!childName || !!nextItem.hasNew;
  1079. const importTemplateId = normalizeTemplateId(
  1080. nextItem.templateId ?? templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
  1081. );
  1082. const currentTemplate = resolveTemplateInfoById(importTemplateId);
  1083. const parentTemplateId = normalizeTemplateId(nextItem.mainId) || normalizeTemplateId(currentTemplate?.mainId);
  1084. const parentTemplate = (parentTemplateId && resolveTemplateInfoById(parentTemplateId)) || currentTemplate;
  1085. const currentFuncRegx = normalizeRecognitionConfigText(nextItem.funcRegx);
  1086. const importFuncRegx = currentFuncRegx ? buildImportRecognitionBasicInfoFuncRegx(nextItem) : '';
  1087. const sheetPayload = {
  1088. ...nextItem,
  1089. tableName: hasNew ? childName : nextItem.tableName,
  1090. tableNameEn: hasNew
  1091. ? normalizeRuleText(parentTemplate?.tableNameEn) ||
  1092. normalizeRuleText(currentTemplate?.tableNameEn) ||
  1093. normalizeRuleText(nextItem.tableNameEn) ||
  1094. null
  1095. : nextItem.tableNameEn,
  1096. hasNew,
  1097. classify: hasNew
  1098. ? normalizeRuleText(parentTemplate?.classify) ||
  1099. normalizeRuleText(currentTemplate?.classify) ||
  1100. normalizeRuleText(nextItem.classify) ||
  1101. null
  1102. : nextItem.classify,
  1103. funcRegx: importFuncRegx || null,
  1104. children,
  1105. } as TreeFileInfo & { childTemplateName?: string | null };
  1106. return sheetPayload;
  1107. };
  1108. const importFileInfos = sanitizedFileInfos
  1109. .map((item) => buildImportFileInfoPayload(item))
  1110. .filter((item): item is TreeFileInfo => Boolean(item));
  1111. sessionStorage.setItem(IMPORT_FILE_INFO_STORAGE_KEY, JSON.stringify(importFileInfos));
  1112. resetImportProgressSession();
  1113. await go(PageEnum.BASE_CLEAN_PROGRESS);
  1114. } catch {
  1115. createMessage.error('导入数据失败');
  1116. } finally {
  1117. importingData.value = false;
  1118. }
  1119. };
  1120. const handleBack = () => {
  1121. // 返回上一级:文件上传页;带上来时的来源地址,保证再返回仍能回到原界面(如工作台)
  1122. const redirect = String(pageQuery.value.redirect || '');
  1123. go(redirect ? { path: PageEnum.BASE_UPLOAD, query: { redirect } } : PageEnum.BASE_UPLOAD);
  1124. };
  1125. onMounted(() => {
  1126. initializePage();
  1127. window.addEventListener('mousemove', onResizeMouseMove);
  1128. window.addEventListener('mouseup', stopResize);
  1129. });
  1130. onBeforeUnmount(() => {
  1131. saveCurrentNodeState();
  1132. window.removeEventListener('mousemove', onResizeMouseMove);
  1133. window.removeEventListener('mouseup', stopResize);
  1134. document.body.classList.remove('is-resizing-layout');
  1135. document.body.style.cursor = '';
  1136. });
  1137. // Phase 2:把 Modal 子组件需要的页面级共享状态/方法 provide 下去(子组件内部按名取用)
  1138. provide(CleaningContextKey, {
  1139. DEFAULT_TEMPLATE_CATEGORY,
  1140. FALLBACK_TEMPLATE_CATEGORY,
  1141. activeCategory,
  1142. activeNodeInfo,
  1143. activeNodeKey,
  1144. activeTemplateItem,
  1145. activeTemplateTab,
  1146. addColumnDialogOpen,
  1147. addColumnForm,
  1148. addColumnMode,
  1149. addedColumnConfig,
  1150. addedColumns,
  1151. applyHeaderRow,
  1152. applyPresetHeadersFromNode,
  1153. applyPreviewDataset,
  1154. applyRecognitionAmountZfState,
  1155. applyRecognitionBasicInfoState,
  1156. applyRecognitionDirectionState,
  1157. autoMatchFields,
  1158. availableSourceHeaders,
  1159. caseDataCleaningShared,
  1160. categories,
  1161. childTemplateName,
  1162. cleaningActions,
  1163. cleaningRules,
  1164. closeTemplateModal,
  1165. createMessage,
  1166. currentColumn,
  1167. dragOverContentIndex,
  1168. draggingContentIndex,
  1169. fieldMapping,
  1170. filteredTemplateList,
  1171. fixedTargetFields,
  1172. generatedColumnHeaderRowMap,
  1173. getColumnRuleBadgeState,
  1174. getColumnRuleBadgeTitle,
  1175. getGeneratedColumnHeaderRowIndex,
  1176. getMatchedHeaderStatusClass,
  1177. getMatchedHeaderStatusText,
  1178. getMatchedHeaderStatusTitle,
  1179. groupingColumnsDraft,
  1180. handleSelect,
  1181. hasDuplicateHeaderName,
  1182. headerRowIndex,
  1183. headerRowInput,
  1184. ignoredRowsSet,
  1185. isSystemTemplateRecord,
  1186. isTargetHandledByRecognitionRule,
  1187. leftCollapsed,
  1188. loadPreviewDatasetFromFile,
  1189. loadTemplateDetail,
  1190. loadTemplatePreviewDetail,
  1191. mappingApi,
  1192. mappingPopoverOpenKey,
  1193. mergeTemplateInfo,
  1194. mockGridTemplate,
  1195. modalTemplateDetail,
  1196. modalTemplateItem,
  1197. nodeStateCache,
  1198. openTemplateModal,
  1199. originFilterKeyword,
  1200. recognitionAmountZfEnabled,
  1201. recognitionApi,
  1202. recognitionBasicInfoRows,
  1203. remapGroupingColumnRules,
  1204. remapSelectRuleHeaderRefs,
  1205. resetNodeWorkState,
  1206. rightTableHeaders,
  1207. runRulePipeline,
  1208. saveCurrentNodeState,
  1209. selectRules,
  1210. selectedAddedColumn,
  1211. selectedContentIndex,
  1212. selectedKeys,
  1213. selectedRowIndex,
  1214. selectedSourceField,
  1215. selectedTemplateId,
  1216. setGeneratedColumnCell,
  1217. showTemplateTabSwitch,
  1218. sortMode,
  1219. sourceTableRows,
  1220. splitColumnConfig,
  1221. splitColumnDialogOpen,
  1222. splitColumnForm,
  1223. structureSyncSuspended,
  1224. syncHeaderRowInput,
  1225. syncModalTemplatePreviewState,
  1226. tableMockRef,
  1227. tablePreviewRows,
  1228. templateApi,
  1229. templateDetailCache,
  1230. templateKeyword,
  1231. templateModalOpen,
  1232. templatePreviewLoading,
  1233. templateSearchBaseList,
  1234. templateSource,
  1235. treeData,
  1236. });
  1237. </script>
  1238. <style lang="less">
  1239. // 样式已按域拆分到 ./cleaning/styles/ 下,由此处引入(保持全局,非 scoped)。
  1240. @import './cleaning/styles/index.less';
  1241. </style>