| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458 |
- <template>
- <div :class="prefixCls" class="case-data-cleaning-page">
- <div class="page-header">
- <div class="page-header-main">
- <div class="page-title-area">
- <Button type="text" class="back-btn" @click="handleBack">←</Button>
- <div class="page-title-block">
- <div class="page-title">案件数据清洗</div>
- </div>
- </div>
- <div class="page-header-actions">
- <Button @click="toggleLeft">{{ leftCollapsed ? '展开左侧' : '收起左侧' }}</Button>
- <Button type="primary" :loading="importingData" @click="handleImportData">导入数据</Button>
- </div>
- </div>
- </div>
- <div class="main-layout" :style="{ gridTemplateColumns: layoutTemplateColumns }">
- <aside v-show="!leftCollapsed" class="left-panel">
- <div class="tree-header">
- <span class="label">文件分类</span>
- <div class="actions">
- <Button size="small" @click="sortMode = sortMode === 'asc' ? '' : 'asc'">升序</Button>
- <Button size="small" @click="sortMode = sortMode === 'desc' ? '' : 'desc'">降序</Button>
- </div>
- </div>
- <div class="tree-wrap">
- <BasicTree
- class="cleaning-file-tree"
- :tree-data="treeData"
- :tree-data-simple-mode="false"
- :field-names="{ key: 'key', title: 'title', children: 'children' }"
- :default-expand-all="true"
- v-model:selectedKeys="selectedKeys"
- @select="handleSelect"
- >
- <template #title="node">
- <div v-if="isTreeFileNode(node.raw)" class="tree-file-row" :title="getTreeFileName(node.raw)">
- <span class="tree-file-name">{{ getTreeFileName(node.raw) }}</span>
- </div>
- <div
- v-else
- class="tree-sheet-row"
- :class="{
- 'is-success': getSheetNodeVisualState(node.raw) === 'success',
- 'is-fail': getSheetNodeVisualState(node.raw) === 'fail',
- 'is-analyzing': getSheetNodeVisualState(node.raw) === 'analyzing',
- }"
- :title="getTreeSheetTitle(node.raw)"
- >
- <span class="tree-sheet-status">
- <span
- v-if="getSheetNodeVisualState(node.raw) === 'analyzing'"
- class="tree-sheet-status-spinner"
- ></span>
- <template v-else>
- {{ getSheetNodeVisualState(node.raw) === 'success' ? '✓' : '✕' }}
- </template>
- </span>
- <span class="tree-sheet-name">{{ getTreeSheetName(node.raw) }}</span>
- </div>
- </template>
- </BasicTree>
- </div>
- <div class="legend">
- <div v-for="item in iconTips" :key="item.key" class="legend-item">
- <span class="legend-icon" :class="`is-${item.key}`">
- <span v-if="item.key === 'matched' || item.key === 'unmatched'" class="legend-dot"></span>
- <template v-else>{{ item.icon }}</template>
- </span>
- <span class="legend-text">{{ item.label }}</span>
- </div>
- </div>
- </aside>
- <div v-show="!leftCollapsed" class="panel-resizer" @mousedown="startResize('left', $event)"></div>
- <section class="middle-panel">
- <div class="template-match-bar" :class="{ 'is-empty': !hasTemplateSelected }">
- <div class="template-match-main">
- <span class="template-match-icon"></span>
- <div class="template-match-copy">
- <div class="template-match-label">匹配模板</div>
- <div class="template-match-name">
- {{ currentTemplateDisplayName }}
- <span v-if="!hasTemplateSelected" class="template-match-badge is-pending">待配置</span>
- <span v-else-if="templateDetailFromMock" class="template-match-badge">示例</span>
- </div>
- </div>
- </div>
- <Button type="link" class="template-select-trigger" @click="openTemplateModal">选择模板</Button>
- </div>
- <template v-if="hasTemplateSelected">
- <div class="middle-panel-body">
- <FieldMappingCard />
- <div
- class="template-foot-panel"
- :class="{
- 'is-collapsed': showRecognitionPanel && !recognitionPanelExpanded,
- 'is-compact': !showRecognitionPanel,
- }"
- >
- <RecognitionRulePanel />
- <ChildTemplatePanel />
- </div>
- </div>
- </template>
- <MiddleEmptyState v-else />
- </section>
- <div class="panel-resizer" @mousedown="startResize('middle', $event)"></div>
- <section class="right-panel">
- <div class="panel-header">
- <div class="panel-title-wrap">
- <div class="panel-title">数据清洗区</div>
- </div>
- </div>
- <div class="toolbar toolbar--column-actions">
- <div class="toolbar-head">
- <span class="toolbar-label">列操作</span>
- </div>
- <div class="toolbar-actions">
- <Button size="small" type="primary" @click="openAddColumn">新增列</Button>
- <Button size="small" @click="openSplitColumn">拆分列</Button>
- <Button size="small" @click="openEditColumn">编辑列</Button>
- <Button size="small" @click="openDeleteColumn">删除列</Button>
- </div>
- <div class="toolbar-meta toolbar-meta--header-row">
- <span class="meta-label">表头行</span>
- <Input
- v-model:value="headerRowInput"
- size="small"
- class="meta-input"
- placeholder="输入行号"
- @pressEnter="applyHeaderRowInput"
- @blur="applyHeaderRowInput"
- />
- </div>
- </div>
- <div class="right-panel-body">
- <div ref="tableMockRef" class="table-mock">
- <div class="mock-head" :style="{ gridTemplateColumns: mockGridTemplate }">
- <span class="row-index-cell row-index-head">表头行</span>
- <span
- v-for="(header, colIndex) in rightTableHeaders"
- :key="header"
- :data-preview-col="colIndex"
- :class="{
- 'is-added': addedColumns.includes(header),
- active: selectedAddedColumn === header,
- 'is-current-col': currentColumn === colIndex,
- 'is-first-col': colIndex === 0,
- 'is-last-col': colIndex === rightTableHeaders.length - 1,
- }"
- @click="onClickHeader(header)"
- >
- <span>{{ header }}</span>
- <span
- v-if="getColumnRuleBadgeState(colIndex)"
- class="rule-badge"
- :class="`is-${getColumnRuleBadgeState(colIndex)}`"
- :title="getColumnRuleBadgeTitle(colIndex)"
- ></span>
- </span>
- </div>
- <div class="mock-match-row" :style="{ gridTemplateColumns: mockGridTemplate }">
- <span class="row-index-cell match-row-index">目标字段</span>
- <span
- v-for="(header, colIndex) in rightTableHeaders"
- :key="`mapping-${header}`"
- :data-preview-col="colIndex"
- :class="{
- 'is-current-col': currentColumn === colIndex,
- 'is-first-col': colIndex === 0,
- 'is-last-col': colIndex === rightTableHeaders.length - 1,
- 'is-empty': !getMatchedTargetFieldText(header),
- }"
- @click="onClickHeader(header)"
- >
- <span class="match-field-content">
- <span class="match-field-text">{{ getMatchedTargetFieldText(header) || '-' }}</span>
- <span class="match-status-group">
- <span
- class="match-status-badge"
- :class="getMatchedHeaderStatusClass(header)"
- :title="getMatchedHeaderStatusTitle(header) || undefined"
- >
- {{ getMatchedHeaderStatusText(header) }}
- </span>
- <span
- v-if="hasHeaderFormatError(header)"
- class="match-status-badge is-formatError"
- :title="getHeaderFormatErrorTitle(header) || undefined"
- >
- !
- </span>
- </span>
- </span>
- </span>
- </div>
- <div
- class="mock-row"
- :style="{ gridTemplateColumns: mockGridTemplate }"
- :class="{
- 'is-ignored-row': ignoredRowsSet.has(rowIndex + 1),
- 'is-current-row': selectedRowIndex === rowIndex,
- 'is-header-row': headerRowIndex === rowIndex,
- }"
- v-for="(row, rowIndex) in tablePreviewRows"
- :key="`row-${rowIndex}`"
- >
- <span class="row-index-cell" @click="selectedRowIndex = rowIndex">
- {{ rowIndex + 1 }}
- </span>
- <span
- v-for="(header, colIndex) in rightTableHeaders"
- :key="`${header}-${rowIndex}`"
- :data-preview-col="colIndex"
- :class="{
- 'is-current-col': currentColumn === colIndex,
- 'is-first-col': colIndex === 0,
- 'is-last-col': colIndex === rightTableHeaders.length - 1,
- }"
- @click="onClickCell(rowIndex, header, colIndex)"
- >
- {{ row[header] || '' }}
- </span>
- </div>
- </div>
- <CleaningRulePanel />
- </div>
- </section>
- </div>
- <AddColumnModal />
- <SplitColumnModal />
- <Modal v-model:open="deleteColumnDialogOpen" title="删除列" :width="520" :centered="true" @ok="confirmDeleteColumn">
- <div>确认删除列「{{ selectedAddedColumn || '-' }}」?</div>
- </Modal>
- <TemplateSelectModal />
- </div>
- </template>
- <script lang="ts" setup name="ViewsDataCleaning">
- import { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
- import { DownOutlined, UpOutlined } from '@ant-design/icons-vue';
- import { Button, Checkbox, Input, InputNumber, Modal, Popover, Radio, Select, Switch, Tooltip } from 'ant-design-vue';
- import { EmptyState } from '@/core/components/EmptyState';
- import { BasicTree } from '@/core/components/Tree';
- import { useDesign } from '@/core/hooks/web/useDesign';
- import { useGo, useQuery } from '@/core/hooks/web/usePage';
- import { PageEnum } from '@/core/enums/pageEnum';
- import { useMessage } from '@/core/hooks/web/useMessage';
- import type { FileInfo } from '@/types/api';
- import { tplCategoryList, tplCheckTableName, tplDetail, tplPage } from '@/plat/api/tpl/tplApi';
- import { rawPreview } from '@/case/api/govern/governApi';
- import type { TableInfo } from '@/plat/api/tpl/types/dto';
- import {
- FunAbs,
- FunDate,
- FunDateTime,
- FunExtra,
- FunMultiplier,
- FunRadix,
- FunRegular,
- FunReplace,
- FunSpExtra,
- FunTime,
- MappingEnum,
- RuleMapping,
- RuleSplit,
- } from '../../../dm/types/Func';
- import {
- buildRemoveSpecialRegex,
- groupColumnsFnValue,
- normalizeDecimalBaseLabel,
- runCleaningPreviewPipeline,
- type CleaningRuleKey as RulePipelineKey,
- } from './cleaningRuleUtils';
- import { resetImportProgressSession } from './importProgressSession';
- import { FileStatusEnum } from '../../types/enum';
- import { FILE_INFO_STORAGE_KEY, IMPORT_FILE_INFO_STORAGE_KEY, dateTypeOptions, decimalBaseOptions, defaultRightTableHeaders, delimiterOptions, iconTips, mockDataColumnWidth, mockIndexColumnWidth, replaceModeOptions, ruleExampleMap, ruleNameMap, rulesList } from './cleaning/constants';
- 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';
- import { createGroupingColumnDraft, getFilledGroupingFieldCount, normalizeGroupingColumnsDraft, normalizeSplitColumnMode, normalizeSplitColumnValue, resolveSplitColumnLength, splitSourceValueByConfig } from './cleaning/utils/columnSplit';
- import { getVisibleOriginOptionList, scrollSelectedTreeNodeIntoView } from './cleaning/utils/domScroll';
- import { buildFormatValidationTitle, isValidAcctCardNoValue, isValidDateOnlyValue, isValidDateTimeValue, isValidIdCardNumberValue, isValidNumberValue, isValidPhoneValue, isValidTimeOnlyValue, normalizeCallTypeCoreValue, normalizeFormatValidationType, normalizeLoanFlagCoreValue, shouldSkipFormatValidationByTarget } from './cleaning/utils/formatValidate';
- import { buildHeaderLabelsByRow, collectMatchedSheetsWithoutHeaderRow, pickPreviewRowByHeaders } from './cleaning/utils/headerRow';
- import { loadStoredFileList, mockFileList, mockTemplateList, mockTemplateMap } from './cleaning/utils/mockData';
- import { clearRecognitionBasicInfoFilledContent, hasOwnRecognitionBasicInfoConfig, hasRecognitionBasicInfoFilledContent, isSameRecognitionBasicInfoConfig, mergeRecognitionDirectionKeywords, normalizeRecognitionBasicInfoText, normalizeRecognitionDirectionGroup, parseRecognitionBasicInfoRows, parseRecognitionDirectionEntries, resolveRecognitionBasicInfoTargetField, resolveRecognitionDirectionTab, splitRecognitionDirectionKeywords } from './cleaning/utils/recognition';
- import { buildStoredColumnRulePayload, defaultRuleConfig, deserializeStoredRule, getSplitColumnRuleIdentity, isCleaningRuleEnabledInUi, sanitizeFileInfoTree, sanitizeSelectedRuleGroups, sanitizeSelectedRuleList, serializeStoredFunRules } from './cleaning/utils/ruleSerialize';
- 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';
- import { buildStoredLineNo, cloneDeepSafe, findBestMatchedHeader, getRequestErrorMessage, normalizeColumnNameForCompare, normalizeFieldMatchText, normalizeFormatValue, normalizeGeneratedColumnHeaderRowIndex, normalizeRecognitionConfigText, normalizeRuleText, normalizeSingleLineText, normalizeStoredLineNoToRowIndex, normalizeTemplateClassifyText, normalizeTemplateHeaders, normalizeTemplateId, normalizeTemplateNameKey, renameRecordKeys, resolveTemplateNameCheckResult, toRuleBoolean } from './cleaning/utils/text';
- import { buildTreeFromFlatList, findFirstSelectableKey, findTreeNodeByKey, getFileStatusVisualState, getTreeFileName, getTreeNodeKey, getTreeSheetName, getTreeSheetTitle, isFileRootItem, isTemplateMatchedFailStatus, isTreeFileNode, isTreeSheetNode, toTreeNodeDeep } from './cleaning/utils/treeUtils';
- import AddColumnModal from './cleaning/components/AddColumnModal.vue';
- import SplitColumnModal from './cleaning/components/SplitColumnModal.vue';
- import TemplateSelectModal from './cleaning/components/TemplateSelectModal.vue';
- import { CleaningContextKey } from './cleaning/context';
- import CleaningRulePanel from './cleaning/components/CleaningRulePanel.vue';
- import { useCleaningRules } from './cleaning/composables/useCleaningRules';
- import FieldMappingCard from './cleaning/components/FieldMappingCard.vue';
- import OriginFieldPopover from './cleaning/components/OriginFieldPopover.vue';
- import RecognitionRulePanel from './cleaning/components/RecognitionRulePanel.vue';
- import ChildTemplatePanel from './cleaning/components/ChildTemplatePanel.vue';
- import MiddleEmptyState from './cleaning/components/MiddleEmptyState.vue';
- import { useFieldMapping } from './cleaning/composables/useFieldMapping';
- import { useRecognitionRules } from './cleaning/composables/useRecognitionRules';
- import { useTemplateLibrary } from './cleaning/composables/useTemplateLibrary';
- const { prefixCls } = useDesign('case-data-cleaning');
- const go = useGo();
- const pageQuery = useQuery();
- const { createMessage } = useMessage();
- const importingData = ref(false);
- const leftCollapsed = ref(false);
- const leftPanelWidth = ref(300);
- const middlePanelWidth = ref(520);
- const minLeftWidth = 220;
- const maxLeftWidth = 520;
- const minMiddleWidth = 360;
- const maxMiddleWidth = 900;
- const minRightWidth = 560;
- const resizeState = ref<{ type: 'left' | 'middle' | ''; startX: number; startLeft: number; startMiddle: number }>({
- type: '',
- startX: 0,
- startLeft: 300,
- startMiddle: 520,
- });
- const pendingClientX = ref<number | null>(null);
- const rafId = ref<number | null>(null);
- const layoutTemplateColumns = computed(() => {
- if (leftCollapsed.value) {
- return `${middlePanelWidth.value}px 6px minmax(${minRightWidth}px, 1fr)`;
- }
- return `${leftPanelWidth.value}px 6px ${middlePanelWidth.value}px 6px minmax(${minRightWidth}px, 1fr)`;
- });
- const selectedKeys = ref<string[]>([]);
- const isTableRuleMatched = (
- item?: TreeTableRuleInfo | null,
- options: { index?: number; amountZfEnabled?: boolean } = {},
- ) => {
- if (!item) return false;
- if (
- options.amountZfEnabled &&
- options.index !== undefined &&
- isLoanFlagTargetField(getTableRuleTargetField(item, options.index))
- ) {
- return true;
- }
- const fileColIndex = Number(item.fileColIndex);
- return (Number.isInteger(fileColIndex) && fileColIndex >= 0) || !!normalizeRuleText(item.fileColName);
- };
- const isMatchSuccessful = (requiredTotal: number, requiredMatched: number, matchedCount: number) => {
- if (requiredTotal > 0) {
- return requiredMatched === requiredTotal;
- }
- return matchedCount > 0;
- };
- const getTableRuleMatchStats = (list?: TreeTableRuleInfo[] | null) => {
- const normalizedRules = normalizeTableRuleList(list);
- const amountZfEnabled = normalizedRules.some(
- (item, index) => isTransAmountTargetField(getTableRuleTargetField(item, index)) && getZfFlag(item.zf),
- );
- const normalizedRuleItems = normalizedRules.map((item, index) => ({ item, index }));
- const requiredRules = normalizedRuleItems.filter(({ item }) => getTableRuleRequiredFlag(item));
- const requiredTotal = requiredRules.length;
- const requiredMatched = requiredRules.filter(({ item, index }) =>
- isTableRuleMatched(item, { index, amountZfEnabled }),
- ).length;
- const matchedCount = normalizedRuleItems.filter(({ item, index }) =>
- isTableRuleMatched(item, { index, amountZfEnabled }),
- ).length;
- return { requiredTotal, requiredMatched, matchedCount };
- };
- const isSheetMatchComplete = (item?: TreeFileInfo | null) => {
- if (!isTreeSheetNode(item)) return false;
- const { requiredTotal, requiredMatched, matchedCount } = getTableRuleMatchStats(item?.tableRuleList);
- return isMatchSuccessful(requiredTotal, requiredMatched, matchedCount);
- };
- const resolveSelectableTreeKey = (key: string) => {
- const selectedNode = findTreeNodeByKey(treeData.value, key);
- if (!selectedNode) return '';
- if (selectedNode.selectable !== false) return selectedNode.key;
- return findFirstSelectableKey(selectedNode.children || []);
- };
- const rightTableHeaders = ref([...defaultRightTableHeaders]);
- const addedColumns = ref<string[]>([]);
- const selectedAddedColumn = ref('');
- const sourceTableRows = ref<Record<string, string>[]>([]);
- const tablePreviewRows = ref<Record<string, string>[]>(JSON.parse(JSON.stringify(sourceTableRows.value)));
- const currentColumn = ref<number | null>(null);
- const selectedRowIndex = ref<number | null>(null);
- const headerRowIndex = ref<number | null>(null);
- // Phase 3:清洗规则域下沉到 composable。页面同名解构接住,原有代码与模板零改动。
- const cleaningRules = useCleaningRules({ addedColumns, createMessage, currentColumn, headerRowIndex, rightTableHeaders, sourceTableRows, tablePreviewRows });
- 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;
- const initialNodeStatusSignatureMap = new Map<string, string>();
- const analyzingNodeKeys = ref<string[]>([]);
- const setNodeAnalyzing = (nodeKey: string, analyzing: boolean) => {
- if (!nodeKey) return;
- if (analyzing) {
- if (!analyzingNodeKeys.value.includes(nodeKey)) {
- analyzingNodeKeys.value = [...analyzingNodeKeys.value, nodeKey];
- }
- return;
- }
- if (analyzingNodeKeys.value.includes(nodeKey)) {
- analyzingNodeKeys.value = analyzingNodeKeys.value.filter((item) => item !== nodeKey);
- }
- };
- const buildNodeStatusSignature = (state: NodeCacheState) =>
- JSON.stringify({
- selectedTemplateId: state.selectedTemplateId,
- fieldMapping: state.fieldMapping,
- rightTableHeaders: state.rightTableHeaders,
- addedColumns: state.addedColumns,
- headerRowIndex: state.headerRowIndex,
- selectRules: state.selectRules.map((rules) =>
- rules.map((rule) => ({
- key: rule.key,
- name: rule.name,
- enabled: rule.enabled,
- rules: rule.rules,
- })),
- ),
- selectedRules: state.selectedRules.map((rule) => ({
- key: rule.key,
- name: rule.name,
- enabled: rule.enabled,
- rules: rule.rules,
- })),
- addedColumnConfig: state.addedColumnConfig,
- splitColumnConfig: state.splitColumnConfig,
- generatedColumnHeaderRowMap: state.generatedColumnHeaderRowMap,
- recognitionDirectionFields: (state.recognitionDirectionFields || []).map((item) => ({
- key: item.key,
- label: item.label,
- tab: item.tab,
- entries: item.entries.map((entry) => ({
- label: entry.label,
- value: entry.value,
- })),
- })),
- recognitionBasicInfoBaseFuncRegx: state.recognitionBasicInfoBaseFuncRegx,
- recognitionBasicInfoManualVisible: state.recognitionBasicInfoManualVisible,
- recognitionBasicInfoRows: state.recognitionBasicInfoRows.map((item) => ({
- headerMatch: item.headerMatch,
- targetField: item.targetField,
- endFlag: item.endFlag,
- })),
- recognitionAmountZfEnabled: state.recognitionAmountZfEnabled,
- childTemplateName: state.childTemplateName,
- });
- const applyNodeCacheState = (state: NodeCacheState) => {
- selectedTemplateId.value = state.selectedTemplateId;
- fieldMapping.value = cloneDeepSafe(state.fieldMapping);
- rightTableHeaders.value = [...state.rightTableHeaders];
- addedColumns.value = [...state.addedColumns];
- selectedAddedColumn.value = state.selectedAddedColumn;
- sourceTableRows.value = cloneDeepSafe(state.sourceTableRows);
- selectRules.value = sanitizeSelectedRuleGroups(cloneDeepSafe(state.selectRules));
- selectedRules.value = sanitizeSelectedRuleList(cloneDeepSafe(state.selectedRules));
- activeRules.value = state.activeRules;
- currentColumn.value = state.currentColumn;
- selectedRowIndex.value = state.selectedRowIndex;
- headerRowIndex.value = state.headerRowIndex;
- headerRowInput.value = state.headerRowInput;
- currentRuleTitle.value = state.currentRuleTitle;
- omitLineInput.value = state.omitLineInput;
- dateConfigType.value = state.dateConfigType;
- extractDataBefore.value = state.extractDataBefore;
- extractDataAfter.value = state.extractDataAfter;
- syncExtractDataMode();
- extractSegmentValue.value = state.extractSegmentValue;
- extractSegmentRow.value = state.extractSegmentRow;
- absoluteValueChecked.value = state.absoluteValueChecked;
- decimalEnabled.value = state.decimalEnabled;
- decimalSourceBase.value = state.decimalSourceBase;
- decimalTargetBase.value = state.decimalTargetBase;
- replaceEnabled.value = state.replaceEnabled;
- replaceMode.value = state.replaceMode;
- replaceSource.value = state.replaceSource;
- replaceTarget.value = state.replaceTarget;
- removeSpaceChecked.value = state.removeSpaceChecked;
- removeTabChecked.value = state.removeTabChecked;
- removeCustomChecked.value = state.removeCustomChecked;
- removeCustomText.value = state.removeCustomText;
- groupingEnabled.value = state.groupingEnabled;
- groupingColumnsDraft.value = normalizeGroupingColumnsDraft(cloneDeepSafe(state.groupingColumnsDraft));
- magnificationValue.value = state.magnificationValue;
- addedColumnConfig.value = cloneDeepSafe(state.addedColumnConfig);
- splitColumnConfig.value = cloneDeepSafe(state.splitColumnConfig);
- generatedColumnHeaderRowMap.value = buildGeneratedColumnHeaderRowMap(
- state.addedColumns,
- state.sourceTableRows,
- state.generatedColumnHeaderRowMap,
- state.headerRowIndex,
- );
- recognitionDirectionFields.value = cloneDeepSafe(state.recognitionDirectionFields || []);
- recognitionBasicInfoBaseFuncRegx.value = state.recognitionBasicInfoBaseFuncRegx;
- recognitionBasicInfoManualVisible.value = state.recognitionBasicInfoManualVisible;
- recognitionBasicInfoExpanded.value = state.recognitionBasicInfoExpanded;
- recognitionBasicInfoRows.value = cloneDeepSafe(state.recognitionBasicInfoRows);
- recognitionAmountZfEnabled.value = !!state.recognitionAmountZfEnabled;
- childTemplateName.value = state.childTemplateName;
- if (!selectedRules.value.some((item) => item.active) && currentColumn.value !== null) {
- const currentColumnRules = selectRules.value[currentColumn.value] || [];
- if (!currentColumnRules.some((item) => item.active)) {
- currentRuleTitle.value = '';
- }
- }
- runRulePipeline();
- };
- const resetNodeWorkState = () => {
- selectedTemplateId.value = 0;
- templateDetail.value = null;
- templateDetailFromMock.value = false;
- activeTemplateItem.value = null;
- fieldMapping.value = {};
- activeMappingTargetKey.value = '';
- rightTableHeaders.value = [];
- addedColumns.value = [];
- selectedAddedColumn.value = '';
- sourceTableRows.value = [];
- selectRules.value = [];
- selectedRules.value = [];
- activeRules.value = 0;
- currentColumn.value = null;
- selectedRowIndex.value = null;
- headerRowIndex.value = null;
- headerRowInput.value = '';
- currentRuleTitle.value = '';
- omitLineInput.value = '';
- dateConfigType.value = 1;
- extractDataMode.value = 'before';
- extractDataBefore.value = '';
- extractDataAfter.value = '';
- extractSegmentValue.value = '';
- extractSegmentRow.value = 1;
- absoluteValueChecked.value = false;
- decimalEnabled.value = false;
- decimalSourceBase.value = '10进制';
- decimalTargetBase.value = '16进制';
- replaceEnabled.value = true;
- replaceMode.value = 0;
- replaceSource.value = '';
- replaceTarget.value = '';
- removeSpaceChecked.value = false;
- removeTabChecked.value = false;
- removeCustomChecked.value = false;
- removeCustomText.value = '';
- groupingEnabled.value = false;
- groupingColumnsDraft.value = normalizeGroupingColumnsDraft();
- magnificationValue.value = 1;
- addedColumnConfig.value = {};
- splitColumnConfig.value = {};
- generatedColumnHeaderRowMap.value = {};
- recognitionDirectionFields.value = [];
- recognitionBasicInfoBaseFuncRegx.value = '';
- recognitionBasicInfoManualVisible.value = false;
- recognitionBasicInfoExpanded.value = false;
- recognitionBasicInfoRows.value = [];
- recognitionAmountZfEnabled.value = false;
- childTemplateName.value = '';
- runRulePipeline();
- };
- const switchNodeState = async (nextKey: string) => {
- if (!nextKey) return;
- if (activeNodeKey.value === nextKey) return;
- saveCurrentNodeState();
- setNodeAnalyzing(nextKey, true);
- activeNodeKey.value = nextKey;
- const currentNode = findTreeNodeByKey(treeData.value, nextKey)?.raw || null;
- structureSyncSuspended.value = true;
- try {
- const cached = nodeStateCache.get(nextKey);
- if (cached) {
- applyNodeCacheState(cached);
- await loadTemplateDetail(cached.selectedTemplateId, { silent: true, requestDetail: false });
- touchNodeCache(nextKey, cached);
- return;
- }
- resetNodeWorkState();
- const shouldLoadNodeTemplate = !isTemplateMatchedFailStatus(currentNode?.fileStatus);
- await loadTemplateDetail(shouldLoadNodeTemplate ? currentNode?.templateId : undefined, {
- silent: true,
- requestDetail: false,
- });
- await applyNodePreset(currentNode);
- const initialState = buildNodeCacheState();
- touchNodeCache(nextKey, initialState);
- if (!initialNodeStatusSignatureMap.has(nextKey)) {
- initialNodeStatusSignatureMap.set(nextKey, buildNodeStatusSignature(initialState));
- }
- } finally {
- structureSyncSuspended.value = false;
- syncActiveNodeStructure();
- setNodeAnalyzing(nextKey, false);
- }
- };
- const structureSyncSuspended = ref(false);
- const persistFileListToSessionStorage = () => {
- try {
- sessionStorage.setItem(FILE_INFO_STORAGE_KEY, JSON.stringify(sanitizeFileInfoTree(fileList.value)));
- } catch (error) {
- // 忽略序列化异常,避免影响当前清洗编辑流程。
- }
- };
- const applyPreviewDataset = (dataset: PreviewFileDataset) => {
- rightTableHeaders.value = [...dataset.headers];
- addedColumns.value = [];
- generatedColumnHeaderRowMap.value = {};
- selectedAddedColumn.value = '';
- sourceTableRows.value = dataset.rows.length ? dataset.rows : [];
- tablePreviewRows.value = cloneDeepSafe(sourceTableRows.value);
- return true;
- };
- const loadPreviewDatasetFromFile = async (item?: TreeFileInfo | null): Promise<PreviewFileDataset | null> => {
- // 从 DuckDB 动态表 raw_{sheetId} 查询预览数据(sheet 节点 id 即 sheetId),
- // 替代原 Electron 端本地文件读取
- if (!isTreeSheetNode(item) || item?.id === null || item?.id === undefined) return null;
- try {
- const payload = (await rawPreview({
- sheetId: item.id,
- headers: normalizeTemplateHeaders(item?.header),
- headerRowIndex: normalizeStoredLineNoToRowIndex(item?.lineNo),
- limit: 2000,
- })) as PreviewFileDataset | null;
- if (!payload?.headers?.length) return null;
- return {
- headers: payload.headers.map((header) => normalizeRuleText(header)).filter(Boolean),
- rows: Array.isArray(payload.rows) ? payload.rows : [],
- };
- } catch (error) {
- console.error('读取预览数据失败', item?.id, error);
- return null;
- }
- };
- const applyPresetHeadersFromNode = (item?: TreeFileInfo | null) => {
- const headers = normalizeTemplateHeaders(item?.header);
- if (!headers.length) return false;
- return applyPreviewDataset({
- headers,
- rows: [],
- });
- };
- const insertHeaderAt = (header: string, targetIndex?: number | null) => {
- if (!header) return;
- if (!rightTableHeaders.value.includes(header)) {
- const maxIndex = rightTableHeaders.value.length;
- const nextIndex =
- typeof targetIndex === 'number' && Number.isInteger(targetIndex)
- ? Math.min(Math.max(targetIndex, 0), maxIndex)
- : maxIndex;
- rightTableHeaders.value.splice(nextIndex, 0, header);
- }
- if (!addedColumns.value.includes(header)) {
- addedColumns.value.push(header);
- }
- if (!Object.prototype.hasOwnProperty.call(generatedColumnHeaderRowMap.value, header)) {
- generatedColumnHeaderRowMap.value[header] = headerRowIndex.value;
- }
- sourceTableRows.value = sourceTableRows.value.map((row) => ({
- ...row,
- [header]: String(row[header] ?? ''),
- }));
- };
- const buildColumnRulePayload = (header: string): TreeStoredColumnRule | null => {
- const splitConfig = splitColumnConfig.value[header];
- if (splitConfig) {
- const splitNameIndex = rightTableHeaders.value.findIndex((item) => item === splitConfig.targetName1);
- const splitNameIndex2 = rightTableHeaders.value.findIndex((item) => item === splitConfig.targetName2);
- const splitMode = normalizeSplitColumnMode(splitConfig.delimiterType);
- const splitValue = normalizeSplitColumnValue(splitConfig.delimiterType, splitConfig.delimiterValue);
- const splitLength = resolveSplitColumnLength(splitConfig.delimiterType, splitConfig.delimiterValue);
- const splitRule = new RuleSplit();
- splitRule.refIndex = rightTableHeaders.value.findIndex((item) => item === splitConfig.source);
- splitRule.mappingEnum = splitMode === 'length' ? MappingEnum.SP_LEN : MappingEnum.SP_CHAR;
- splitRule.splitNameIndex = splitNameIndex >= 0 ? splitNameIndex : undefined;
- splitRule.splitNameIndex2 = splitNameIndex2 >= 0 ? splitNameIndex2 : undefined;
- splitRule.len = splitMode === 'length' && splitLength > 0 ? splitLength : undefined;
- splitRule.splitChar = splitMode === 'special' && splitValue ? splitValue : undefined;
- return buildStoredColumnRulePayload(splitRule);
- }
- const addedConfig = addedColumnConfig.value[header];
- if (!addedConfig) return null;
- const mappingRule = new RuleMapping();
- mappingRule.refIndex = addedConfig.contentFields
- .map((field) => rightTableHeaders.value.findIndex((item) => item === field))
- .filter((index) => index >= 0);
- mappingRule.mappingEnum =
- addedConfig.mode === 'custom'
- ? addedConfig.customContent
- ? MappingEnum.NEW_DATA
- : MappingEnum.BLANK
- : mappingRule.refIndex.length
- ? MappingEnum.REF
- : MappingEnum.BLANK;
- mappingRule.content = addedConfig.mode === 'custom' ? addedConfig.customContent || '' : '';
- return buildStoredColumnRulePayload(mappingRule);
- };
- const restoreStoredColumnRules = (list?: TreeTableRuleInfo[] | null) => {
- const headerSnapshot = [...rightTableHeaders.value];
- const normalizedRules = normalizeTableRuleList(list);
- const splitRuleGroups = normalizedRules.reduce((acc, rule) => {
- const header = normalizeRuleText(rule.fileColName);
- const columnRule = rule.columnRule;
- const className = normalizeRuleText(columnRule?.['@class']);
- const mode = normalizeRuleText(columnRule?.mode);
- if (!header || (!className.endsWith('RuleSplit') && mode !== 'split')) {
- return acc;
- }
- const fileColIndex = Number(rule.fileColIndex);
- const identity = getSplitColumnRuleIdentity(columnRule);
- const prev = acc.get(identity) || [];
- prev.push({
- header,
- fileColIndex: Number.isInteger(fileColIndex) && fileColIndex >= 0 ? fileColIndex : Number.MAX_SAFE_INTEGER,
- });
- acc.set(identity, prev);
- return acc;
- }, new Map<string, Array<{ header: string; fileColIndex: number }>>());
- normalizedRules.forEach((rule) => {
- const header = normalizeRuleText(rule.fileColName);
- if (!header) return;
- const columnRule = rule.columnRule;
- const fileColIndex = Number(rule.fileColIndex);
- const targetIndex = Number.isInteger(fileColIndex) && fileColIndex >= 0 ? fileColIndex : undefined;
- if (columnRule) {
- insertHeaderAt(header, targetIndex);
- }
- const className = normalizeRuleText(columnRule?.['@class']);
- const mode = normalizeRuleText(columnRule?.mode);
- if (className.endsWith('RuleSplit') || mode === 'split') {
- const refIndex = Number(columnRule?.refIndex);
- const source =
- Number.isInteger(refIndex) && refIndex >= 0
- ? headerSnapshot[refIndex] || rightTableHeaders.value[refIndex] || ''
- : '';
- if (!source) return;
- const splitChar = String(columnRule?.splitChar ?? '');
- const splitLength = Number(columnRule?.len);
- const splitMappingEnum = Number(columnRule?.mappingEnum);
- const isLengthSplit =
- splitMappingEnum === MappingEnum.SP_LEN ||
- (!Number.isInteger(splitMappingEnum) && Number.isInteger(splitLength) && splitLength > 0);
- const splitNameIndex = Number(columnRule?.splitNameIndex);
- const splitNameIndex2 = Number(columnRule?.splitNameIndex2);
- const groupedTargetItems = (splitRuleGroups.get(getSplitColumnRuleIdentity(columnRule)) || [])
- .slice()
- .sort((a, b) => a.fileColIndex - b.fileColIndex);
- const groupedTargets = groupedTargetItems.map((item) => item.header).filter(Boolean);
- const uniqueTargets = Array.from(new Set(groupedTargets));
- const resolveGroupedTargetByIndex = (index: number) =>
- Number.isInteger(index) && index >= 0
- ? groupedTargetItems.find((item) => item.fileColIndex === index)?.header || ''
- : '';
- let targetName1 = resolveGroupedTargetByIndex(splitNameIndex);
- let targetName2 = resolveGroupedTargetByIndex(splitNameIndex2);
- if (!targetName1 && !targetName2) {
- targetName1 = uniqueTargets[0] || header;
- targetName2 = uniqueTargets.find((item) => item !== targetName1) || '';
- }
- if (targetName1 && targetName2 && targetName1 === targetName2) {
- if (Number.isInteger(fileColIndex) && fileColIndex === splitNameIndex2) {
- targetName1 = '';
- } else {
- targetName2 = '';
- }
- }
- splitColumnConfig.value[header] = {
- source,
- delimiterType: isLengthSplit ? 'length' : 'special',
- delimiterValue: isLengthSplit ? String(splitLength > 0 ? splitLength : '') : splitChar,
- targetName1,
- targetName2,
- };
- return;
- }
- if (className.endsWith('RuleMapping') || mode === 'ref' || mode === 'custom') {
- const refIndexList = Array.isArray(columnRule?.refIndex)
- ? columnRule.refIndex.map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0)
- : [];
- const mappingEnum = Number(columnRule?.mappingEnum);
- const isRefMapping = mappingEnum === MappingEnum.REF || (!Number.isFinite(mappingEnum) && mode === 'ref');
- addedColumnConfig.value[header] = {
- mode: isRefMapping ? 'ref' : 'custom',
- contentFields: refIndexList
- .map((index) => headerSnapshot[index] || rightTableHeaders.value[index])
- .filter(Boolean),
- customContent: String(columnRule?.content ?? ''),
- };
- }
- });
- };
- const restoreStoredFunRules = (list?: TreeTableRuleInfo[] | null) => {
- selectedRules.value = [];
- selectRules.value = rightTableHeaders.value.map(() => []);
- const sharedRuleKeySet = new Set<string>();
- const mergeColumnRule = (
- columnRules: SelectedRuleItem[],
- nextRule: SelectedRuleItem,
- rawRule: TreeStoredFunRule,
- ) => {
- if (nextRule.key !== 'extractData') {
- columnRules.push(nextRule);
- return;
- }
- const lastRule = columnRules[columnRules.length - 1];
- if (!lastRule || lastRule.key !== 'extractData') {
- columnRules.push(nextRule);
- return;
- }
- const direction = normalizeRuleText(rawRule?.metaDirection) || (rawRule?.opened === false ? 'after' : 'before');
- if (direction === 'after') {
- lastRule.rules.nextModel = nextRule.rules.nextModel;
- } else {
- lastRule.rules.beforeModel = nextRule.rules.beforeModel;
- }
- };
- normalizeTableRuleList(list).forEach((tableRule) => {
- const header = normalizeRuleText(tableRule.fileColName);
- const columnIndex = header ? rightTableHeaders.value.findIndex((item) => item === header) : -1;
- const columnRules: SelectedRuleItem[] = [];
- (tableRule.funList || [])
- .slice()
- .sort((a, b) => Number(a?.sort ?? 0) - Number(b?.sort ?? 0))
- .forEach((rule) => {
- const nextRule = deserializeStoredRule(rule);
- if (!nextRule) return;
- if (nextRule.key === 'ignoreLine') {
- if (!sharedRuleKeySet.has(nextRule.key)) {
- selectedRules.value.push(nextRule);
- sharedRuleKeySet.add(nextRule.key);
- }
- return;
- }
- mergeColumnRule(columnRules, nextRule, rule);
- });
- if (columnIndex >= 0 && columnRules.length) {
- selectRules.value[columnIndex] = sanitizeSelectedRuleList(columnRules);
- }
- });
- selectedRules.value = sanitizeSelectedRuleList(selectedRules.value);
- };
- const restoreFileLevelRules = (item?: TreeFileInfo | null) => {
- const ignoreLines = Array.isArray(item?.ignoreLines)
- ? item.ignoreLines.map((line) => Number(line)).filter((line) => Number.isInteger(line) && line > 0)
- : [];
- if (ignoreLines.length) {
- const existingIgnoreRule = selectedRules.value.find((rule) => rule.key === 'ignoreLine');
- const nextIgnoreRules = {
- ...defaultRuleConfig(),
- ...(existingIgnoreRule?.rules || {}),
- omitLine: ignoreLines.join(','),
- };
- if (existingIgnoreRule) {
- existingIgnoreRule.rules = nextIgnoreRules;
- existingIgnoreRule.enabled = true;
- } else {
- selectedRules.value = [
- ...selectedRules.value,
- {
- name: ruleNameMap.ignoreLine,
- key: 'ignoreLine',
- active: selectedRules.value.length === 0,
- enabled: true,
- rules: nextIgnoreRules,
- },
- ];
- }
- }
- };
- const buildTableRuleListFromState = (list?: TreeTableRuleInfo[] | null) => {
- const previousRules = normalizeTableRuleList(list);
- const currentTemplateTableId = normalizeTemplateId(
- templateDetail.value?.id ??
- activeTemplateItem.value?.id ??
- selectedTemplateId.value ??
- activeNodeInfo.value?.templateId,
- );
- const previousRuleMap = previousRules.reduce(
- (acc, item, index) => {
- const key = getTableRuleTargetKey(item, index);
- if (key) acc[key] = item;
- return acc;
- },
- {} as Record<string, TreeTableRuleInfo>,
- );
- const sharedFunRules = serializeStoredFunRules(selectedRules.value);
- const directionConfMap = recognitionDirectionFields.value.reduce(
- (acc, item) => {
- acc[item.key] = buildRecognitionDirectionConf(item.entries);
- return acc;
- },
- {} as Record<string, string>,
- );
- return fixedTargetFields.value.map((field, index) => {
- const previous = previousRuleMap[field.key];
- const requiredMarker = getTableRuleRequiredFlag(previous, field.required) || field.required;
- const mappedHeader = normalizeRuleText(effectiveFieldMapping.value[field.key]);
- const matchedByRecognitionRule = isTargetHandledByRecognitionRule(field.key);
- const fileColIndex = mappedHeader ? rightTableHeaders.value.findIndex((item) => item === mappedHeader) : -1;
- const columnFunRules =
- fileColIndex >= 0 ? serializeStoredFunRules(selectRules.value[fileColIndex] || [], sharedFunRules.length) : [];
- const templateField = resolveTemplateFieldMeta(field, index);
- const templateFieldRecord = (templateField || {}) as Record<string, any>;
- // 交易金额字段以「正负识别」开关为准(开关本身已从节点/模板的 zf 初始化);
- // 其余字段沿用模板字段 / 上一条规则的 zf 并兜底 0,保证每条规则都带整数 zf
- // ——后端 TableRuleDTO.zf 是 Integer,清洗器 `rule.getZf() == 1` 会自动拆箱,缺字段必 NPE。
- const zf = isTransAmountTargetField(field)
- ? recognitionAmountZfEnabled.value
- ? 1
- : 0
- : resolveStoredZfFlag(templateFieldRecord, previous);
- const requiredFlagValue = pickFirstDefinedRuleFlagValue(
- getRawRuleFlagValue(templateFieldRecord, 'required'),
- getRawRuleFlagValue(previous, 'required'),
- requiredMarker ? 1 : 0,
- );
- const matchedFlagValue =
- matchedByRecognitionRule || mappedHeader
- ? 1
- : pickFirstDefinedRuleFlagValue(
- getRawRuleFlagValue(templateFieldRecord, 'matched'),
- getRawRuleFlagValue(previous, 'matched'),
- requiredMarker ? 1 : 0,
- );
- const matchedRequiredFlagValue = pickFirstDefinedRuleFlagValue(getRawRuleFlagValue(previous, 'matchedRequired'));
- const directionConf =
- directionConfMap[field.key] ||
- normalizeRecognitionConfigText(previous?.directionConf) ||
- normalizeRecognitionConfigText(templateFieldRecord.directionConf ?? templateFieldRecord.direction_conf);
- const fileRuleList = [...(index === 0 ? sharedFunRules : []), ...columnFunRules];
- return {
- id: previous?.id ?? null,
- tableId: currentTemplateTableId || normalizeTemplateId(previous?.tableId) || null,
- fieldNameCn: field.label,
- fieldNameEn: field.key,
- fieldType: normalizeRuleText(templateField?.fieldType) || normalizeRuleText(previous?.fieldType) || 'string',
- required: requiredFlagValue,
- fieldLen: previous?.fieldLen ?? null,
- matched: matchedFlagValue,
- matchedRequired: matchedRequiredFlagValue,
- directionConf: directionConf || null,
- zf,
- fieldSort: index + 1,
- fileColIndex: fileColIndex >= 0 ? fileColIndex : null,
- fileColName: mappedHeader || null,
- funList: fileRuleList,
- columnRule: mappedHeader ? buildColumnRulePayload(mappedHeader) : null,
- } as TreeTableRuleInfo;
- });
- };
- const syncNodeStructure = (node?: TreeFileInfo | null) => {
- if (!isTreeSheetNode(node)) return;
- const templateId = normalizeTemplateId(
- templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value ?? node.templateId,
- );
- const mainId = getSelectedTemplateMainId(node.mainId);
- const tableName =
- normalizeRuleText(templateDetail.value?.tableNameCn) ||
- normalizeRuleText(activeTemplateItem.value?.tableNameCn) ||
- normalizeRuleText(node.tableName);
- const baseHeaders = rightTableHeaders.value.filter((header) => !addedColumns.value.includes(header));
- const existingHeaders = normalizeTemplateHeaders(node.header);
- const hasRealHeaderConfig =
- headerRowIndex.value !== null ||
- !!existingHeaders.length ||
- addedColumns.value.length > 0 ||
- Object.values(effectiveFieldMapping.value).some(Boolean) ||
- baseHeaders.length > 0;
- const shouldPersistRecognitionBasicInfo = hasRecognitionBasicInfoFilledContent(recognitionBasicInfoRows.value);
- const ignoreLines = Array.from(ignoredRowsSet.value).sort((a, b) => a - b);
- const groupColumns = buildEnabledGroupingColumnsPayload();
- node.templateId = templateId || node.templateId || null;
- node.mainId = mainId || null;
- node.tableName = tableName || node.tableName || null;
- if (headerRowIndex.value !== null) {
- node.lineNo = buildStoredLineNo(headerRowIndex.value);
- }
- if (hasRealHeaderConfig) {
- node.header = baseHeaders.length ? [...baseHeaders] : [];
- }
- node.ignoreLines = ignoreLines;
- node.groupColumns = groupColumns;
- node.tableRuleList = buildTableRuleListFromState(node.tableRuleList);
- node.childTemplateName = normalizeRuleText(childTemplateName.value) || null;
- node.hasNew = !!node.childTemplateName;
- if (shouldPersistRecognitionBasicInfo) {
- node.funcRegx = buildRecognitionBasicInfoFuncRegx();
- } else if (Object.prototype.hasOwnProperty.call(node, 'funcRegx')) {
- node.funcRegx = null;
- }
- persistFileListToSessionStorage();
- };
- const syncActiveNodeStructure = () => {
- if (structureSyncSuspended.value) return;
- syncNodeStructure(activeNodeInfo.value);
- };
- const hasHeaderFormatError = (header: string) => !!headerFormatValidationMap.value[header]?.invalidTargets.length;
- const getHeaderFormatErrorTitle = (header: string) => headerFormatValidationMap.value[header]?.title || '';
- const getMatchedTargetFieldText = (header: string) =>
- matchedTargetFieldsByHeader.value[header]?.map((item) => item.label).join('、') || '';
- const hasMatchedTargetField = (header: string) => !!matchedTargetFieldsByHeader.value[header]?.length;
- const getMatchedHeaderStatusClass = (header: string) => (hasMatchedTargetField(header) ? 'is-success' : 'is-fail');
- const getMatchedHeaderStatusText = (header: string) => (hasMatchedTargetField(header) ? '✓' : '✕');
- const getMatchedHeaderStatusTitle = (header: string) => (hasMatchedTargetField(header) ? '已匹配' : '未匹配');
- const remapGroupingColumnRules = (
- list: Array<Partial<GroupingColumnDraft> | Record<string, any>> | undefined,
- keyMap: Record<string, string>,
- removedHeaders: Set<string> = new Set(),
- ) =>
- normalizeGroupingColumnsDraft(list).map((item) => {
- const nextHeader = removedHeaders.has(item.header) ? '' : keyMap[item.header] || item.header;
- return createGroupingColumnDraft({
- ...item,
- header: nextHeader,
- title: nextHeader || '',
- });
- });
- const remapSelectRuleHeaderRefs = (
- groups: SelectedRuleItem[][],
- keyMap: Record<string, string>,
- removedHeaders: Set<string> = new Set(),
- ) =>
- groups.map((rules) =>
- rules.map((rule) => {
- if (rule.key !== 'groupingColumns') {
- return rule;
- }
- return {
- ...rule,
- rules: {
- ...rule.rules,
- groupColumns: remapGroupingColumnRules(rule.rules?.groupColumns, keyMap, removedHeaders),
- },
- };
- }),
- );
- const syncHeaderRowInput = () => {
- headerRowInput.value = headerRowIndex.value === null ? '' : String(headerRowIndex.value + 1);
- };
- const applyHeaderRow = (rowIndex: number, options: { silent?: boolean; autoMatch?: boolean } = {}) => {
- const { silent = false, autoMatch = true } = options;
- if (rowIndex < 0 || rowIndex >= sourceTableRows.value.length) {
- syncHeaderRowInput();
- if (!silent) {
- createMessage.warning('请输入正确的表头行号');
- }
- return;
- }
- const previousHeaders = [...rightTableHeaders.value];
- const headerSourceRow = sourceTableRows.value[rowIndex] || ({} as Record<string, string>);
- const nextHeaders = buildHeaderLabelsByRow(previousHeaders, headerSourceRow, addedColumns.value);
- if (
- headerRowIndex.value === rowIndex &&
- nextHeaders.length === previousHeaders.length &&
- nextHeaders.every((header, index) => header === previousHeaders[index])
- ) {
- selectedRowIndex.value = rowIndex;
- syncHeaderRowInput();
- return;
- }
- const headerKeyMap = previousHeaders.reduce(
- (acc, header, index) => {
- acc[header] = nextHeaders[index];
- return acc;
- },
- {} as Record<string, string>,
- );
- const fixedHeaderSet = new Set(addedColumns.value);
- sourceTableRows.value = sourceTableRows.value.map((row, index) => {
- const nextRow: Record<string, string> = {};
- previousHeaders.forEach((header, colIndex) => {
- const nextHeader = nextHeaders[colIndex];
- if (fixedHeaderSet.has(header)) {
- nextRow[nextHeader] = String(row[header] ?? '');
- return;
- }
- nextRow[nextHeader] = index === rowIndex ? nextHeader : String(row[header] ?? '');
- });
- return nextRow;
- });
- rightTableHeaders.value = nextHeaders;
- generatedColumnHeaderRowMap.value = addedColumns.value.reduce(
- (acc, item) => {
- const nextKey = headerKeyMap[item] || item;
- acc[nextKey] = getGeneratedColumnHeaderRowIndex(item);
- return acc;
- },
- {} as Record<string, number | null>,
- );
- addedColumns.value = addedColumns.value.map((item) => headerKeyMap[item] || item);
- selectedAddedColumn.value = selectedAddedColumn.value ? headerKeyMap[selectedAddedColumn.value] || '' : '';
- fieldMapping.value = Object.entries(fieldMapping.value).reduce(
- (acc, [key, value]) => {
- acc[key] = headerKeyMap[value] || value;
- return acc;
- },
- {} as Record<string, string>,
- );
- addedColumnConfig.value = Object.entries(renameRecordKeys(addedColumnConfig.value, headerKeyMap)).reduce(
- (acc, [key, value]) => {
- acc[key] = {
- ...value,
- contentFields: value.contentFields.map((field) => headerKeyMap[field] || field),
- };
- return acc;
- },
- {} as Record<string, { mode: 'ref' | 'custom'; contentFields: string[]; customContent: string }>,
- );
- splitColumnConfig.value = Object.entries(renameRecordKeys(splitColumnConfig.value, headerKeyMap)).reduce(
- (acc, [key, value]) => {
- acc[key] = {
- ...value,
- source: headerKeyMap[value.source] || value.source,
- targetName1: headerKeyMap[value.targetName1] || value.targetName1,
- targetName2: headerKeyMap[value.targetName2] || value.targetName2,
- };
- return acc;
- },
- {} as Record<
- string,
- { source: string; delimiterType: string; delimiterValue: string; targetName1: string; targetName2: string }
- >,
- );
- selectRules.value = remapSelectRuleHeaderRefs(selectRules.value, headerKeyMap);
- groupingColumnsDraft.value = remapGroupingColumnRules(groupingColumnsDraft.value, headerKeyMap);
- if (splitColumnForm.value.source) {
- splitColumnForm.value.source = headerKeyMap[splitColumnForm.value.source] || splitColumnForm.value.source;
- }
- headerRowIndex.value = rowIndex;
- selectedRowIndex.value = rowIndex;
- syncHeaderRowInput();
- rebuildGeneratedColumnsInSourceRows();
- runRulePipeline();
- if (autoMatch) {
- autoMatchFields(false);
- }
- if (!silent) {
- createMessage.success(`已将第${rowIndex + 1}行设为表头行`);
- }
- };
- const applyHeaderRowInput = () => {
- const rawValue = headerRowInput.value.trim();
- if (!rawValue) {
- syncHeaderRowInput();
- return;
- }
- const rowNumber = Number(rawValue);
- if (!Number.isInteger(rowNumber)) {
- syncHeaderRowInput();
- createMessage.warning('请输入正确的表头行号');
- return;
- }
- applyHeaderRow(rowNumber - 1);
- };
- const ensureCurrentHeaderRowConfigured = (warningText = '请先填写表头所在行,再进行模板匹配') => {
- if (headerRowIndex.value !== null) return true;
- const rawValue = headerRowInput.value.trim();
- if (!rawValue) {
- createMessage.warning(warningText);
- return false;
- }
- const rowNumber = Number(rawValue);
- if (!Number.isInteger(rowNumber) || rowNumber <= 0 || rowNumber > sourceTableRows.value.length) {
- createMessage.warning('请输入正确的表头行号');
- return false;
- }
- applyHeaderRow(rowNumber - 1, { silent: true, autoMatch: false });
- return headerRowIndex.value !== null;
- };
- const mockGridTemplate = computed(() =>
- [`${mockIndexColumnWidth}px`, ...rightTableHeaders.value.map(() => `${mockDataColumnWidth}px`)].join(' '),
- );
- const getColumnRuleBadgeState = (colIndex: number) => {
- if (colIndex < 0) return false;
- const rules = sanitizeSelectedRuleList(selectRules.value[colIndex] || []);
- const hasEnabledRules = rules.some((rule) => rule && rule.key && rule.enabled);
- if (hasEnabledRules) return 'active';
- return rules.some((rule) => rule && rule.key) ? 'draft' : '';
- };
- const getColumnRuleBadgeTitle = (colIndex: number) => {
- const state = getColumnRuleBadgeState(colIndex);
- if (state === 'active') return '该列已启用清洗流程';
- if (state === 'draft') return '该列已添加清洗规则,待启用';
- return '';
- };
- const addColumnDialogOpen = ref(false);
- const splitColumnDialogOpen = ref(false);
- const deleteColumnDialogOpen = ref(false);
- const addColumnMode = ref<'add' | 'edit'>('add');
- const addColumnForm = ref<{
- name: string;
- mode: 'ref' | 'custom';
- contentFields: string[];
- customContent: string;
- }>({
- name: '',
- mode: 'ref',
- contentFields: [],
- customContent: '',
- });
- const selectedSourceField = ref('');
- const selectedContentIndex = ref(-1);
- const draggingContentIndex = ref<number | null>(null);
- const dragOverContentIndex = ref<number | null>(null);
- const availableSourceHeaders = computed(() =>
- rightTableHeaders.value.filter((item) => item && !addedColumns.value.includes(item)),
- );
- const hasDuplicateHeaderName = (name: string, excludedRawNames: string[] = []) => {
- const normalizedName = normalizeColumnNameForCompare(name);
- if (!normalizedName) return false;
- return rightTableHeaders.value.some(
- (header) => !excludedRawNames.includes(header) && normalizeColumnNameForCompare(header) === normalizedName,
- );
- };
- const splitColumnForm = ref({
- source: '',
- targetName1: '',
- targetName2: '',
- delimiterType: 'length',
- delimiterValue: '',
- });
- const mappingPopoverOpenKey = ref<string>('');
- const originFilterKeyword = ref('');
- const resolveTargetKeyByTableRule = (rule: TreeTableRuleInfo) => {
- const fieldNameEn = normalizeRuleText(rule.fieldNameEn);
- if (fieldNameEn) {
- const matchedByKey = fixedTargetFields.value.find((item) => item.key === fieldNameEn);
- if (matchedByKey) return matchedByKey.key;
- }
- const fieldNameCn = normalizeRuleText(rule.fieldNameCn);
- if (fieldNameCn) {
- const matchedByLabel =
- fixedTargetFields.value.find((item) => item.label === fieldNameCn) ||
- fixedTargetFields.value.find((item) => item.label.includes(fieldNameCn) || fieldNameCn.includes(item.label));
- if (matchedByLabel) return matchedByLabel.key;
- }
- return '';
- };
- const applyTableRuleMapping = (list?: TreeTableRuleInfo[] | null) => {
- const tableRules = normalizeTableRuleList(list);
- if (!tableRules.length || !fixedTargetFields.value.length) return false;
- const headerSet = new Set(rightTableHeaders.value);
- const nextMapping = tableRules.reduce(
- (acc, item) => {
- const targetKey = resolveTargetKeyByTableRule(item);
- const sourceField = normalizeRuleText(item.fileColName);
- if (!targetKey || !sourceField || !headerSet.has(sourceField)) return acc;
- acc[targetKey] = sourceField;
- return acc;
- },
- {} as Record<string, string>,
- );
- if (!Object.keys(nextMapping).length) return false;
- fieldMapping.value = { ...fieldMapping.value, ...nextMapping };
- return true;
- };
- const isTargetMatchedByRecognitionBasicInfo = (targetKey: string) =>
- recognitionBasicInfoMatchedTargetKeySet.value.has(targetKey);
- const isTargetHandledByRecognitionRule = (targetKey: string) =>
- recognitionRuleHandledTargetKeySet.value.has(targetKey);
- const closeMappingPopoverPreview = () => {
- mappingPopoverOpenKey.value = '';
- originFilterKeyword.value = '';
- clearOriginHoverPreviewState();
- };
- const childTemplateName = ref('');
- const showDuplicateChildTemplateWarning = (names: string[]) => {
- const previewNames = names.slice(0, 3).join('、');
- const suffix = names.length > 3 ? ` 等 ${names.length} 个名称` : '';
- Modal.warning({
- title: '子模板名称重复',
- content: `当前清洗结果中存在重复的子模板名称:${previewNames}${suffix},请修改后再导入数据。`,
- centered: true,
- });
- };
- const collectChildTemplateNames = (nodes: TreeFileInfo[] = []) => {
- const names: string[] = [];
- const walk = (items: TreeFileInfo[] = []) => {
- items.forEach((item) => {
- const childName = normalizeRuleText(item.childTemplateName);
- if (childName) {
- names.push(childName);
- }
- if (Array.isArray(item.children) && item.children.length) {
- walk(item.children);
- }
- });
- };
- walk(nodes);
- return names;
- };
- const findDuplicateChildTemplateNames = (names: string[]) => {
- const duplicateNameMap = new Map<string, string>();
- const counter = new Map<string, number>();
- names.forEach((name) => {
- const key = normalizeTemplateNameKey(name);
- if (!key) return;
- const nextCount = (counter.get(key) || 0) + 1;
- counter.set(key, nextCount);
- if (nextCount > 1 && !duplicateNameMap.has(key)) {
- duplicateNameMap.set(key, name);
- }
- });
- return Array.from(duplicateNameMap.values());
- };
- const ensureChildTemplateNamesAvailable = async (nodes: TreeFileInfo[] = []) => {
- const childTemplateNames = collectChildTemplateNames(nodes);
- if (!childTemplateNames.length) return true;
- const duplicateNames = findDuplicateChildTemplateNames(childTemplateNames);
- if (duplicateNames.length) {
- showDuplicateChildTemplateWarning(duplicateNames);
- return false;
- }
- const uniqueNameMap = new Map<string, string>();
- childTemplateNames.forEach((name) => {
- const key = normalizeTemplateNameKey(name);
- if (key && !uniqueNameMap.has(key)) {
- uniqueNameMap.set(key, name);
- }
- });
- for (const name of uniqueNameMap.values()) {
- const available = await checkTemplateNameAvailable(name);
- if (!available) {
- showChildTemplateExistsWarning(name);
- return false;
- }
- }
- return true;
- };
- const recognitionBasicInfoBaseFuncRegx = ref('');
- const recognitionBasicInfoManualVisible = ref(false);
- // Phase 4:识别规则域下沉到 composable。页面同名解构接住,原有代码与模板零改动。
- 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 });
- const { activeRecognitionRule, buildNodeCacheState, headerRowInput, nodeStateCache, recognitionBasicInfoExpanded, recognitionPanelExpanded, recognitionRuleTabs, saveCurrentNodeState, showRecognitionPanel, touchNodeCache } = recognitionApi;
- const requiredTotal = computed(() => fixedTargetFields.value.filter((item) => isMappingTargetRequired(item)).length);
- const requiredMatchedCount = computed(
- () => fixedTargetFields.value.filter((item) => isMappingTargetRequired(item) && isTargetMatched(item.key)).length,
- );
- const matchedFieldCount = computed(() => fixedTargetFields.value.filter((item) => isTargetMatched(item.key)).length);
- const buildRecognitionDirectionConf = (entries?: RecognitionDirectionItem[]) => {
- if (!entries?.length) return '';
- const payload = entries.reduce(
- (acc, item) => {
- const label = normalizeRecognitionConfigText(item.label);
- if (!label) return acc;
- acc[label] = normalizeRecognitionConfigText(item.value);
- return acc;
- },
- {} as Record<string, string>,
- );
- return Object.keys(payload).length ? JSON.stringify(payload) : '';
- };
- const applyRecognitionDirectionState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
- recognitionDirectionFields.value = resolveRecognitionDirectionFields(node, options);
- };
- const resolveRecognitionBasicInfoSource = (
- node?: TreeFileInfo | null,
- options: { preferTemplate?: boolean } = {},
- ) => {
- const templateFuncRegx = normalizeRecognitionConfigText(
- templateDetail.value?.funcRegx || activeTemplateItem.value?.funcRegx,
- );
- const hasNodeFuncRegx = hasOwnRecognitionBasicInfoConfig(node);
- const nodeFuncRegx = normalizeRecognitionConfigText(node?.funcRegx);
- if (options.preferTemplate) {
- return templateFuncRegx;
- }
- if (hasNodeFuncRegx) {
- return nodeFuncRegx;
- }
- return templateFuncRegx;
- };
- const applyRecognitionBasicInfoState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
- const templateId = normalizeTemplateId(
- node?.templateId ?? templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
- );
- const parentTemplateId = getSelectedTemplateMainId(node?.mainId);
- const parentTemplate = parentTemplateId
- ? resolveTemplateInfoById(parentTemplateId)
- : resolveParentTemplateInfoById(templateId);
- const source = resolveRecognitionBasicInfoSource(node, options);
- const hasNodeFuncRegx =
- hasOwnRecognitionBasicInfoConfig(node) &&
- !isSameRecognitionBasicInfoConfig(
- node?.funcRegx,
- templateDetail.value?.funcRegx || activeTemplateItem.value?.funcRegx,
- ) &&
- !isSameRecognitionBasicInfoConfig(node?.funcRegx, parentTemplate?.funcRegx);
- const shouldClearFilledContent = options.preferTemplate || !hasNodeFuncRegx;
- recognitionBasicInfoBaseFuncRegx.value = source;
- const parsedRows = parseRecognitionBasicInfoRows(source, parentTemplate?.funcRegx);
- recognitionBasicInfoRows.value = shouldClearFilledContent
- ? clearRecognitionBasicInfoFilledContent(parsedRows)
- : parsedRows;
- recognitionBasicInfoExpanded.value = !!recognitionBasicInfoRows.value.length;
- };
- const resolveRecognitionAmountZfEnabled = (
- node?: TreeFileInfo | null,
- options: { preferTemplate?: boolean } = {},
- ) => {
- const tableRule = findTransAmountTableRule(node?.tableRuleList);
- const tableRuleZf = getRecordZfValue(tableRule);
- const templateField = findTransAmountTemplateFieldMeta(templateDetail.value || activeTemplateItem.value);
- const templateZf = getRecordZfValue(templateField);
- if (options.preferTemplate) {
- return templateZf !== undefined
- ? getZfFlag(templateZf)
- : tableRuleZf !== undefined
- ? getZfFlag(tableRuleZf)
- : false;
- }
- return tableRuleZf !== undefined
- ? getZfFlag(tableRuleZf)
- : templateZf !== undefined
- ? getZfFlag(templateZf)
- : false;
- };
- const applyRecognitionAmountZfState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
- recognitionAmountZfEnabled.value = resolveRecognitionAmountZfEnabled(node, options);
- };
- const buildRecognitionBasicInfoFuncRegx = () => {
- if (!recognitionBasicInfoRows.value.length) {
- return recognitionBasicInfoBaseFuncRegx.value;
- }
- if (!hasRecognitionBasicInfoFilledContent(recognitionBasicInfoRows.value)) {
- return '';
- }
- const payload = recognitionBasicInfoRows.value.reduce(
- (acc, item) => {
- const targetField = resolveRecognitionBasicInfoTargetField(item.targetField);
- if (!targetField) return acc;
- acc[targetField] = [
- normalizeRecognitionBasicInfoText(item.headerMatch),
- normalizeRecognitionBasicInfoText(item.endFlag),
- ];
- return acc;
- },
- {} as Record<string, string[]>,
- );
- return Object.keys(payload).length ? JSON.stringify(payload) : '';
- };
- const applyNodePreset = async (item?: TreeFileInfo | null) => {
- const previewDataset = await loadPreviewDatasetFromFile(item);
- const hasPresetHeaders = previewDataset ? applyPreviewDataset(previewDataset) : applyPresetHeadersFromNode(item);
- restoreStoredColumnRules(item?.tableRuleList);
- applyRecognitionDirectionState(item);
- applyRecognitionBasicInfoState(item);
- applyRecognitionAmountZfState(item);
- childTemplateName.value = normalizeRuleText(item?.childTemplateName);
- const headerRow = normalizeStoredLineNoToRowIndex(item?.lineNo);
- if (headerRow !== null && !hasPresetHeaders) {
- applyHeaderRow(headerRow, { silent: true, autoMatch: false });
- } else {
- headerRowIndex.value = headerRow;
- selectedRowIndex.value = headerRow;
- syncHeaderRowInput();
- }
- if (!applyTableRuleMapping(item?.tableRuleList)) {
- autoMatchFields(false);
- }
- restoreStoredFunRules(item?.tableRuleList);
- restoreFileLevelRules(item);
- runRulePipeline();
- };
- const DEFAULT_TEMPLATE_CATEGORY = '全部分类';
- const templateModalOpen = ref(false);
- const activeTemplateTab = ref<TemplateTab>('template');
- const activeCategory = ref(DEFAULT_TEMPLATE_CATEGORY);
- const templateKeyword = ref('');
- const templateSource = ref<number>(1);
- const modalTemplateDetail = ref<TableInfo | null>(null);
- const templatePreviewLoading = ref(false);
- let templatePreviewRequestToken = 0;
- const categories = computed(() => {
- const set = new Set<string>(
- templateCategoryOptions.value.length ? templateCategoryOptions.value : [DEFAULT_TEMPLATE_CATEGORY],
- );
- templateList.value.forEach((t) => set.add(t.classify || FALLBACK_TEMPLATE_CATEGORY));
- return Array.from(set);
- });
- const showTemplateTabSwitch = computed(
- () => activeCategory.value === DEFAULT_TEMPLATE_CATEGORY || pluginCategorySet.value.has(activeCategory.value),
- );
- const resolveNodeTargetFields = (item?: TreeFileInfo | null, preferredTemplateId?: string | number | null) => {
- const tableRules = normalizeTableRuleList(item?.tableRuleList);
- const templateId = normalizeTemplateId(preferredTemplateId ?? item?.templateId);
- const template = (templateId && (getTemplateListItemById(templateId) || mockTemplateMap[templateId])) || null;
- const baseFields = tableRules.length
- ? buildTargetFieldsFromRuleList(tableRules)
- : buildTargetFieldsFromTemplateInfo(template);
- return applyTableRuleRequiredFlags(baseFields, tableRules);
- };
- const getRequiredMatchStats = (
- fields: MappingTargetField[],
- mapping: Record<string, string>,
- recognitionRows: RecognitionBasicInfoRow[] = [],
- amountZfEnabled = false,
- ) => {
- const matchedByRecognitionBasicInfo = buildRecognitionBasicInfoMatchedTargetKeySet(recognitionRows, fields);
- const handledTargetKeySet = new Set(matchedByRecognitionBasicInfo);
- buildAmountZfHandledTargetKeySet(fields, amountZfEnabled).forEach((key) => {
- handledTargetKeySet.add(key);
- });
- const effectiveMapping = buildEffectiveFieldMapping(mapping, handledTargetKeySet);
- const requiredFields = fields.filter((item) => item.required);
- const requiredTotal = requiredFields.length;
- const requiredMatched = requiredFields.filter(
- (item) => handledTargetKeySet.has(item.key) || !!effectiveMapping[item.key],
- ).length;
- const matchedCount = fields.filter(
- (item) => handledTargetKeySet.has(item.key) || !!effectiveMapping[item.key],
- ).length;
- return { requiredTotal, requiredMatched, matchedCount };
- };
- const getSheetNodeVisualState = (item?: TreeFileInfo | null): SheetNodeVisualState => {
- if (!isTreeSheetNode(item)) return 'fail';
- const nodeKey = getTreeNodeKey(item);
- if (analyzingNodeKeys.value.includes(nodeKey)) {
- return 'analyzing';
- }
- const defaultState = getFileStatusVisualState(item.fileStatus) || (isSheetMatchComplete(item) ? 'success' : 'fail');
- const initialSignature = initialNodeStatusSignatureMap.get(nodeKey);
- if (activeNodeKey.value === nodeKey) {
- const currentState = buildNodeCacheState();
- const hasEditedState = !initialSignature || buildNodeStatusSignature(currentState) !== initialSignature;
- if (!hasEditedState) {
- return defaultState;
- }
- return isMatchSuccessful(requiredTotal.value, requiredMatchedCount.value, matchedFieldCount.value)
- ? 'success'
- : 'fail';
- }
- const cached = nodeStateCache.get(nodeKey);
- if (cached) {
- const hasEditedState = !initialSignature || buildNodeStatusSignature(cached) !== initialSignature;
- if (!hasEditedState) {
- return defaultState;
- }
- const {
- requiredTotal: cachedRequiredTotal,
- requiredMatched: cachedRequiredMatched,
- matchedCount: cachedMatchedCount,
- } = getRequiredMatchStats(
- resolveNodeTargetFields(item, cached.selectedTemplateId),
- cached.fieldMapping,
- cached.recognitionBasicInfoRows,
- cached.recognitionAmountZfEnabled,
- );
- return isMatchSuccessful(cachedRequiredTotal, cachedRequiredMatched, cachedMatchedCount) ? 'success' : 'fail';
- }
- return defaultState;
- };
- const syncModalTemplateItem = (preferredId?: string | number | null) => {
- const currentBaseList = filteredTemplateList.value;
- if (activeTemplateTab.value === 'plugin') {
- const currentPluginId = modalTemplateItem.value?.id;
- const matchedPlugin = currentBaseList.find((item) => String(item.id) === String(currentPluginId)) || null;
- modalTemplateItem.value = matchedPlugin;
- return modalTemplateItem.value;
- }
- const preferredTemplateId = normalizeTemplateId(preferredId);
- const currentTemplateId = normalizeTemplateId(modalTemplateItem.value?.id);
- if (preferredTemplateId) {
- const matched = currentBaseList.find((item) => normalizeTemplateId(item.id) === preferredTemplateId) || null;
- if (matched) {
- modalTemplateItem.value = matched;
- return matched;
- }
- }
- if (currentTemplateId) {
- const currentMatched = currentBaseList.find((item) => normalizeTemplateId(item.id) === currentTemplateId) || null;
- if (currentMatched) {
- modalTemplateItem.value = currentMatched;
- return currentMatched;
- }
- }
- modalTemplateItem.value = currentBaseList[0] || null;
- return modalTemplateItem.value;
- };
- const resolveTemplateInfoById = (templateId?: string | number | null) => {
- const id = normalizeTemplateId(templateId);
- if (!id) return null;
- const activeDetail = normalizeTemplateId(templateDetail.value?.id) === id ? templateDetail.value : null;
- const activeItem = normalizeTemplateId(activeTemplateItem.value?.id) === id ? activeTemplateItem.value : null;
- const cachedDetail = templateDetailCache.get(id) || null;
- const listItem = getTemplateListItemById(id) || mockTemplateMap[id] || null;
- return (
- mergeTemplateInfo(activeItem || listItem || activeDetail, activeDetail || cachedDetail) ||
- activeDetail ||
- cachedDetail ||
- activeItem ||
- listItem ||
- null
- );
- };
- const resolveParentTemplateInfoById = (templateId?: string | number | null) => {
- const template = resolveTemplateInfoById(templateId);
- const mainId = normalizeTemplateId((template as TableInfo | null)?.mainId);
- return (mainId && resolveTemplateInfoById(mainId)) || template;
- };
- const getSelectedTemplateMainId = (fallbackMainId?: string | number | null) => {
- if (templateDetail.value && Object.prototype.hasOwnProperty.call(templateDetail.value, 'mainId')) {
- return normalizeTemplateId(templateDetail.value.mainId);
- }
- if (activeTemplateItem.value && Object.prototype.hasOwnProperty.call(activeTemplateItem.value, 'mainId')) {
- return normalizeTemplateId(activeTemplateItem.value.mainId);
- }
- return normalizeTemplateId(fallbackMainId);
- };
- const syncModalTemplatePreviewState = (
- template?: TemplateListItem | null,
- options: { preferAppliedDetail?: boolean } = {},
- ) => {
- modalTemplateItem.value = template || null;
- modalTemplateDetail.value = null;
- templatePreviewLoading.value = false;
- templatePreviewRequestToken += 1;
- const id = normalizeTemplateId(template?.id);
- if (!id || Number(template?.id) < 0) return null;
- if (options.preferAppliedDetail && normalizeTemplateId(templateDetail.value?.id) === id && templateDetail.value) {
- modalTemplateDetail.value = templateDetail.value;
- return modalTemplateDetail.value;
- }
- const cachedDetail = templateDetailCache.get(id);
- if (cachedDetail) {
- modalTemplateDetail.value = cachedDetail;
- return modalTemplateDetail.value;
- }
- return null;
- };
- const loadTemplatePreviewDetail = async (
- template?: TemplateListItem | null,
- options: { preferAppliedDetail?: boolean } = {},
- ) => {
- const existingDetail = syncModalTemplatePreviewState(template, options);
- if (existingDetail) {
- return existingDetail;
- }
- const id = normalizeTemplateId(template?.id);
- if (!id || Number(template?.id) < 0) return null;
- const requestToken = ++templatePreviewRequestToken;
- templatePreviewLoading.value = true;
- try {
- const detail = await tplDetail({ id });
- if (requestToken !== templatePreviewRequestToken) return null;
- if (detail) {
- templateDetailCache.set(id, detail);
- modalTemplateDetail.value = detail;
- mergeTemplateListItem(mergeTemplateInfo(template, detail));
- modalTemplateItem.value = getTemplateListItemById(id) || template || null;
- }
- return modalTemplateDetail.value;
- } catch (error) {
- if (requestToken === templatePreviewRequestToken) {
- modalTemplateDetail.value = null;
- }
- return null;
- } finally {
- if (requestToken === templatePreviewRequestToken) {
- templatePreviewLoading.value = false;
- }
- }
- };
- const closeTemplateModal = () => {
- templateModalOpen.value = false;
- templatePreviewRequestToken += 1;
- templatePreviewLoading.value = false;
- modalTemplateDetail.value = null;
- modalTemplateItem.value = null;
- };
- const loadTemplateDetail = async (
- templateId?: string | number | null,
- options: { silent?: boolean; requestDetail?: boolean } = {},
- ) => {
- const id = normalizeTemplateId(templateId ?? selectedTemplateId.value);
- if (!id) {
- syncSelectedTemplateState(id);
- return templateDetail.value;
- }
- if (!templateListLoaded.value && !templateListLoading.value) {
- await loadTemplateList({ silent: true, preferredId: id });
- }
- const { matchedListItem, cachedDetail } = syncSelectedTemplateState(id);
- if (!id || !options.requestDetail || cachedDetail) {
- return templateDetail.value;
- }
- try {
- const resp = await tplDetail({ id });
- const detail = resp || null;
- if (detail) {
- templateDetailCache.set(id, detail);
- }
- templateDetail.value = detail
- ? mergeTemplateInfo(matchedListItem, detail)
- : matchedListItem || mockTemplateMap[id] || null;
- templateDetailFromMock.value = !detail && !matchedListItem && !!mockTemplateMap[id];
- if (templateDetail.value) {
- mergeTemplateListItem(templateDetail.value);
- activeTemplateItem.value = getTemplateListItemById(id) || (templateDetail.value as TemplateListItem);
- }
- } catch (error) {
- templateDetail.value = matchedListItem || mockTemplateMap[id] || null;
- templateDetailFromMock.value = !matchedListItem && !!mockTemplateMap[id];
- activeTemplateItem.value = (matchedListItem || (templateDetail.value as TemplateListItem | null)) ?? null;
- if (!options.silent) {
- createMessage.warning('模板详情接口暂不可用,已使用当前列表匹配结果');
- }
- }
- return templateDetail.value;
- };
- const openTemplateModal = async () => {
- templateModalOpen.value = true;
- activeTemplateTab.value = 'template';
- activeCategory.value = DEFAULT_TEMPLATE_CATEGORY;
- templateKeyword.value = '';
- templateSource.value = 0;
- await Promise.all([
- loadTemplateCategories(),
- loadTemplateList({ silent: true, preferredId: selectedTemplateId.value }),
- ]);
- const previewTarget = syncModalTemplateItem(selectedTemplateId.value);
- await loadTemplatePreviewDetail(previewTarget, { preferAppliedDetail: true });
- };
- watch([activeCategory, templateSource, templateKeyword, activeTemplateTab], async () => {
- if (!templateModalOpen.value) return;
- if (!showTemplateTabSwitch.value) {
- activeTemplateTab.value = 'template';
- }
- const previewTarget = syncModalTemplateItem(
- activeTemplateTab.value === 'plugin' ? undefined : selectedTemplateId.value,
- );
- await loadTemplatePreviewDetail(previewTarget, {
- preferAppliedDetail:
- activeTemplateTab.value !== 'plugin' && normalizeTemplateId(previewTarget?.id) === selectedTemplateId.value,
- });
- });
- watch(activeTemplateTab, (tab) => {
- if (!templateModalOpen.value) return;
- if (tab === 'plugin') {
- modalTemplateDetail.value = null;
- templatePreviewLoading.value = false;
- templatePreviewRequestToken += 1;
- }
- });
- watch(categories, (nextCategories) => {
- if (!nextCategories.includes(activeCategory.value)) {
- activeCategory.value = DEFAULT_TEMPLATE_CATEGORY;
- }
- });
- const activeNodeInfo = computed(() => {
- return activeNodeKey.value ? findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw || null : null;
- });
- // Phase 4:模板域(匹配条 / 创建子模板 / 空态)下沉到 composable。页面同名解构接住,原有代码与模板零改动。
- 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 });
- 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;
- // Phase 4:字段映射域下沉到 composable。页面同名解构接住,原有代码与模板零改动。
- 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 });
- const { activeMappingTargetKey, autoMatchFields, clearOriginHoverPreviewState, focusPreviewColumn, headerFormatValidationMap, isMappingTargetRequired, isTargetMatched, resolveRecognitionDirectionFields, tableMockRef } = mappingApi;
- const activeRecognitionRuleDesc = computed(() => {
- const templateName = currentTemplateName.value === '-' ? '当前模板' : currentTemplateName.value;
- const ruleLabel = activeRecognitionRule.value || recognitionRuleTabs.value[0] || '识别规则';
- return `围绕「${templateName}」预置 ${ruleLabel} 的识别配置,后续可结合导入样本继续沉淀成子模板。`;
- });
- watch(
- recognitionRuleTabs,
- (tabs) => {
- if (!tabs.length) {
- activeRecognitionRule.value = '';
- return;
- }
- if (!tabs.includes(activeRecognitionRule.value)) {
- activeRecognitionRule.value = tabs[0];
- }
- },
- { immediate: true },
- );
- watch(
- fixedTargetFields,
- (fields) => {
- if (activeMappingTargetKey.value && !fields.some((item) => item.key === activeMappingTargetKey.value)) {
- activeMappingTargetKey.value = '';
- }
- },
- { deep: true },
- );
- const startResize = (type: 'left' | 'middle', event: MouseEvent) => {
- event.preventDefault();
- resizeState.value = {
- type,
- startX: event.clientX,
- startLeft: leftPanelWidth.value,
- startMiddle: middlePanelWidth.value,
- };
- document.body.classList.add('is-resizing-layout');
- document.body.style.cursor = 'col-resize';
- };
- const applyResize = () => {
- if (!resizeState.value.type || pendingClientX.value === null) return;
- const deltaX = pendingClientX.value - resizeState.value.startX;
- if (resizeState.value.type === 'left' && !leftCollapsed.value) {
- const next = resizeState.value.startLeft + deltaX;
- leftPanelWidth.value = Math.min(maxLeftWidth, Math.max(minLeftWidth, next));
- }
- if (resizeState.value.type === 'middle') {
- const next = resizeState.value.startMiddle + deltaX;
- middlePanelWidth.value = Math.min(maxMiddleWidth, Math.max(minMiddleWidth, next));
- }
- rafId.value = null;
- };
- const onResizeMouseMove = (event: MouseEvent) => {
- if (!resizeState.value.type) return;
- event.preventDefault();
- pendingClientX.value = event.clientX;
- if (rafId.value === null) {
- rafId.value = window.requestAnimationFrame(applyResize);
- }
- };
- const stopResize = () => {
- resizeState.value.type = '';
- resizeState.value.startX = 0;
- pendingClientX.value = null;
- if (rafId.value !== null) {
- window.cancelAnimationFrame(rafId.value);
- rafId.value = null;
- }
- document.body.classList.remove('is-resizing-layout');
- document.body.style.cursor = '';
- };
- const openAddColumn = () => {
- addColumnMode.value = 'add';
- addColumnDialogOpen.value = true;
- addColumnForm.value = { name: '', mode: 'ref', contentFields: [], customContent: '' };
- selectedSourceField.value = availableSourceHeaders.value[0] || '';
- selectedContentIndex.value = -1;
- draggingContentIndex.value = null;
- dragOverContentIndex.value = null;
- };
- const openSplitColumn = () => {
- if (!availableSourceHeaders.value.length) {
- createMessage.warning('当前没有可供拆分的原始列');
- return;
- }
- splitColumnDialogOpen.value = true;
- splitColumnForm.value = {
- source: availableSourceHeaders.value[0] || '',
- targetName1: '',
- targetName2: '',
- delimiterType: 'length',
- delimiterValue: '',
- };
- };
- const openEditColumn = () => {
- if (!selectedAddedColumn.value) {
- createMessage.warning('请先选中一个新增列');
- return;
- }
- addColumnMode.value = 'edit';
- addColumnDialogOpen.value = true;
- const config = addedColumnConfig.value[selectedAddedColumn.value];
- addColumnForm.value = {
- name: selectedAddedColumn.value,
- mode: config?.mode || 'ref',
- contentFields: (config?.contentFields || []).filter((field) => availableSourceHeaders.value.includes(field)),
- customContent: config?.customContent || '',
- };
- selectedSourceField.value = availableSourceHeaders.value[0] || '';
- selectedContentIndex.value = -1;
- draggingContentIndex.value = null;
- dragOverContentIndex.value = null;
- };
- const openDeleteColumn = () => {
- if (!selectedAddedColumn.value) {
- createMessage.warning('请先选中一个新增列');
- return;
- }
- deleteColumnDialogOpen.value = true;
- };
- const buildEnabledGroupingColumnsPayload = () =>
- rightTableHeaders.value.reduce(
- (acc, header) => {
- if (!addedColumns.value.includes(header)) {
- return acc;
- }
- const groupColumns = getEnabledGroupingColumnsForHeader(header);
- if (!groupColumns.length) {
- return acc;
- }
- acc.push({
- targetHeader: header,
- groupFlag: true,
- groupColumns: cloneDeepSafe(groupColumns),
- });
- return acc;
- },
- [] as Array<{
- targetHeader: string;
- groupFlag: boolean;
- groupColumns: Array<Record<string, any>>;
- }>,
- );
- const confirmDeleteColumn = () => {
- const name = selectedAddedColumn.value;
- if (!name) return;
- rightTableHeaders.value = rightTableHeaders.value.filter((h) => h !== name);
- addedColumns.value = addedColumns.value.filter((h) => h !== name);
- delete addedColumnConfig.value[name];
- const deletedSplitConfig = splitColumnConfig.value[name];
- delete splitColumnConfig.value[name];
- if (deletedSplitConfig) {
- [deletedSplitConfig.targetName1, deletedSplitConfig.targetName2].filter(Boolean).forEach((targetName) => {
- const targetConfig = splitColumnConfig.value[targetName];
- if (targetName === name || !targetConfig) return;
- splitColumnConfig.value[targetName] = {
- ...targetConfig,
- targetName1: targetConfig.targetName1 === name ? '' : targetConfig.targetName1,
- targetName2: targetConfig.targetName2 === name ? '' : targetConfig.targetName2,
- };
- });
- }
- delete generatedColumnHeaderRowMap.value[name];
- sourceTableRows.value.forEach((row) => {
- delete row[name];
- });
- Object.keys(fieldMapping.value).forEach((key) => {
- if (fieldMapping.value[key] === name) {
- fieldMapping.value[key] = '';
- }
- });
- selectRules.value = remapSelectRuleHeaderRefs(selectRules.value, {}, new Set([name]));
- groupingColumnsDraft.value = remapGroupingColumnRules(groupingColumnsDraft.value, {}, new Set([name]));
- selectedAddedColumn.value = addedColumns.value[0] || '';
- currentColumn.value = selectedAddedColumn.value
- ? rightTableHeaders.value.findIndex((h) => h === selectedAddedColumn.value)
- : null;
- selectedRowIndex.value = null;
- runRulePipeline();
- deleteColumnDialogOpen.value = false;
- createMessage.success('删除列成功');
- };
- const onClickHeader = (header: string) => {
- const index = rightTableHeaders.value.findIndex((h) => h === header);
- if (index < 0) return;
- closeMappingPopoverPreview();
- focusPreviewColumn(header, index, { resetSelectedRow: true });
- };
- const onClickCell = (rowIndex: number, header: string, colIndex: number) => {
- closeMappingPopoverPreview();
- selectedRowIndex.value = rowIndex;
- focusPreviewColumn(header, colIndex, { resetSelectedRow: false });
- };
- const toggleLeft = () => {
- leftCollapsed.value = !leftCollapsed.value;
- };
- const handleSelect = (keys: (string | number)[]) => {
- const rawKey = keys[0] ? String(keys[0]) : '';
- const nextKey = rawKey ? resolveSelectableTreeKey(rawKey) : '';
- selectedKeys.value = nextKey ? [nextKey] : [];
- if (nextKey) {
- switchNodeState(nextKey);
- }
- };
- const handleImportData = async () => {
- saveCurrentNodeState();
- syncActiveNodeStructure();
- const sanitizedFileInfos = sanitizeFileInfoTree(fileList.value, { keepRuleKey: false });
- if (!sanitizedFileInfos.length) {
- createMessage.warning('未找到可导入的数据,请先上传并完成模板匹配');
- return;
- }
- const matchedSheetsWithoutHeaderRow = collectMatchedSheetsWithoutHeaderRow(sanitizedFileInfos);
- if (matchedSheetsWithoutHeaderRow.length) {
- const previewNames = matchedSheetsWithoutHeaderRow.slice(0, 3).join('、');
- const suffix =
- matchedSheetsWithoutHeaderRow.length > 3 ? ` 等 ${matchedSheetsWithoutHeaderRow.length} 个工作表` : '';
- createMessage.warning(`已匹配模板的工作表必须先填写表头所在行:${previewNames}${suffix}`);
- return;
- }
- try {
- const childTemplateNameValid = await ensureChildTemplateNamesAvailable(sanitizedFileInfos);
- if (!childTemplateNameValid) {
- return;
- }
- } catch (error) {
- createMessage.error(getRequestErrorMessage(error, '模板名称校验失败'));
- return;
- }
- sessionStorage.setItem(FILE_INFO_STORAGE_KEY, JSON.stringify(sanitizedFileInfos));
- importingData.value = true;
- try {
- const resolveMappedHeaderByTargetField = (item?: TreeFileInfo | null, targetField?: string) => {
- const normalizedTarget = normalizeFieldMatchText(targetField);
- if (!normalizedTarget) return '';
- const matchedRule = normalizeTableRuleList(item?.tableRuleList).find((rule, index) => {
- const targetKey = normalizeFieldMatchText(getTableRuleTargetKey(rule, index));
- const targetLabel = normalizeFieldMatchText(getTableRuleTargetLabel(rule, index));
- return normalizedTarget === targetKey || normalizedTarget === targetLabel;
- });
- return normalizeRecognitionConfigText(matchedRule?.fileColName);
- };
- const buildImportRecognitionBasicInfoFuncRegx = (item?: TreeFileInfo | null) => {
- const source = normalizeRecognitionConfigText(item?.funcRegx);
- if (!source) return '';
- const rows = parseRecognitionBasicInfoRows(source);
- if (!rows.length) return source;
- if (!hasRecognitionBasicInfoFilledContent(rows)) return '';
- const payload = rows.reduce(
- (acc, row) => {
- const targetField = resolveRecognitionBasicInfoTargetField(row.targetField);
- const headerMatch = normalizeRecognitionBasicInfoText(row.headerMatch);
- const endFlag = normalizeRecognitionBasicInfoText(row.endFlag);
- const mappedHeader = resolveMappedHeaderByTargetField(item, targetField);
- const nextKey = headerMatch || mappedHeader || targetField;
- if (!nextKey) return acc;
- acc[nextKey] = [headerMatch, endFlag];
- return acc;
- },
- {} as Record<string, string[]>,
- );
- return Object.keys(payload).length ? JSON.stringify(payload) : '';
- };
- const buildImportFileInfoPayload = (item?: TreeFileInfo | null): TreeFileInfo | null => {
- if (!item) return null;
- const nextItem = cloneDeepSafe(item) as TreeFileInfo;
- const children = Array.isArray(nextItem.children)
- ? nextItem.children
- .map((child) => buildImportFileInfoPayload(child))
- .filter((child): child is TreeFileInfo => Boolean(child))
- : nextItem.children;
- if (isFileRootItem(nextItem)) {
- const rootPayload = {
- ...nextItem,
- children,
- } as TreeFileInfo & { childTemplateName?: string | null };
- delete rootPayload.childTemplateName;
- return rootPayload;
- }
- const childName = normalizeRuleText(nextItem.childTemplateName);
- const hasNew = !!childName || !!nextItem.hasNew;
- const importTemplateId = normalizeTemplateId(
- nextItem.templateId ?? templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
- );
- const currentTemplate = resolveTemplateInfoById(importTemplateId);
- const parentTemplateId = normalizeTemplateId(nextItem.mainId) || normalizeTemplateId(currentTemplate?.mainId);
- const parentTemplate = (parentTemplateId && resolveTemplateInfoById(parentTemplateId)) || currentTemplate;
- const currentFuncRegx = normalizeRecognitionConfigText(nextItem.funcRegx);
- const importFuncRegx = currentFuncRegx ? buildImportRecognitionBasicInfoFuncRegx(nextItem) : '';
- const sheetPayload = {
- ...nextItem,
- tableName: hasNew ? childName : nextItem.tableName,
- tableNameEn: hasNew
- ? normalizeRuleText(parentTemplate?.tableNameEn) ||
- normalizeRuleText(currentTemplate?.tableNameEn) ||
- normalizeRuleText(nextItem.tableNameEn) ||
- null
- : nextItem.tableNameEn,
- hasNew,
- classify: hasNew
- ? normalizeRuleText(parentTemplate?.classify) ||
- normalizeRuleText(currentTemplate?.classify) ||
- normalizeRuleText(nextItem.classify) ||
- null
- : nextItem.classify,
- funcRegx: importFuncRegx || null,
- children,
- } as TreeFileInfo & { childTemplateName?: string | null };
- return sheetPayload;
- };
- const importFileInfos = sanitizedFileInfos
- .map((item) => buildImportFileInfoPayload(item))
- .filter((item): item is TreeFileInfo => Boolean(item));
- sessionStorage.setItem(IMPORT_FILE_INFO_STORAGE_KEY, JSON.stringify(importFileInfos));
- resetImportProgressSession();
- await go(PageEnum.BASE_CLEAN_PROGRESS);
- } catch {
- createMessage.error('导入数据失败');
- } finally {
- importingData.value = false;
- }
- };
- const getFocusedNodeKeyFromQuery = () => {
- const rawFocusKey = pageQuery.value?.focusKey;
- return Array.isArray(rawFocusKey) ? normalizeRuleText(rawFocusKey[0]) : normalizeRuleText(rawFocusKey);
- };
- const handleBack = () => {
- // 返回上一级:文件上传页
- go(PageEnum.BASE_UPLOAD);
- };
- const loadFileTree = async () => {
- const storedList = loadStoredFileList();
- fileList.value = storedList.length ? storedList : sanitizeFileInfoTree(cloneDeepSafe(mockFileList));
- if (!treeData.value.length) return;
- const focusedKey = resolveSelectableTreeKey(getFocusedNodeKeyFromQuery());
- const initialKey =
- focusedKey ||
- (selectedKeys.value.length
- ? resolveSelectableTreeKey(String(selectedKeys.value[0]))
- : findFirstSelectableKey(treeData.value));
- if (!initialKey) return;
- selectedKeys.value = [initialKey];
- await switchNodeState(initialKey);
- if (focusedKey) {
- scrollSelectedTreeNodeIntoView();
- }
- };
- const initializePage = async () => {
- void Promise.all([loadTemplateCategories(), loadTemplateList({ silent: true })]);
- runRulePipeline();
- await loadFileTree();
- };
- onMounted(() => {
- initializePage();
- window.addEventListener('mousemove', onResizeMouseMove);
- window.addEventListener('mouseup', stopResize);
- });
- onBeforeUnmount(() => {
- saveCurrentNodeState();
- window.removeEventListener('mousemove', onResizeMouseMove);
- window.removeEventListener('mouseup', stopResize);
- document.body.classList.remove('is-resizing-layout');
- document.body.style.cursor = '';
- });
- // Phase 2:把 Modal 子组件需要的页面级共享状态/方法 provide 下去(子组件内部按名取用)
- 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 });
- </script>
- <style lang="less">
- // 样式已按域拆分到 ./cleaning/styles/ 下,由此处引入(保持全局,非 scoped)。
- @import './cleaning/styles/index.less';
- </style>
|