diff --git a/docs/reports/2026-07-20-npm-designer-quality-report.md b/docs/reports/2026-07-20-npm-designer-quality-report.md new file mode 100644 index 0000000000000000000000000000000000000000..3e03c09873e77c77bffbf9c9bdf6152fd838c4dd --- /dev/null +++ b/docs/reports/2026-07-20-npm-designer-quality-report.md @@ -0,0 +1,254 @@ + + +# warm-flow-vue-designer 只读质量审查报告 + +审查对象:`warm-flow-vue-designer`(`@dromara/warm-flow-designer` v1.8.2,工作树含未提交改动)+ 三个 demo + 与 `warm-flow-ui` 基线的关系。所有结论均基于当前代码与 `dist-lib` 产物的实际检查(含 tsc 探针实测),未做任何修改。 + +--- + +## 一、缺陷(bug) + +### 发布面 / 类型 + +**1.【P1】`FlowDesigner` 组件在发布类型中退化为 `any`** +【文件】`dist-lib/src/designer/index.d.ts:2` ← `dist-lib/src/components/design/` 缺 `FlowDesigner.vue.d.ts` +【问题】主入口 d.ts `import { default as FlowDesigner } from '../components/design/FlowDesigner.vue'`,但 dts 产物只有 `FlowDesignerHeader/Toolbar.vue.d.ts`,`FlowDesigner.vue.d.ts` 未生成(同批缺失:`selectUser.vue`、`nodeExtList.vue`、`UserPicker*.vue`、`plugins/modal.ts`、`store/index.ts`)。已用 tsc 探针实测:`const n: number = FlowDesigner` 编译通过(应报 TS2322),即旗舰组件 props/事件对消费方全部失去类型。`todo.md`「发布类型退化为 any——已修复」的验证只覆盖了类型导出(`FlowDesignerInstance` 等),没覆盖组件本体。 +【建议】排查 vite-plugin-dts 对这批文件静默跳过 emit 的原因(大概率文件内 TS 诊断错误,如 `proxy.$refs.*` 调用链);修复后在 `build:lib` 加「d.ts 覆盖率断言」(对比 src 文件清单与 dist-lib 产物清单),防回归。 + +**2.【P2】`package.json` 声明 `vue-router` 为必选 peer,但库零引用** +【文件】`warm-flow-vue-designer/package.json:72` +【问题】`src/` 与 `dist-lib/warm-flow-designer.es.js` 均无 vue-router import(实测产物外部 import 仅 `vue/pinia/@logicflow/*`)。消费方被迫安装无用 peer;三个 demo 的 `dependencies` 也跟着带上了。 +【建议】从 peerDependencies/devDependencies/README:233/demo 依赖中移除。 + +**3.【P3】`file-saver` 声明为打包运行时依赖但源码零使用** +【文件】`package.json:96`;模块 `AGENTS.md` 也写「file-saver 导出」 +【问题】`rg file-saver|saveAs` 在 `src/` 无命中(`downJson` 用手写 `` 实现,`useLogicFlowCanvas.ts:664-676`)。死依赖 + 文档漂移。 +【建议】删除依赖并同步 AGENTS。 + +**4.【P3】ESM-only 形态与 `sideEffects` 风险** +【文件】`package.json:19-47` +【问题】无 `main`、exports 无 `require` 条件(ESM-only 是可接受决策但 README 未声明);`sideEffects: ["**/*.css"]` 把 JS 全部标记为无副作用,而主入口 import 时执行 `addCollection(epIcons/wfIcons)`(`src/icons/index.ts:15-16`)注册图标,理论上在激进 tree-shake 场景可被裁剪。其余方面 exports/types/style 路径与 dist-lib 实物一一对上(已核)。 +【建议】README 标注 ESM-only;将图标注册改为 `install()` 内显式调用,或在 sideEffects 中补主入口文件。 + +### 运行时逻辑 / 内存泄漏 + +**5.【P1】卸载后不恢复 `document.body.style.overflow`,宿主页面永久失去滚动** +【文件】`src/components/design/FlowDesigner.vue:343`(`onMounted` 设 `hidden`)、`420-423`(`onUnmounted` 只复位 `__WF_FLOW_DESIGN_MODE__`) +【问题】作为 npm 组件嵌入业务系统时,关闭设计器后整页不能滚动。`docs/warm-flow-npm-refactoring-plan.md` C8 早已规划「NPM 场景条件化」,未落地。 +【建议】mount 时记录原值、unmount 恢复;或按 C8 仅 iframe(`window.parent !== window`)场景设置。 + +**6.【P1】`window._markDrawerOpen/_markDrawerClosed` 挂载后永不清理(内存泄漏 + 多实例互踩)** +【文件】`src/composables/useLogicFlowCanvas.ts:244-258`(赋值)、`694-698`(onUnmounted 只移除 resize 与 `__WF_DESIGNER_DISABLED__`) +【问题】闭包持有 `lf`/`containerRef`,组件卸载后 LogicFlow 实例与 DOM 不能被回收;两个设计器实例并存时后挂载者覆盖前者。同族问题:`window.__WF_FLOW_DESIGN_MODE__`(FlowDesigner.vue:345/422,卸载置 `false` 而非恢复,多实例互踩)、`window.isTooltipHovered`(EdgeTooltip.vue:64-68)。 +【建议】unmount 时删除回调;根治方案是抽屉状态经 props/事件或模块级注册表传递,放弃 window 通道。 + +**7.【P2】`propertySetting.vue` 匿名 resize 监听器永不解绑** +【文件】`src/components/design/common/vue/propertySetting.vue:42-44` +【问题】setup 时 `window.addEventListener('resize', () => …)`,无对应 remove(对照 `baseInfo.vue:152-163` 是成对的)。每次挂载泄漏一个监听器与组件引用。 +【建议】`onUnmounted` 解绑,或用 `useWindowWidth` 类小 composable 统一管理(`between.vue:656-659`、`useUserPicker.ts:47` 还有多处 `window.innerWidth` 一次性读取,行为也不响应)。 + +**8.【P2】`between.vue` 校验链两处失效:`slice` 恒空 + Promise 竞态** +【文件】`src/components/design/common/vue/between.vue:686`、`670-683`(`warm-flow-ui/src/.../between.vue:708` 同病,属复制继承) +【问题】① `tabsList.value.slice(tabsList.value.length)` 永远返回 `[]`,「扩展页签(type=2 节点扩展)必填校验」是死代码;② `validate()` 中 `formRef.validate(callback)` 的异步 `reject` 与同步 `tabsValidate → resolve(true)` 赛跑,后端无节点扩展数据时(`nodeBase` 不存在)主表单校验结果被直接绕过——票签比例、监听器必填在关抽屉时可能不拦截。 +【建议】`slice(3)`(或记录初始 tab 数);`await` Promise 形态的 validate 后再串行执行 tabs 校验。 + +**9.【P2】`json2LogicFlowJson` 对缺省 `nodeRatio` 直接崩溃,demo 在替库兜底** +【文件】`src/components/design/common/js/tool.ts:59`(`node.nodeRatio.toString()`);佐证 `warm-flow-designer-demo/warm-flow-ep-designer-demo/demoProvider.ts:98-106`(注释明说「避免 …undefined.toString() 崩溃(如 nodeRatio)」) +【问题】`initialJson` / `v-model:json` 是新的公开契约,消费方手写 JSON 少个 `nodeRatio` 字段组件即抛 TypeError 白屏。 +【建议】库内 `String(node.nodeRatio ?? '0')` 兜底,demo 的规范化逻辑随之删除。 + +**10.【P2】`request.ts` 请求拦截器错误分支吞错** +【文件】`src/utils/request.ts:68-70` +【问题】`error => { Promise.reject(error) }` 缺 `return`,拦截器错误被吞、请求链以 undefined 继续(ruoyi 继承 bug)。另:line 45 `Object.keys(JSON.stringify(...)).length` 求字符串长度的写法晦涩;响应拦截器 `getUiAdapter()` 在未注册适配器时会抛错掩盖原始网络错误。 +【建议】补 `return Promise.reject(error)`;长度用 `.length`。 + +**11.【P2】`FlowDesigner` 保存链路无 catch + `JSON.parse` 无防护** +【文件】`src/components/design/FlowDesigner.vue:494-514`(`saveJson(...).then(...).finally(...)` 无 `.catch` → unhandled rejection)、`354`(`JSON.parse(localSource)` 对非法字符串直接抛出导致挂载失败) +【建议】补 catch(保存失败提示)与 try/catch(进入 `isEmpty` 空态)。 + +**12.【P3】`eventCenter.on('edge:click ', …)` 事件名带两个尾随空格** +【文件】`src/components/design/FlowDesigner.vue:602` +【问题】恰好 LogicFlow 内部对事件名做 `split(',') + trim()`(已在 `@logicflow/core` min 包实测)才得以工作,极脆弱。 +【建议】去掉空格。 + +**13.【P3】`propertySetting.vue` 跨选中切换时 watcher 误发** +【文件】`src/components/design/common/vue/propertySetting.vue:206-213`(`nodeName` watcher 无节点类型 guard)对照 `271-280`(`skipName` watcher 有 `['skip'].includes(...)` guard) +【问题】节点→边切换时会对边执行 `updateText(edgeId, undefined)` 与 `setProperties({nodeName: undefined})`,靠后注册的 `skipName` watcher 顺序恢复文本,属侥幸正确。 +【建议】所有 watcher 按 `props.node.type` 对称加 guard,或改为显式表单提交而非 12 个细粒度 watcher。 + +### UI 适配层(三适配器对齐) + +28 个语义键三家均已注册(已逐一核对 elementPlusAdapter:71-100 / antdvAdapter:718-748 / naiveAdapter:770-799,无键位缺失),但行为面有实际缺口: + +**14.【P2】非 EP 适配器下 ref 方法转发缺失 → 运行时 TypeError** +【文件】`src/components/design/common/vue/start.vue:129`、`end.vue:50`(`proxy.$refs.nodeInput.focus()`);`UserPickerTree.vue:94-95`(`treeRef.value?.filter(value)` / `setCurrentKey`);根因 `src/ui/antdvAdapter.ts:118-140`(AntInput)、`568-588`(AntTree)、naive 同构——包装组件均未 `expose` `focus/filter/setCurrentKey`,而 `createNeutralComponent.ts:27-43` 的 ref 代理只能转发到包装组件实例。 +【问题】antd/naive 下:开始/结束节点名称输入触发 `change` 即抛 `focus is not a function`(antd 的 a-input `change` 每击键触发一次);人员选择弹窗部门搜索框输入即抛 `filter is not a function`,且 `filter-node-method` 语义本身未翻译(部门过滤在非 EP 下不可用)。 +【建议】适配器包装组件统一 `expose` 常用方法(focus/blur/filter/setCurrentKey…)转发内层实例;树过滤在 antd/naive 侧用受控 `filteredData`/`pattern` 实现。 + +**15.【P2】naive 抽屉丢弃 `before-close` → 关抽屉前校验与节点编码改名链路失效** +【文件】`src/ui/naiveAdapter.ts:529`(把 `before-close` 直接 delete,`onUpdate:show` 直改 v-model)对照 `antdvAdapter.ts:505-516`(映射为 `onClose`)、EP 原生支持;消费点 `propertySetting.vue:11`(`:before-close="handleClose"`,内含表单校验 + `changeNodeId/changeEdgeId`) +【问题】naive 下点遮罩/关闭按钮直接关闭:必填不校验、修改的节点编码不落到画布 id。三栈行为不一致。 +【建议】NaiveDrawer 把 `before-close` 接到 `onUpdate:show` 前置调用(或 `onMaskClick`/`onEsc`)。 + +**16.【P2】antd `AntSelect` 未映射 `multiple`** +【文件】`src/ui/antdvAdapter.ts:217-247`(只处理 `allow-create→tags`);对照 `naiveAdapter.ts:231`(`multiple: !!multiple`)与 EP 原生;消费点 `nodeExtList.vue:18-19`(``) +【问题】antd 下「节点扩展属性」的多选下拉退化为单选,数组值塞进单选 Select 行为未定义。同类小缺口:`wf-time-picker :is-range`(nodeExtList.vue:66)在 antd/naive 均未翻译成 RangePicker。 +【建议】AntSelect 补 `mode: multiple ? 'multiple' : (allowCreate ? 'tags' : undefined)`;时间范围按需补映射。 + +**17.【P2】`isDrawerOpen` 用 `.el-drawer` DOM class 判定,非 EP 栈移动端触摸拦截失效** +【文件】`src/composables/useLogicFlowCanvas.ts:350-360` +【问题】「UI 无关」核心里写死 EP 的 DOM 结构;antd(`.ant-drawer-open`)/naive(`.n-drawer`)下抽屉打开时触摸事件照常转发给画布,移动端会隔着抽屉拖动画布。 +【建议】仅依赖 `_drawerActive` 标志(本就由 show/close 维护),删除 EP class 探测。 + +**18.【P3】适配器命令式反馈默认文案不一致 + 未接 i18n** +【文件】`elementPlusAdapter.ts:10`(`系统提示`)、`antdvAdapter.ts:659`(notify 默认 `提示`)/`664/671-674`(`系统提示/确定/取消`)、`naiveAdapter.ts:703/709-711/721-724` +【问题】同一 `notify` 三家默认标题不同(`系统提示` vs `提示`);所有按钮/标题硬编码中文,`setLocale('en')` 后仍是中文(`request.ts:59/91-96` 网络错误、`useFlowDesigner.ts:74` 亦同)。 +【建议】适配器内文案走 `translate('common.confirm')` 等既有 key(catalog 已有 confirm/cancel/tip)。 + +**19.【P3】`getUiAdapter()` 报错文案与现实相反** +【文件】`src/ui/uiAdapter.ts:83-91`(「或 app.use(WarmFlowDesigner)(会默认注册 Element Plus 适配器)」)、`43`(「默认实现为 Element Plus」) +【问题】主入口已 UI 无关,`install()`(designer/index.ts:78-91)不注册任何适配器;按报错提示操作无效,误导排障。 +【建议】改为「请先 setUiAdapter(elementPlusAdapter/antdvAdapter/naiveAdapter)(分别来自 /element-plus、/antdv、/naive 子入口)」。 + +### 数据层 / i18n / 杂项 + +**20.【未发现问题】DataProvider 契约与 api 委托层完全一致**:`provider.ts` 接口 11 个方法(saveJson…listenerList + config)与 `httpProvider.ts`、`mockProvider.ts` 实现、`api/flow/definition.ts` + `api/anony.ts` 委托一一对齐,`setDataProvider` 部分覆盖合并语义正确。仅注意 `AGENTS.md` 还提「DataProvider 的表单方法(getFormContent 等)暂保留」——实际已删除,文档漂移。 + +**21.【未发现问题】i18n 中英 catalog 完全对齐**:脚本实测 zh/en 均 187 个叶子 key,双向差集为空。次级问题【P3】:`FlowDesigner.vue:309` 注释行引用的 `flowDesigner.stepFormDesign` 不在 catalog(一旦启用表单设计步骤即回落 key 原文);`i18n/index.ts:17` 文档示例用了不存在的 `common.deleteConfirm`;12 个 `common.*` key(cancel/delete/tip/success…)暂无人使用;`setMessages`/`WfLocale` 类型是闭合联合 `'zh'|'en'`,与注释「可新增语言」矛盾。 + +**22.【P2】`useDark().initFromUrl` 在库形态下形同虚设 + pinia 隐性耦合** +【文件】`src/composables/useDark.ts:28`(`useAppStore().appParams`)、`src/store/app.ts:11`(`fetchTokenName` 全库无调用点、未导出) +【问题】`appParams` 只能由 `fetchTokenName()` 填充,而它在纯库/所有 demo 中都不被调用 → `initFromUrl` 读到的 params 恒为 null,`?theme=theme-dark`、`darkColors` URL 能力实际失效(warm-flow-ui 的 App 壳才会调它)。且 `useDark` 一被调用即触达 pinia,是 pinia 成为必选 peer 的唯一实质原因。另有死 import(useDark.ts:1 的 `onMounted, onUnmounted` 未使用)。 +【建议】`initFromUrl` 直接读 `window.location.search`(不再绕 store),可顺带把 pinia 降级为可选;或导出 `fetchTokenName` 并文档化。 + +**23.【P2】auto-import + `tsconfig.include` 残缺 → 类型质量塌陷** +【文件】`vite/plugins/auto-import.js:10`(`dts: false`)、`src/store/app.ts:4`(`defineStore` 无 import)、`src/store/index.ts:1`(`createPinia` 无 import)、`SvgIcon/index.vue:10`(`defineComponent/computed` 无 import)、`tsconfig.json:24`(include 不含 `src/components`/`store`/`plugins`/`utils`) +【问题】`dist-lib/src/store/app.d.ts` 实测内容为 `declare const useAppStore: any`;不在 include 的目录对 tsc/dts 是盲区(与缺陷 1 的 d.ts 缺失同源)。库代码依赖 auto-import 也降低可移植性。 +【建议】库源码全部显式 import(组件库不该依赖 auto-import);tsconfig include 覆盖整个 `src`。 + +**24.【P3】`SvgIcon` 的 `