cleaning-refactor-plan.md(仓库根),已获用户确认。用户拍板的决策:
cleaning.vue 保留为入口文件,子模块放同级 cleaning/ 目录<style scoped>Phase 0 已完成(零逻辑改动):把 cleaning.vue 的 3819 行 <style lang="less"> 拆到 ai-frontend/src/case/views/data/cleaning/styles/
variables.less(@prefix-cls)page.less 2734 行(巨型块 8214-10922 + body.is-resizing-layout + keyframes)modals.less 1097 行(Modal/Popover 全局样式 10924-11360 / 11368-12018)index.less(汇总入口)cleaning.vue 的 style 块改为 @import './cleaning/styles/index.less';cleaning.vue: 12029 → 8214 行pnpm build 退出码 0,产物 dist/assets/cleaning-*.css 86.0 kB,抽查 17 个代表性选择器全部存在build/theme/modifyVars.ts 里的 hack: 'true; @import (reference) "src/core/design/var/index.less";',通过 vite less modifyVars 注入。
所以 @modal-header-bg-color 这类变量在任意 .vue style 块和任意 .less 文件里都能直接用。
→ 把 SFC 的 style 抽成独立 .less 再 @import 回来是安全的(同一编译单元)。Modal / Popover / Select 会 teleport 到 body,scoped 样式打不进去。
页面用 wrapClassName="cleaning-config-modal add-column-modal" 配合全局样式命中。ai-frontend/src/core/router/routes/index.ts,cleaning 页是独立全屏路由(hideMenu/hideTab)。pnpm build(在 ai-frontend/ 下),约 2 分 20 秒。ls/grep/wc 等命令不可用(shim 报 dirname: command not found),
PowerShell 工具的 stdout 也不回显 → 需要看输出时一律把结果写进临时文件再用 Read 读。cleaning.vue: 8214 → 6122 行;新增 ai-frontend/src/case/views/data/cleaning/ 下 11 个文件:
types.ts 246 行(25 个类型定义)constants.ts 175 行(17 个常量/选项)utils/text.ts 167、utils/treeUtils.ts 136、utils/tableRuleBuild.ts 207、utils/formatValidate.ts 313utils/ruleSerialize.ts 455、utils/headerRow.ts 78、utils/recognition.ts 195utils/columnSplit.ts 88、utils/domScroll.ts 31、utils/mockData.ts 134export / 补 import(跨文件内部依赖 + cleaning.vue 原有外部依赖),
逐行原样搬运,只去掉统一 2 空格缩进,业务逻辑零改动。pnpm type:check 中 cleaning 相关报错 0 条(仓库现有 89 条报错全部来自 call/、trans/、graph/ 等无关文件的既有问题);
pnpm build 退出码 0,✓ built in 1m 58s,产出 dist/assets/cleaning-*.js。E:\workspace\zsjz-ai\_bak\cleaning.vue.phase0(8214 行基线,后续 Phase 出错可直接覆盖回来)。export,否则报 TS2459 declares X locally, but it is not exported / TS2306 File is not a module
(无 import 也无 export 的 .ts 会被 TS 当全局脚本,不是模块)。../../../dm/types/Func 的 Fun* 类、FileStatusEnum、TableInfo 等)时必须按新目录层级重算相对路径
(cleaning/ 深 1 层、cleaning/utils/ 深 2 层)。漏了会报 TS2304 Cannot find name 'FunReplace' 之类。`${fn(x)}` 这种模板字符串插值会被整段当成字符串抹掉,
导致漏 import。改用原文扫描,注释/字符串里的同名标识符最多只多出一个无害的 import。cleaning.vue: 6123 → 5369 行。新增 7 个文件:
cleaning/context.ts(20 行):CleaningContextKey + useCleaningContext()cleaning/components/AddColumnModal.vue 118 行、SplitColumnModal.vue 71 行、TemplateSelectModal.vue 187 行cleaning/composables/useAddColumnModal.ts 203 行、useSplitColumnModal.ts 116 行、useTemplateModal.ts 167 行provide(CleaningContextKey, {...}),子组件/composable 用 useCleaningContext() 按名取用。
const 声明顺序造成的 TDZpnpm type:check cleaning 相关报错 0 条;pnpm build exit 0,✓ built in 1m 50s,cleaning-*.js 118.68 kB_bak/cleaning.vue.phase1(6123 行)/ _bak/cleaning.vue.phase0(8214 行)let/var 标量不能注入:注入是按值快照,搬走的代码和页面会各改一份副本(templatePreviewRequestToken 这种请求序号守卫会直接失效)。
规则:凡直接引用页面 let/var 的声明一律不搬。cleaning.vue: 5370 → 3921 行(本轮 −1449)。累计 12029 → 3921(−67%)。rule-col(规则库) / rule-op(操作按钮) / rule-col(已选规则) / rule-config(参数配置)。composables/useCleaningRules.ts(948 行);只留 7 个依赖(createMessage / rightTableHeaders / addedColumns / sourceTableRows / tablePreviewRows / currentColumn / headerRowIndex);页面同名解构 59 个名字。composables/useCleaningRules.ts、components/CleaningRulePanel.vue(56) / CleaningRuleLibrary.vue(27) / SelectedRuleList.vue(32) / RuleConfigForm.vue(527,13 类规则表单暂内联,远低于 900 上限);context.ts 扩成带类型的 CleaningContextpnpm type:check cleaning 相关 0 报错;pnpm build exit 0,✓ built in 2m 12s,cleaning-*.js 102.80 kB_bak/cleaning.vue.phase2(5370 行)composable 由「页面」实例化,页面同名解构接住返回值。
const cleaningRules = useCleaningRules({ /* 7 个依赖 */ });
const { selectRules, selectedRules, runRulePipeline, ... } = cleaningRules; // 页面代码/模板零改动
provide(CleaningContextKey, { ...原有一堆, cleaningRules }); // 子组件从 context 取
这样「页面自己也引用」不再是不能搬的理由(Phase 2 的不动点收缩就是被这条卡住的): Phase 2 只搬得动 36 个声明 / 433 行,Phase 3 直接搬到 113 个 / 872 行。
${...} 插值。strip() 会把整个模板字符串抹掉,`is-${currentRuleScopeType}` 里的标识符就丢了
→ 组件少一个绑定(TS2551)。改成只剥普通字符串('…' / "…"),遇到反引号时把 ${...} 内容取出来。Record<string, any>。页面里「留在页面的函数,其签名/返回值依赖解构出来的 composable 返回值」时,
TS 无法推 Deps,直接退回约束类型 → 注入项全是 any → 而 Object.entries(<any>) 在这个 TS 版本返回 [string, unknown][]
→ 下游一片 targetMeta is of type 'unknown'。解法:deps 必须显式声明类型(interface CleaningRulesDeps),
不要让 TS 从实参反推;ref 的类型可以从页面声明里自动抠出来(ref<...> 按尖括号配对;ref([...X]) 用 Ref<(typeof X)[number][]>)。provide(...) 算进去。它本身就是顶层语句且列了一堆名字,
会被判成「wiring 之前被立即引用」,把一大批声明错误地退回页面 → wiring 位置被推到很晚 → 反而引发一串 TS2448。
正确做法:只扫描 wiring 位置之前的行,并且「可搬集合 ↔ wiring 位置」互相影响,必须迭代到收敛。
另外 watch([...一堆 ref]) 是顶层语句、数组在 setup 期立即求值 → 整条 watch 语句都要算「立即」。const x = ref<\n Record<\n string,\n {...}\n >\n>({}) 会让深度在中途归零,
按 endsWith('}') 提前收尾 → 把 splitColumnConfig 切掉一半(语法错误)。改成「找下一个顶层起点」(2 空格缩进的声明或第 0 列的代码)。cleaning.vue: 3921 → 2459 行(本轮 −1462)。累计 12029 → 2459(−80%)。mapping-table-card(98-228) → FieldMappingCard.vue 97 行 + OriginFieldPopover.vue 70 行(Popover #content 插槽 149-200)recognition-panel(237-394) → RecognitionRulePanel.vue 175 行child-template-panel(396-408) → ChildTemplatePanel.vue 29 行middle-empty-shell(413-452) → MiddleEmptyState.vue 56 行(v-else 留在父级:<MiddleEmptyState v-else />)template-foot-panel 外层 wrapper(230-236/409)useFieldMapping.ts(759,53 个声明 680 行)、useRecognitionRules.ts(262,23/163)、useTemplateLibrary.ts(375,33/301)
pnpm type:check cleaning 0 报错;pnpm build exit 0,✓ built in 1m 53s_bak/cleaning.vue.phase3(3921 行)script + 中栏之外的模板,把「中栏里保留的部分」(匹配条、template-foot-panel wrapper)
漏掉了 → 页面用到的 hasTemplateSelected / openTemplateModal / showRecognitionPanel 没被解构出来。正确做法:整份模板 minus 被抽走的块。any → 又是 Object.entries(any) → [string, unknown][] → 一片 unknown。
解法:若某依赖是页面里 const { x } = cleaningRules; 解构出来的,就把它写成 ReturnType<typeof useCleaningRules>['x'](并 import 那个 composable 的类型)。
另外 deps 类型文本里出现的类型名(如 Ref<TemplateTab> 里的 TemplateTab)也要补 import。let/var 标量不能注入(Phase 2 已经踩过,这次又忘了):搬走的代码要 token += 1,注入后是 const 直接报错。
规则:凡直接引用页面 let/var 的声明一律不搬,把标量留在页面。pnpm type:check 抓不到 Vue 模板标签不平衡! 这次 child-template-panel 的结束行我多算了一行(408 结束、409 是外层 wrapper 的闭合),
结果把外层 <div> 的闭合标签删掉了 —— type:check 报 0 个 cleaning 错误,但 pnpm build 直接失败(El
ement is missing end tag)。
→ 每阶段必须 type:check + build 双验证,而且抽块时要用「配对的 </div> 行」当边界,不能凭缩进猜。
结论:没有完成。当前仓库停在 Phase 4 的状态(cleaning.vue 2458 行),已确认 type:check 0 报错 + build exit 0。
Phase 5/6 的生成器(_phase56.cjs,已删除)反复调试后仍然不成立,原因不是脚本 bug 而是架构约束,记录如下:
useFileTree 想要的底层状态(tablePreviewRows / rightTableHeaders / addedColumns …)
也是已有 composable(cleaningRules / recognitionApi / mappingApi / templateApi)的依赖;反过来已有域拥有的名字又被新域需要。
于是「wiring 顺序」必须做拓扑排序,而图里真有环。useFileTree / usePreviewData / useNodeStateCache / useCaseDataCleaningPage
全部退化成 19 行的空壳(垃圾文件),页面只从 2459 掉到 2260ownerOfName 漏了查已有 handle 的解构名)导致图里没有环,
属于「侥幸正确」,不可信 —— 这种结果不能交付。watch( / provide( / 生命周期调用,否则上一个声明的范围会把它们整段吞掉;
② wiring 要插在锚点行之后(锚点是依赖声明的最后一行,插前面会插进声明内部)。不要再让「多个平级 composable 互相注入」硬解 —— 应该先抽出唯一一个 useCaseDataCleaningShared(owns 所有底层共享状态),
让 tree / preview / columnOps / rules / recognition / mapping / template 全部以它为 dep 单向依赖,环自然消失。
这是「设计一步」而不是「自动搬运一步」,值得单独一轮 review。
aside 左栏树 58 行、table-mock 预览表 89 行)的抽取边界已核实清楚(L19-76 / L149-237)${...})、依赖类型显式化、provide 收敛、
以及「页面实例化 + 同名解构」的接线模式 —— 这些在 Phase 3/4 已验证,Phase 5/6 可以直接照搬用户拍板「按这个思路单独做一轮」。成功,双验证通过:
cleaning.vue: 2459 → 2170 行(累计 12029 → 2170,−82%)composables/useCaseDataCleaningShared.ts(356 行):底层共享状态唯一 store(54 个声明 / 297 行)pnpm type:check cleaning 0 报错(89 条既有);pnpm build exit 0,✓ built in 1m 33s_bak/cleaning.vue.phase4useCaseDataCleaningShared 认领「页面里除壳之外的声明」,但必须剔掉两类:
rightTableHeaders 这类不受影响,
但 templateModalOpen / activeCategory 因为出现在顶层 watch 列表里被退回)const x = ref(0) 会因为引用自己而误判sharedApi → 已有 4 个 handle;
下一步加叶子域时必须放在 handle 之后(叶子域同时依赖 shared 和 handle 是允许的,全是单向)→ 无环把「引用了 handle 产物的 64 个声明」按域拆出去,且这些域要放在 4 个 handle 之后:
useFileTree / usePreviewData / useColumnOps / useNodeStateCache + 抽 FileTreePanel.vue(L19-76) / DataPreviewTable.vue(L149-237)。
因为叶子域依赖 shared 和 handle 都是单向,不会再出现环 —— 这一轮才是把页面压到 ≤400 行的关键。
成功,双验证通过:cleaning.vue 2170 → 944 行(累计 12029 → 944,−92%);type:check cleaning 0 报错;build exit 0(1m 39s)。
composables/useCaseDataCleaningActions.ts(1310 行):45 个声明 / 1088 行的叶子逻辑
(树匹配度、预览数据集载入、列规则恢复、列操作、扫描触发、文件导入、模板选择入口…)components/FileTreePanel.vue(75 行,原 aside L19-76)、components/DataPreviewTable.vue(102 行,原 table-mock L149-237)useCaseDataCleaningShared → 4 个 handle → useCaseDataCleaningActions(单向,无环)watch([...refs]) 列表里)_bak/cleaning.vue.phase5a(2170)../../../dm/types/Func 这类要单独处理用 git show HEAD:.../cleaning.vue 取原始版,与「cleaning.vue + cleaning/** 整棵树」做逐行多集合比对
(归一化:去 export 前缀 / 折叠空白 / 去行尾 ,;;跳过注释与 import)。结果:
addedColumns: Ref<string[]>、ReturnType<typeof useCleaningRules>['x'])const { / } = deps / 6 条 wiring 行 / 3 个 Phase-2 composable 的 function useXxx() { / CleaningContextKey}(+23)<div v-else class="middle-empty-shell"> → 设计为 v-else 上提到父级
(<MiddleEmptyState v-else />)+ 组件根保留 <div class="middle-empty-shell">,两者都在,语义等价<style> / </style> 两个包裹标签,样式内容全量进了 styles/*.less用户要求:清未使用 import + 修 ESLint: Insert ·· (prettier/prettier)。最优路径就是用项目自己的 eslint --fix,
因为 prettier/prettier 是 eslint 规则,一把就能把格式和未使用 import 都修掉:
cd ai-frontend
pnpm exec eslint --fix "src/case/views/data/cleaning.vue" "src/case/views/data/cleaning/**/*.{ts,vue}"
pnpm exec prettier --write "src/case/views/data/cleaning/**/*.less"
eslint(不带 --fix)复检:0 problems;prettier --check(less):通过cleaning.vue 里残留的 24 个绑定
(nextTick / DownOutlined / UpOutlined / Checkbox / InputNumber / Popover / Radio / Select / Switch / Tooltip /
EmptyState / BasicTree / FileInfo / tplCategoryList / tplCheckTableName / tplPage / rawPreview / FileStatusEnum /
getVisibleOriginOptionList / scrollSelectedTreeNodeIntoView / loadStoredFileList / mockFileList / mockTemplateList / OriginFieldPopover)
—— composables 与 components 都是按需生成的,一个多余的都没有type:check cleaning 0 报错(89 条既有);pnpm build exit 0cleaning.vue 行数 944 → 1431:prettier 按 printWidth 把 12 行「接线管线」折成了每行一个名字
(6 条 const api = useXxx({ ...80 个依赖... }) + 6 条 const { ...80 个名字... } = api;)≈ +490 行。
不是代码变多,是格式化的必然结果。想让行数回落要把这种大解构改掉(比如让 composable 返回分组对象),属于设计变更。styles/*.less 需要 dedent:从 .vue 的 <style> 块抽成独立 .less 时保留了 2 空格缩进(.vue 里合法),
独立 .less 要求从 0 开始 → page.less / modals.less 被 prettier 重新缩进了一遍(纯格式)。格式化前把整棵树快照到临时目录,然后逐文件比较去掉所有空白后的字符流:
结果 41 个文件里 16 个完全一致、24 个仅差 prettier 补/去的尾逗号、1 个(context.ts)是 eslint 自动移除了一条已失效的 eslint-disable 注释 → 零逻辑改动。
(这个「空白无关字符流比对」比行级比对更适合校验格式化,因为 prettier 会重排换行。)
做这类验证的坑(下次注意):
> 重定向写 UTF-16 → 用 node execSync(..., {encoding:'buffer'}) 取字节流(cmd /c 被安全策略拦)<script 而不是 <style(原文件是 template→script→style 顺序)export 开头的行,要在归一化时去掉 export 前缀再比data/ 下其它页面(importList/cleanProgress/index)算进来,否则「新增」会虚高 3500+git stash / 备份原始文件)
页面还剩 ~944 行 = 模板(~110) + import(~100) + 壳(22 个声明 ~180) + 21 个因 watch 列表退回的声明 + wiring/provide。
要再压:watch([...]) 语句移进 composable(watcher 在 composable 内注册仍绑定到页面实例),那 21 个声明就能搬走... 展开,我上次因为排除了 . 误删了 2 个 import)useCaseDataCleaningActions(1310 行)按域拆成 useFileTree / usePreviewData / useColumnOps(都依赖 shared + handle,彼此不互相依赖)