@epxing/cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,381 @@
1
+ # 踩坑清单(每条都在 product/hr 上实证过)
2
+
3
+ 按"什么时候咬人"分组。每条给**判据**(怎么确认是它)和**修法**。
4
+
5
+ ---
6
+
7
+ ## A. 迁移工具阶段(`epx migrate`)
8
+
9
+ ### A1. gogocode 会改坏二进制文件
10
+
11
+ **现象**:字体/图片体积莫名变化,构建仍然绿。实测 `FontAwesome.otf` 134808→258854 B、
12
+ `fontawesome-webfont.eot` 165742→212524 B。
13
+
14
+ **根因**:整目录 `cpSync` 把 gogocode 处理过的文件原样拷回,二进制被按文本处理过。
15
+
16
+ **判据**:`git status` 看 `src/assets/` 有没有莫名其妙的改动;`git diff --stat` 看体积。
17
+
18
+ **修法**:拷回只走扩展名白名单(`.vue/.js/.jsx/.ts/.tsx`)。已修在
19
+ `tools/epx-migrate/copy-back-sources.mjs`。踩到就 `git checkout -- src/assets/`。
20
+
21
+ ### A2. `install(Vue, options)` 的正则漏匹配
22
+
23
+ **现象**:运行时 install 报 TypeError,构建期完全正常。
24
+
25
+ **根因**:老正则只认 `install = function (Vue)`,产品写成 `(Vue, options)` 就不匹配 →
26
+ gogocode 按它自己的约定把函数体改成 `window.$vueApp.*`,而**平台从不设置这个全局**。
27
+
28
+ **判据**:`grep -n 'window.\$vueApp' src/` 有命中就是它。
29
+
30
+ **修法**:`tools/epx-migrate/codemod-lib-entry.mjs` 覆盖 fn/method 两种形态与额外形参,
31
+ 且必须排在 gogocode(stage ③)**之前**。
32
+
33
+ ### A3. 具名导入收敛丢掉正文还在用的名字
34
+
35
+ **现象**:运行时 `ReferenceError: store is not defined`,webpack 构建是绿的。
36
+
37
+ **修法**:import 收敛必须放在**所有正文改写之后**,且保留仍被引用的名字。
38
+
39
+ ### A4. eslint 全家被删,工具链先坏
40
+
41
+ **现象**:`npx eslint --fix <file>` 报 `couldn't find the config "@vue/standard"`。
42
+
43
+ **根因**:依赖迁移删 vue-cli 生态时连带删了它。
44
+
45
+ **修法**(eslint 8 的正确组合):`@vue/eslint-config-standard@8` +
46
+ `eslint-config-standard@17`(16 要 eslint@7)+ `@babel/eslint-parser`;
47
+ `.eslintrc.js` 切 `plugin:vue/vue3-essential`、`parser: '@babel/eslint-parser'`。
48
+ 注意 standard 17 起 `eslint-plugin-node` 改名 `eslint-plugin-n`。
49
+
50
+ ---
51
+
52
+ ## B. Vite 化阶段
53
+
54
+ ### B1. `recorder-core` 引擎与核心之间**没有模块图边**
55
+
56
+ **现象**:dev 壳白屏,`TypeError: Cannot read properties of undefined (reading 'i18n')`
57
+ 指向 `.vite/deps/chunk-XXXX.js`。
58
+
59
+ **根因**:引擎(`src/engine/wav.js`)对核心是**零 import**,执行时从 `window.Recorder` 取,
60
+ 第一行就是 `var rec = win.Recorder, ni = rec.i18n`。模块图里没有 wav→core 这条边,
61
+ **没有任何打包器有义务先执行核心**。源码里两条相邻 import 只是"声明序",
62
+ esbuild 依赖预打包一拆 chunk 就没了。
63
+
64
+ **注意**:炸的是 **epx-frame 自己**(`lib/epx-frame.esm.mjs` 第 1、2 行),产品侧收口模块管不着。
65
+
66
+ **修法**:补边 shim + **dev 专用** alias(正则 `$` 锚定,shim 内部引带 `.js` 后缀的说明符
67
+ 避免 alias 回自己)。
68
+
69
+ **别用 `optimizeDeps.exclude`**:recorder-core 只有 CJS 入口,排除预打包就没有 interop,
70
+ 换来 `SyntaxError: ... does not provide an export named 'default'`。
71
+
72
+ ### B2. `import * as ns` 挡住 shim 生成
73
+
74
+ **现象**:按需构建报死,说某个 external 无法生成 shim。
75
+
76
+ **根因**:shim 无法静态枚举命名空间导入会访问哪些名字。
77
+
78
+ **修法**:改具名导入。这是**构建期报死**的设计(不静默),改掉即可。
79
+
80
+ ### B3. 未映射的 external 全局名
81
+
82
+ **现象**:构建期报 `external 'xxx' 没有对应的运行时全局映射`。
83
+
84
+ **修法**:产品自己知道全局名的(如 `recorder-core` → `Recorder`),
85
+ 在 `vite.config.mjs` 的 `output.globals` 里配;平台提供的去 `build/lazy-lib/gen-shims.mjs` 补。
86
+
87
+ ### B4. `url(~@)` 在 Vite 下静默 404
88
+
89
+ webpack 遗留写法,Vite 不认 `~`。改成 `@/` 或相对路径。
90
+
91
+ ---
92
+
93
+ ## C. 按需打包特有(**dev 壳一个都验不出来**)
94
+
95
+ ### C1. `extractName` 抽不到 `name` → 注册名退成路径兜底
96
+
97
+ **现象**:页面按 `epx-hr-search` 找组件找不到,控制台一声不吭。
98
+
99
+ **根因**(已修):老实现用 `export default` 之后 **400 字符的窗口**找 `name`。
100
+ `Search.vue` 里 `export default {` 在 264 行、`name` 在 303 行,隔了 39 行远超窗口 →
101
+ 退到路径兜底名 `hrcomponentsSearch`。而 dev 壳读**运行时** `comp.name`,两边不一致。
102
+
103
+ **判据**:`index.manifest.json` 里有不含 `-` 的键,就是路径兜底名。
104
+
105
+ ```bash
106
+ node -e "const m=require('./lib/index.manifest.json');console.log(Object.keys(m.components).filter(k=>!k.includes('-')))"
107
+ ```
108
+
109
+ **修法**:babelParse 真解析、只认顶层属性。**别把窗口调大**——`name` 写在 methods
110
+ 后面又会漏。
111
+
112
+ ### C2. 平台占位抢注
113
+
114
+ **现象**:`[loadComponent] system.component 里没有 epx-xxx`。
115
+
116
+ **根因**:平台 `loadComponent` 与产品 `install` 都是「没人占我才占」
117
+ (都是 `if (!app.component(name))`),谁先跑谁赢。页面配置引用组件时
118
+ `Page.vue` 先按名字调平台工厂抢注了占位,产品 install 随后跳过。
119
+ 而占位的解析链里**只有平台组件 + dashboard + designer,没有产品库**。
120
+
121
+ **判据**:控制台查槽位里是谁——产品的懒加载 wrapper 有 `.name`,平台占位没有。
122
+
123
+ ```js
124
+ const c = document.querySelector('#app').__vue_app__._context.components['epx-hr-search']
125
+ c?.name ? 'product wrapper' : 'platform stub'
126
+ ```
127
+
128
+ **修法**(已修):解析链在平台组件之后、可选能力组之前查 `window.componentLibrary`。
129
+ 排在可选能力组之前很关键,否则一个产品组件会白拉 dashboard + 体积很大的 designer。
130
+
131
+ ### C3. 记账写在 install 之前 → 产品幂等守卫空转
132
+
133
+ **现象**:`window.__epxInstalledLibs__` 里有 `hr@3.0.1`,但 app 里 **0 个**产品组件。完全静默。
134
+
135
+ **根因**:平台 `util.loadJs` 先记账再调 install,而产品 install 开头是
136
+ `if (marks[sig]) return`(PROJECT-GUIDE 约定的幂等写法)——**一进门就返回**。
137
+ 两侧去重分两次提交时各自都对,撞一起才炸。
138
+
139
+ **判据**:
140
+
141
+ ```js
142
+ JSON.stringify({
143
+ libs: Object.keys(window.__epxInstalledLibs__ || {}),
144
+ count: Object.keys(document.querySelector('#app').__vue_app__._context.components)
145
+ .filter(n => /^epx-/.test(n)).length
146
+ })
147
+ ```
148
+ 表里有记录、count 却是 0,就是它。
149
+
150
+ **修法**(已修):记账挪到 `useVuePlugin` **之后**。
151
+
152
+ ### C4. 嵌套构建没继承 `assetsInlineLimit`
153
+
154
+ **现象**:图片变成 `assets/*.png` 文件,产物里引用为**绝对路径** `/assets/xxx.png`。
155
+ 远程库从文件服务子路径加载,这个 URL 解析到平台页面的 origin 根目录 → **运行时 404,构建期无声**。
156
+
157
+ **判据**:整体单体形态**不产生** `assets/` 目录(全内联),按需形态却有 → 就是没继承。
158
+
159
+ **修法**(已修):`inheritedBuildOptions()` 把 `assetsInlineLimit`/`assetsDir`/`target`/
160
+ `cssTarget`/`sourcemap`/`minify` 一并转发。`target` 同样要紧:壳由外层构建、chunk 由嵌套
161
+ 构建,两边不一致会产出语法级别不同的代码。
162
+
163
+ ### C5. 没有读者的 `.deps.json`
164
+
165
+ 第①趟给每个 chunk、第③趟给 `index.full.js` 都会产出一份,**都没人读**
166
+ (chunk 走 `import()` 不过 ensure;`index.full.js` 只经降级裸 script 加载)。
167
+ 实测 hr:49 个文件 49 KB 死重量。
168
+
169
+ **修法**(已修):`rmStrayDepsJson` 事后删,**不能**把 `epx-frame-deps` 插件从嵌套构建滤掉——
170
+ 组件源码只在第①趟被 transform,插件靠同一实例跨趟累积,滤掉会让壳的清单变成空数组。
171
+
172
+ ---
173
+
174
+ ## D. 本地联调环境
175
+
176
+ ### D1. junction 链接平台必须配 `resolve.dedupe`
177
+
178
+ Vite 默认 `preserveSymlinks:false`,按真实路径解析 → vue 会有两份
179
+ (实测 frame 3.5.39 / 产品 3.5.42),provide/inject 断链、组件注册表分裂。
180
+
181
+ ```js
182
+ resolve: { dedupe: ['vue', 'vuex', 'vue-router'] }
183
+ ```
184
+
185
+ ### D2. Git Bash 不跟随 junction
186
+
187
+ `ls/stat/grep` 会报文件不存在。用 PowerShell 或 node 验。Vite/Node 走 Windows 原生 API 正常。
188
+
189
+ ### D3. 本地顶替远程库时,CSS 会走线上旧构建
190
+
191
+ 后端 `listWebpackUrl` 返回的**一相对一绝对**:
192
+
193
+ ```json
194
+ {"code":"0","data":{"entitys":[{"path":[
195
+ "/file-server/npm/test/web_hr/index.js", // 相对 → 拼 window.IP → 本地
196
+ "https://dev.epxing.com:88/file-server/npm/test/web_hr/index.css" // 绝对 → 原样用 → 线上
197
+ ]}]}}
198
+ ```
199
+
200
+ `loadLibrary` 只给相对路径拼 `window.IP`。于是组件是本地新 chunk、样式是线上旧 CSS,
201
+ scoped 的 `data-v-<hash>` 两边对不上 → **组件渲染出来但完全没样式**。
202
+
203
+ **判据**:`[...document.styleSheets].map(s=>s.href).filter(h=>h&&h.includes('<pkg>'))`
204
+ 看 host 是不是本地。
205
+
206
+ **修法**:本地反代里拦 `listWebpackUrl` 响应,把绝对地址改写成相对。
207
+ 注意响应外面**包了一层 `data`**(`{code,msg,data:{entitys},...}`),
208
+ 直接取 `json.entitys` 会拿到 undefined 并静默改写 0 条。
209
+
210
+ ### D4. `epx-frame` 未发布时 npm 装不了任何东西
211
+
212
+ 产品 package.json 声明 `epx-frame@^5.3.0`,未发布前 npm **解析根清单就失败**(ETARGET),
213
+ 连加一个 devDep 都装不了。绕法:别处建空目录装好,再逐包拷进产品的 `node_modules`
214
+ (所有 `@scope` 要下沉到子包粒度判存在,按整个 scope 目录判会漏);
215
+ 版本冲突的包放**嵌套** `node_modules`。
216
+
217
+ ---
218
+
219
+ ## E. 主题/样式
220
+
221
+ ### E1. 换肤 mixin 按主题展开,既臃肿又漏主题
222
+
223
+ **现象**:`index.css` 里同一条规则有 N 份 `[data-theme=x]` 拷贝;
224
+ 用户选了某些主题色,产品组件不跟着变。
225
+
226
+ **根因**:产品自带的 `mixins.scss` 用 `@each` 遍历**写死的主题列表**。
227
+ 实测 hr 写死 8 套,而平台有 **11 套预设 + custom**(运行时注入)——**4 套完全不生效**。
228
+
229
+ **判据**:
230
+
231
+ ```bash
232
+ node -e "const c=require('fs').readFileSync('lib/index.css','utf8');console.log(c.split('}').filter(r=>/\[data-theme=/.test(r.split('{')[0])).length)"
233
+ ```
234
+
235
+ **修法**:统一走 `rgba(var(--color-primary), $opacity)`。平台在 `[data-theme=x]` 上设
236
+ `--color-primary: r,g,b`,custom 由运行时 JS 注入,所有主题自动生效。
237
+ `$color` 参数改作 `var()` 的兜底值。实测 hr:130.4 → 85.0 KB(−35%),
238
+ `[data-theme=*]` 规则 600 → 0。
239
+
240
+ **注意 gzip 收益很小**(21.3 → 17.4 KB)——重复内容压得好,**省的是解析与选择器匹配成本**。
241
+
242
+ ---
243
+
244
+ ## F. gogocode 转换残留 & 第三方库(product/recruit 145 组件实证)
245
+
246
+ > 这一组是「gogocode 没转干净 + 项目自带 Vue2 依赖」,多数在 `build:lib:lazy` 才暴露。
247
+ > **能机械化的已进 CLI 自动做**(下面标「已 CLI」),**结构性/换库的靠本节手动**。
248
+
249
+ ### F1. gogocode 漏转「元素上的」旧具名插槽 `slot="x"`
250
+
251
+ **现象**:build 报 `[vite:vue] Cannot read properties of undefined (reading 'type')`(**无行号**),
252
+ 或 `slot="x" is deprecated`。gogocode-plugin-vue 转了 `<template slot>`,却漏了**直接写在元素上**的。
253
+
254
+ **判据**:`grep -rn 'slot="' src --include=*.vue | grep -v v-slot`(排掉注释行)。
255
+
256
+ **修法**(结构变化,逐个手改;CLI 不做——配对闭合标签的正则不可靠):
257
+ ```
258
+ <el-dropdown-menu slot="dropdown">…</el-dropdown-menu>
259
+ → <template #dropdown><el-dropdown-menu>…</el-dropdown-menu></template>
260
+
261
+ <span slot="footer" class="dialog-footer">…</span> (el-dialog 的 footer)
262
+ → <template #footer><span class="dialog-footer">…</span></template>
263
+ ```
264
+ recruit 实测 7 处需改(另 3 处在注释里,忽略)。定位无行号的 `reading 'type'`:从报错文件里逐个 `slot="` 查起。
265
+
266
+ ### F2. gogocode 漏转 `slot-scope`
267
+ `slot-scope="scope"` → `v-slot="scope"`(或 `#default="scope"`)。recruit 1 处,手改。
268
+
269
+ ### F3.(已 CLI)`:prop.sync` → `v-model:prop`
270
+ Vue3 移除 `.sync`。gogocode 漏,现由 `fix-inline-handlers.mjs` 的 `fixSyncModifier` 在 migrate ③ 自动转,不用手改。
271
+
272
+ ### F4. epxing 字段名里的 `|` 把 gogocode filters 规则搞崩(良性,忽略)
273
+ **现象**:③ 转换刷 `文件转换异常,规则:filters … expressionStatementAst.attr is not a function`。
274
+ **根因**:epxing 字段名带竖线(`info['SCHEME|NOTICE_FILE#']`),gogocode 过滤器规则把它当 Vue2 过滤器管道解析 AST,崩。
275
+ **良性**:不是真过滤器,文件其它转换正常、内容没坏。dry-run 报的「过滤器 N 文件」也因此**虚高**(把字段名 `|` 误算成过滤器)。
276
+ **判据**:`grep -rnE "\{\{[^}]*\|\s*[a-z_]+\s*\}\}" src` 把 `|大写字段名` 排掉后基本为 0 → 确认无真过滤器,忽略刷屏即可。
277
+
278
+ ### F5.(已 CLI)内联事件多语句缺分号
279
+ Vue2 容忍换行分隔、Vue3 编译器报 `Unexpected token, expected ","`。现由 `fixInlineHandlers` 在 migrate ③ 自动补分号。
280
+
281
+ ### F6. 换掉没有 Vue3 版的第三方库(⑥ 会告警,换库是人工决策)
282
+
283
+ | 库 | Vue3 替代 | 改动 |
284
+ |---|---|---|
285
+ | `vue-quill-editor@3` | `@vueup/vue-quill` | 换 import + 组件用法(v-model/options 略变) |
286
+ | `vuedraggable@2` | `vuedraggable@4`(同名升 major) | 用法基本兼容,重装升版本 |
287
+ | `vue-orgchart-direction` | 无——改 `bpmn-js` 直用 / SVG 自实现 | 组织架构图重写(hr T2 同款) |
288
+ | `element-ui` | 平台运行时已提供 element-plus | **直接删 import**(el-* 全局可用),不装 element-plus |
289
+ | `vant@2` | `vant@4`(移动端) | 用法有差异,逐个核 |
290
+
291
+ **先查有几个文件真 import 再决定工作量**:`grep -rl <lib> src --include=*.vue --include=*.js`。
292
+ recruit 实测:element-ui 1 / vue-quill-editor 1 / vuedraggable 2 / vant 2 —— 量小,逐个换即可。
293
+ (依赖在 package.json 但源码 0 引用的是**遗留依赖**,如 recruit 的 bpmn-js/vue-bpmn,直接从 deps 删。)
294
+
295
+ ### F7.(已 CLI)`node-sass` / `fibers` 残留
296
+ 老 scss 工具链(node-gyp 原生编译),新 node 上 `npm i` 必挂、还骗过 exit code(管道吞 code)。
297
+ 已加进 to-vite 的 `WEBPACK_ONLY_DEPS` 自动删,Vite 用 dart-sass(`sass`)。**判 npm i 真成败别信 exit code**——
298
+ `grep -iE "gyp ERR|Build failed" 安装日志`。
299
+
300
+ ### F8. 垃圾副本文件(`Xxx copy.vue`)在按需形态撞名
301
+ **现象**:build 报 `[lazy-lib] 组件注册名冲突:epx-xxx 同时来自 A copy.vue / A.vue`。
302
+ 单体形态静默覆盖(线上不暴露)、按需形态 `assertUniqueKeys` 拦下(替项目揪出垃圾副本)。
303
+ **判据**:`find src -iname "*copy*"`。**修法**:删副本(或改其 name)。
304
+
305
+ ### F9.(已 CLI)无扩展名 import .vue
306
+ 老项目 webpack `resolve.extensions` 含 `.vue`,`import X from './X'` 遍地;Vite 默认不含。
307
+ create 模板的 vite.config 已带 `resolve.extensions: ['.mjs','.js','.json','.vue']`,`--to-vite` 生成的配置自带,不用手加。
308
+
309
+ ### F10.(已 CLI)`import * as Vue/Vuex/VueRouter` 命名空间导入 → 具名
310
+ gogocode 把 Vue2 默认导入转 Vue3 时,对 vue/vuex/vue-router 常产**命名空间导入**:
311
+ `import * as Vue from 'vue'` + `Vue.h(...)`、`import * as Vuex from 'vuex'` + `Vuex.createStore(...)`、
312
+ `import * as VueRouter from 'vue-router'` + `VueRouter.createRouter/createWebHashHistory(...)`(router/store 是 .js,gogocode `-s ./src` 也转了它们)。
313
+ `lazyLib` 为 external `'vue'` 生成 shim 时**不支持 `import * as ns`**(见 B2,recruit 实测 build 挂:`无法为 external 'vue' 生成 shim`)。
314
+ CLI ③ 修补⑥(`fix-namespace-import.mjs`)收敛为具名:收集 `NS.member` 用到的成员改 `import { m1, m2 } from 'lib'`,
315
+ `Vue.h`→`h` 等;整文件没用到就删这行。只认这三个框架库(白名单),业务库的命名空间导入不碰。
316
+
317
+ ### F11.(已 CLI)`require('@/assets/*.png')` → 静态 import + `assetsInlineLimit`
318
+ webpack 支持 `require('图片')` 当 URL 用(三元/字符串拼接里),Vite 不支持 → 运行时 `require is not defined`。
319
+ CLI V 阶段(`fix-vite-source.mjs`)提为顶部静态 import(别名 `@` 支持)、`require(...)` 换成变量。
320
+ **只在 `<script>`/JS 里改**:模板表达式里的 `require` 换成模块顶层变量后 Options API 的 this 取不到,那种报人工(手工挂 data/computed,见 F11 配套的 recruit Center.vue/Guide.vue 改法)。
321
+ 配套:create 模板的 `build.assetsInlineLimit` 设 `50*1024`,让这些小图标内联成 data URI——否则超 4096 的(recruit 的 `fileHover.png` 4115B,只超一点点)会出独立文件、远程库形态引用 `/assets/xxx` 绝对路径运行时 404(构建期看不出;C4 管 lazy 嵌套构建继承这个值,前提是主 build 设了)。
322
+
323
+ ### F12.(已 CLI)`require.context` 非入口(store 收集 vuex modules)→ `import.meta.glob`
324
+ 入口的 `require.context('@/X', true, ...)` `--to-vite` 一直会转;**漏网的是 store/index.js** 的
325
+ `require.context('./modules', false, ...)`(收集 vuex modules)。CLI V 阶段现遍历全 src 通用改写:
326
+ recursive 决定 `**/` 还是单层、正则决定扩展名(`.vue`/`.js`)、dir 前缀(`@/` 或 `./`)原样保留;
327
+ 配套 `VAR.keys()`→`Object.keys(VAR)`、`VAR(k)`→`VAR[k]`(变量名不一定叫 files,store 里叫 context,故按声明抓变量名)。
328
+
329
+ ### F13. `::v-deep` 弃用警告(**不进 CLI;容器块形态手工 / postcss**)
330
+ Vue3 弃用 `::v-deep` 作 combinator,建议 `:deep(<inner>)`。recruit 实测 **141 处是容器块 `::v-deep{ ... }`**(combinator 仅 10、`>>>` 仅 5)。
331
+ 容器块转 `:deep()` 要拆 SCSS 嵌套块(花括号配对、多层嵌套),141 处正则拆分**极易改坏 style,风险远大于收益**——
332
+ 而它只是**弃用警告:build 通过、运行正常**。故**不进 CLI**(同 gogocode 的 style 快照保护:不用正则动 style 块)。
333
+ 真要消警告:写 postcss 插件按 AST 转、或按文件手工,别正则批量。
334
+
335
+ ### F14.(部分已 CLI)`<script>` 里的 Vue2 JSX 残留 → Vue3(gogocode 只转 template,不碰 JSX)
336
+ gogocode 只转 `<template>`,**不碰 `<script>` 里的 JSX / 手写 render**——Vue2 JSX 前缀原样留到 Vue3,
337
+ `@vue/babel-plugin-jsx` 不识别、当普通属性丢弃 → **build 过、console 不报错、运行时静默失效**(最隐蔽的一类)。
338
+ recruit 实测(145 组件):表单 note 不显示、折叠操作点击无效、子表格自定义列/操作列不渲染。
339
+ ③ 本地简历审核详情「流程图」tab 空白也是同款——平台 `ProcessChart.vue` 用 `domPropsInnerHTML` 贴 saveSVG 的图,
340
+ 实证显示容器 `innerHTML.length=0`(图在隐藏容器画好了、没贴出来);平台侧 Vue2 JSX 残留共 **11 处 / 8 文件**(`domPropsInnerHTML` 9 + `nativeOnClick` 2:`ProcessChart` / `EntityList`×2 / `Mobile`×2 / `Field` / `AddFormItem` / `NoInstallSystemList` / `AppNav` / `Login`),本会话已用**同一个 CLI `fix-jsx-vue2`** 批量修完(平台组件同样适用)、`build:pre` 通过,`ProcessChart` 流程图肉眼验证 `innerHTML 0→30753`。
341
+ - **已 CLI**(③ 修补⑦ `fix-jsx-vue2.mjs`,只换 JSX 属性名、不动值/结构,安全):
342
+ `domPropsInnerHTML=`→`innerHTML=`、`domPropsValue=`→`value=`、`nativeOn<E>=`→`on<E>=`(nativeOnClick→onClick,Vue3 组件事件默认透传根元素)、`scopedSlots={{`→`v-slots={{`。
343
+ - **人工**(结构性重排,CLI 不碰):手写 render 的 data 对象 `return { scopedSlots:{...} }` / `h(tag,{on,props,attrs})`——
344
+ Vue2 createElement data 到 Vue3 差异大,正则不保险。recruit 实测 InnerEntitySubList 3 处(方法返回 `{scopedSlots:{default}}` +
345
+ 调用点 `<el-table-column {...this.renderX()}>` spread),最小改法:调用点 `{...this.renderX()}` → `v-slots={this.renderX().scopedSlots}`(方法内数据属性名不动)。改后实测子表格字段/操作列/彩色标签正常渲染。
346
+ - 排查:`grep -rE "domProps|nativeOn[A-Z]|scopedSlots" src`;`scopedSlots=`(等号)=JSX 属性可自动,`scopedSlots:`(冒号)=render 对象需人工。
347
+ - **第 5 类 `slot="xxx"`(postAppoint 补)**:`<script>`/JSX 区的具名插槽(`slot="reference"`/`slot="footer"`/`slot="title"`)同样被 Vue3 JSX 丢弃 → el-popover 触发钮不渲染(「更多」按钮消失)/ el-dialog 底部按钮·标题不显示。`fix-jsx-vue2` 现**报告**(列人工清单,不自动改——改 `v-slots` 要重组 JSX 结构,正则做不了)。⚠ **不能一律判失效**:实测 el-dropdown 的 `slot="dropdown"` 靠组件运行时兼容仍正常(`$slots` 含 dropdown),el-popover `reference` / el-dialog `footer`·`title` 才真失效——逐个 `$slots` 或 DOM 实证。postAppoint `MainEntityList..vue` 的 `slot="reference"`(卡片「更多」)实证失效,改 `v-slots={{ reference: () => (...) }}`(commit `d6386c3`)。
348
+
349
+ ### F15. JSX/render 组件的 `<style scoped>` 在 Vue3 深层失效(样式问题系统根因)
350
+ Vue3 的 scoped `data-v` 属性只在 `<template>` 编译时**逐元素**注入;**JSX/render 函数只有根元素运行时带 scopeId、深层元素不带** → 「无 `<template>` + `<style scoped>`」的组件,scoped 深层样式在 Vue3 **整体失效**(Vue2 的 vue-jsx 给所有元素加 scopeId,所以旧版正常、迁移后才塌)。
351
+ recruit 实测:简历审核详情左侧头像**撑满整栏**(平台 `CardFormItem`)——实证元素 `.awatar-container` 只有 class、**无 data-v**,而 scoped 规则是 `.awatar-container[data-v-1a175043]` → 不匹配、86×120 约束失效、图片按原图 728×1024 撑开;博士后基本资料照片则空白。**特征:build 过、console 不报错、样式默默塌**——功能层排查抓不到,必须 ①③ 样式对照才发现。
352
+ - **CLI(已加)**:⑦ 扫描告警(`migrate.mjs` 扫「无 `<template>` + `<style scoped>`」组件、标注含几处 ::v-deep,报人工清单),**不自动改**(根 class 命名 / extends 根 override / 作用域判断都是设计决策)。
353
+ - **修法**(dom-pitfalls 第7条,逐个手工):① `<style scoped>`→`<style>`;② 组件根加**唯一 class**——按组件形态四选一:(a) **自有 render** 根元素 class 数组追加 `class={['原class','epx-xxx']}`;(b) **extends/mixins 无自有根**(继承父类 render)用 `mounted(){ this.$el && this.$el.classList && this.$el.classList.add('epx-xxx') }`(比 override render/`data.className` 简单可靠,recruit 9 个 extends/mixins 组件均用此);(c) **多分支 render**(如 `Login`)各分支实际 DOM 都须位于命名空间内(如 `Login`,默认分支和 `renderPage` 自定义页面分支都要检查,见 F19);(d) **纯 BEM 组件**(class 已全局唯一,如 `.m-task-*` / `@include e()`)**仅去 scoped、不必包命名空间**(class 本身即隔离);③ 样式全包进该根 class(scss 嵌套 `.epx-xxx{…}`)防全局污染;④ 深层选择器去掉改普通后代选择器(非 scoped 本就穿透)——`::v-deep`/`/deep/`/`>>>`(旧)**与 `:deep(<inner>)`(新语法,平台组件常用,如 CardFormItem)都要去**:`:deep()` 在非 scoped `<style>` 里编译器不转换、会原样输出成无效 CSS 选择器被浏览器丢弃,须把 `:deep(.foo)` 改成 `.foo`;⑤ **对照①线上样式改**(extends 子类 scoped 作用域 Vue2 下本就微妙,别盲目让全部生效、反而多出样式)。
354
+ - **已验证**:recruit `ConditionFormItem`(自有 render、1 条 `.addCondition`)改后产物 css 由 `.addCondition[data-v-x]`(失效)变 `.epx-recruit-admin-condition-form-item .addCondition`(非 scoped、生效)。
355
+ - **排查**:`grep -rL "<template" --include=*.vue src`(无 template 的 .vue)∩ 有 `<style scoped>`。recruit 实测 9 个:EntitySubList / InnerEntitySubList / AddForm / ConditionFormItem / WriteFormItem / ExpertEntityList / ExpertSubList / ResumePage×2;平台侧 CardFormItem 等同病(平台仓修)。
356
+ - **机制精化(2026-09-14 实证)**:不是「scoped 深层样式整体失效」——精确说是**普通深层选择器失效、顶层 `:deep()` 仍生效**。scoped 下 `.foo{}` 编译成 `.foo[data-v]`(元素自身要带 data-v,深层元素没有→失效);而顶层 `:deep(.foo)` 编译成 `[data-v] .foo`(靠**根元素**的 data-v,根后代 .foo 命中→生效)。所以同是「头像」:**自定义图片框**(`.awatar-container`/`.logo`/`img` 用普通选择器写死尺寸)→ 失效撑满(真 bug);**用顶层 `:deep(.el-avatar)` 写的 EP 头像** → 靠根 data-v 生效、正常(不是 bug)。甄别「真坏」优先查「普通选择器 + 固定尺寸/图片框/绝对定位」,颜色字体类失效视觉退化轻。
357
+ - **平台侧同病是系统性的**:平台 `frontend/frame/packages` 实测 73 个 render+scoped 组件(结构风险)。已按本修法修的撑满型:`CardFormItem`(简历头像)/`NavAddForm`(卡片头像,均自有 render 根加 class)/`Login`(登录页 logo,默认分支根 `.container` 加 class)/`TaskList` 移动端头像块(全 BEM 唯一 class、仅去 scoped 即隔离)。**平台团队另在用「整簇搬到无 scoped 块 + BEM 命名(`@include e()`)」方式渐进迁移**(见 `TaskList.vue` 块注释)——改平台组件优先随此 BEM 方式、不要混用 `.epx-xxx` 包裹;extends 父类的组件(如 `HallNav` extends `AppNav`)改前先核实自身 style 的 class 是否还对应实际 DOM(父类 BEM 化后可能已是历史死样式)。
358
+ - **移动端 F15 验证技巧**:受控浏览器 `resize_window` 常无效(视口固定 1920)、UA 是桌面时,用 `Object.defineProperty(window,'innerWidth',{configurable:true,get:()=>390}); window.dispatchEvent(new Event('resize'))` 让 epx-frame 重算成 mobileViews(**仅骗 JS 判断、不改真实渲染视口**,截图仍宽屏;真移动端视觉需本机 DevTools 设备模式 / 真机窄视口——用户手动切设备模式后 `innerWidth/outerWidth` 才真变 390)。移动端页 F15 用 JS 量尺寸排查:`[...document.querySelectorAll('img,[class*=avatar],[class*=photo]')].map(el=>el.getBoundingClientRect())`,宽/高远超设计值/视口即撑满。recruit 移动端 site(`zpwzx`)实测首页 banner 293×165、缩略图 80×80、我的空状态图 150×130、职位列表——图片全正常无撑满。
359
+ - **CLI 静态检测(postAppoint 加)**:`detect-vue3-risks.mjs` R1 = extends+scoped+无 template+**裸类选择器**(`.foo{`),退出码 2 可 CI 拦;**已改 `:deep()` 的按裸选择器判定不误报**。postAppoint `MainEntityList..vue`(extends EntityList、卡片透明+状态点尺寸 0)实证:28 个裸类选择器 `.foo{`→`:deep(.foo){` 后卡片背景 `rgb(255,245,234)`/状态点 8×8/字号字重与线上一致(commit `d6386c3`)。⚠ scoped 块里若含平台类(`.epx-entity-list__*`)**别一律去 scoped**(会泄漏成全局污染其它页面),此时 `:deep()` 更稳。
360
+
361
+ ### F16. 数组 `splice` 同引用 + `deep:1` watcher 不触发(弹窗标题恒空,postAppoint 平台侧实证)
362
+ Vue3 起浅层 watcher 不响应 push/splice;`deep:1` 只监听「数组元素引用」这一层。若 `route.name=title; routes.splice(i,1,route)`——splice 放回**同一对象引用**、只改元素内部属性 `name`,`deep:1` 监听不到 → watcher 不触发 → `$emit('change')` 不重发 → 消费方(`Popup.routerChange`)拿到的 route.name 恒空 → **弹窗标题不显示**(build 过、无报错)。
363
+ - 修法:splice 放**浅拷贝新对象** `routes.splice(i,1,{...route,name:title})`(改元素引用才触发 `deep:1`);消费方再加空值兜底(异步 `$refs.component` 未 resolve 时用 `route.name`)。平台 `RouterView.pageChange` + `Popup.routerChange` 双改、build:pre 实测标题空→「新增公示」(commit `0061cb7c63`)。修前先运行时验证:手动 `rv.routes.splice(0,1,{...rv.routes[0]})` 看消费方值是否变,成立再改、免白 build。
364
+ - 通用:Vue3 靠 push/splice 增删的数组,watcher 要么 `deep`、要么 splice 换新对象引用;**改元素内部属性不触发浅层 watcher**(同类历史 commit `df29393`/`2161c04` 数组 watcher 补 deep)。
365
+
366
+ ### F17. CSS code-split 下组件借用别组件的全局样式(图表容器塌陷,postAppoint 平台侧实证)
367
+ Vue2 webpack 把全部 CSS 合进主 bundle,任何组件的非 scoped 全局样式总能加载;Vue3 vite **CSS code-split** 按组件拆 CSS chunk。若「实际渲染的组件」自身无 style、借用**别组件**里定义的非 scoped 全局样式(如 `.epx-chart` 布局散在 `chart/Table.vue`、`report-chart/Chart.vue` 的 `@include b(chart)`,而实际渲染的 `Common.vue`(epx-chart-common) 无 style),只加载渲染组件 chunk 时**不带那些 CSS chunk** → 样式缺失。实证:`.epx-chart__main` 高度塌陷 0、echarts 画不出(JS chunk 已加载、`getReportData` 数据正常)。
368
+ - 修法:让渲染组件**自带**渲染必需的样式(Common.vue 补 scoped `<style>` 定义 `.epx-chart` 根 flex+min-height / `__main` flex / `__echarts` 铺满,随本组件 chunk 加载、有 template 故 scoped 生效、零污染,commit `0061cb7c63`);别关 `cssCodeSplit`(牺牲首屏)。
369
+ - 排查判据:`getComputedStyle(容器).height==='0px'` + `performance` 里该组件 CSS chunk **不在**已加载列表(`document.styleSheets` 里也搜不到那几条规则)。
370
+
371
+ ### F18. `$emit` 不读实例 `__epxEvents`(程序化事件断链、内容区空白,postAppoint 平台侧实证)
372
+ Vue3 移除实例 `$on/$off`,平台改用实例级轻量事件 `$epxOn/$epxEmit`(handler 存实例 `__epxEvents`,见 core `mixins/methods-to-prototype.js`)。子组件(如 RouterView)用 `$parent.$epxOn('createPage',...)` 注册;而发射方仍是裸 `this.$emit('createPage')`——Vue3 `$emit` 只触发**父模板上**的监听器、**不读 `__epxEvents`** → 注册方永远收不到(emit 没人听的事件是合法 no-op、无报错)→ 内容区整块空白、点击无效。实证 `EntityTreeManage.vue`(机构管理右侧空白)。
373
+ - 修法:发射走「`$epxEmit` + `$emit`」**双发射**闸口(保留 `$emit` 以防模板上有 `@xxx`)。commit `543f85daf3`。
374
+ - 排查:全仓 grep `\.\$emit\('程序化事件名'`(createPage / backModule / push 等),逐个确认消费方是 `$epxOn` 注册还是模板绑定;全仓「没有任何模板 `@createPage`」即强信号。
375
+
376
+ ### F19. 非 scoped 样式改为根 class 命名空间后,其他渲染入口漏加 class(服务大厅登录页实证)
377
+
378
+ 迁移中把 `<style scoped>` 改成 `<style>`,并将规则包进 `.epx-xxx { ... }` 时,所有实际渲染入口都必须在该命名空间内。检查 `extends` 子组件覆盖的 `render()`、父组件的自定义页面分支、动态 `<component>` 和多分支根节点;只给默认分支加 class 不够。此问题与 `data-v` hash 失配不同:CSS 已加载、DOM 也有原来的内部类,但根节点缺少 `.epx-xxx`,整组选择器匹配不到,构建和控制台通常正常。
379
+
380
+ - **实证**:`Login.vue` 样式改为 `.epx-login` 包裹后,默认登录分支有该类;`ClientLogin.vue` 覆盖的根节点及配置 `LOGIN_PAGE` 走的 `renderPage()` 没有。`#/hall/login/frame` 渲染出 `.PCcontainer` 和 `.loginCon`,却没有 `.epx-login` 祖先,登录页失去布局。给 `ClientLogin` 根和 `renderPage()` 的页面容器补 class 后,浏览器实测 `.loginCon` 恢复绝对定位、宽 357px,`.PCcontainer` 铺满 1440×900 视口。
381
+ - **升级检查**:凡将 scoped 样式改为根 class 命名空间,逐个打开使用该组件的实际页面/分支,检查 `el.closest('.epx-xxx')` 是否存在,并用 `getComputedStyle(el)` 验证宽高、定位等关键规则。若 CSS 已加载但命名空间不存在,在相应渲染根或共同祖先补 class;不要靠失效的 `__scopeId` 转写补丁。静态扫描只能提示风险,页面验收仍需运行时确认。