From d46cb7f93dcfe88082870cb903b2a373b6c6e276 Mon Sep 17 00:00:00 2001 From: chenqi Date: Wed, 3 Jun 2026 15:12:48 +0800 Subject: [PATCH] =?UTF-8?q?```=20feat(build):=20=E6=B7=BB=E5=8A=A0=20Vue?= =?UTF-8?q?=203=20=E5=85=BC=E5=AE=B9=E6=80=A7=E8=A1=A5=E4=B8=81=E6=8F=92?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了 Vite 插件来拦截依赖模块加载并返回兼容 Vue 3 的补丁版本 - 添加 xe-utils hasOwnProp 补丁解决 Proxy 兼容性问题 - 添加 element-plus form-label-wrap 补丁防止 NaN 宽度和生命周期错误 - 实现虚拟模块系统避免修改 node_modules 文件 - 添加 _isMounted 守卫防止组件卸载后访问已销毁的上下文 - 实现缓存机制优化补丁代码加载性能 ``` --- .../src/patches/patch-deps-plugin.js | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 openhis-ui-vue3/src/patches/patch-deps-plugin.js diff --git a/openhis-ui-vue3/src/patches/patch-deps-plugin.js b/openhis-ui-vue3/src/patches/patch-deps-plugin.js new file mode 100644 index 000000000..ab4169ff3 --- /dev/null +++ b/openhis-ui-vue3/src/patches/patch-deps-plugin.js @@ -0,0 +1,143 @@ +/** + * Vite 插件:在构建时拦截依赖模块加载,返回兼容 Vue 3 的补丁版本。 + * + * ⚠️ 不修改 node_modules 中的任何文件。 + * + * 拦截清单: + * 1. xe-utils/hasOwnProp.js — Vue 3 Proxy 兼容 + * 2. element-plus form-label-wrap.mjs — NaN 防护 + 生命周期守卫 + */ + +const VIRTUAL_HASOWNPROP = '\0patched:xe-utils/hasOwnProp' +const VIRTUAL_FORM_LABEL = '\0patched:element-plus/form-label-wrap' + +export default function patchDepsPlugin() { + return { + name: 'patch-deps-vue3-compat', + enforce: 'pre', + + // ── resolveId: 拦截模块 ID,返回虚拟模块路径 ── + resolveId(source, importer) { + // 拦截 xe-utils 内部的 require('./hasOwnProp') + if ( + source === './hasOwnProp' && + importer && + importer.includes('xe-utils') + ) { + return VIRTUAL_HASOWNPROP + } + // 拦截 element-plus form-label-wrap(完整路径匹配) + if ( + source.includes('element-plus') && + source.includes('form-label-wrap') + ) { + return VIRTUAL_FORM_LABEL + } + }, + + // ── load: 对被拦截的模块返回补丁代码 ── + load(id) { + if (id === VIRTUAL_HASOWNPROP) { + return PATCHED_HASOWNPROP + } + if (id === VIRTUAL_FORM_LABEL) { + return PATCHED_FORM_LABEL_WRAP + } + }, + } +} + +// ═══════════════════════════════════════════════ +// 补丁 1:xe-utils hasOwnProp — Proxy 兼容 +// ═══════════════════════════════════════════════ +// 根因:Object.prototype.hasOwnProperty.call(proxyObj, key) +// 在 Vue 3 reactive Proxy 上触发 reactivity 拦截, +// 抛出 "obj.hasOwnProperty is not a function"。 +// ═══════════════════════════════════════════════ +const PATCHED_HASOWNPROP = ` +function hasOwnProp(obj, key) { + if (obj == null) return false + try { + return Object.prototype.hasOwnProperty.call(obj, key) + } catch (e) { + try { return key in Object(obj) } catch (e2) { return false } + } +} +export default hasOwnProp +export { hasOwnProp } +` + +// ═══════════════════════════════════════════════ +// 补丁 2:element-plus form-label-wrap +// ═══════════════════════════════════════════════ +// 根因:vxe-table 展开行收起时 el-form 组件已卸载, +// 但 nextTick/onUpdated 回调仍访问已销毁的 formContext, +// 导致 NaN width 和 teardown 错误。 +// +// 策略:从原始文件读取内容,在运行时用正则替换。 +// 这样不依赖 node_modules 中的修改。 +// ═══════════════════════════════════════════════ +import fs from 'fs' +import path from 'path' + +let cachedFormLabelWrap = null + +function getFormLabelWrapCode() { + if (cachedFormLabelWrap) return cachedFormLabelWrap + + const filePath = path.resolve( + process.cwd(), + 'node_modules/element-plus/es/components/form/src/form-label-wrap.mjs' + ) + + if (!fs.existsSync(filePath)) { + console.warn('[patch-deps] form-label-wrap.mjs not found, skipping') + return null + } + + const code = fs.readFileSync(filePath, 'utf-8') + + cachedFormLabelWrap = code + // NaN 防护 + .replace( + 'return Math.ceil(Number.parseFloat(width))', + 'return Math.ceil(Number.parseFloat(width)) || 0' + ) + // _isMounted 守卫 + .replace( + 'const updateLabelWidth = (action = "update") => {', + 'let _isMounted = true;\n const updateLabelWidth = (action = "update") => {' + ) + .replace( + 'nextTick(() => {', + 'nextTick(() => {\n if (!_isMounted) return;' + ) + .replace( + 'if (slots.default && props.isAutoWidth) {', + 'try {\n if (slots.default && props.isAutoWidth) {' + ) + .replace( + 'else if (action === "remove") formContext?.deregisterLabelWidth(computedWidth.value);', + 'else if (action === "remove") formContext?.deregisterLabelWidth(computedWidth.value);\n }\n } catch (e) { /* teardown race */ }' + ) + .replace( + 'onBeforeUnmount(() => {', + 'onBeforeUnmount(() => {\n _isMounted = false;' + ) + .replace( + 'onUpdated(() => updateLabelWidthFn())', + 'onUpdated(() => { if (_isMounted) updateLabelWidthFn(); })' + ) + .replace( + 'if (props.updateAll) formContext?.registerLabelWidth(val, oldVal);', + 'if (_isMounted && props.updateAll) formContext?.registerLabelWidth(val, oldVal);' + ) + .replace( + 'return () => {', + 'return () => {\n if (!_isMounted) return null;' + ) + + return cachedFormLabelWrap +} + +const PATCHED_FORM_LABEL_WRAP = getFormLabelWrapCode() \ No newline at end of file