yudao框架使用v-scale-screen缩放插件遇到的一些问题
主要问题当菜单导航栏是横向时下拉子菜单不会随着插件缩放原因子菜单没有在插件里面而是在body中和app是统一层级解决方案使用zoom同步缩放也试过transform但是这个不缩放布局只缩放像素代码我目前是单独写了给ts文件然后在App.vue页面调用并同步到index.scss里面设置子菜单的样式ts文件import { onMounted, onBeforeUnmount, nextTick } from vue /** * 把 v-scale-screen 的 .screen-wrapper 实时缩放比例同步到全局 CSS 变量 --screen-scale。 * * 背景v-scale-screen 通过 transform: scale() 缩放 .screen-wrapper但 Element Plus 的弹层 * 菜单子菜单 .el-menu--popup、el-select 下拉框 .el-select-dropdown 等默认 teleport 到 * document.body脱离 .screen-wrapper 的 transform 容器不会被自动缩放。 * * 本 hook 只负责“把比例写到 CSS 变量”具体缩放交给 CSS * .el-menu--popup { zoom: var(--screen-scale, 1); } * .el-select-dropdown { zoom: var(--screen-scale, 1); } * 用 zoom 而非 transformzoom 会缩放“布局”min-width / padding / 字号一起按比例缩并重新排版 * 不像 transform 那样只缩放绘制像素、不改变布局。 * * 健壮性 * - v-scale-screen 写入 transform 的时机晚于本 hook 的初次 setup多次 nextTick 防抖 * 提供轮询兜底 计算样式兜底确保 --screen-scale 一定能读到真实值。 * - App.vue 在登录/非登录页切换时会整体替换 .screen-wrapper 节点监听 body 子树变化以重新接管。 */ // 匹配 transform 里的 scale(0.8,0.8) / scale(0.8) const SCALE_VALUE_RE /scale\(\s*(-?[\d.])\s*(?:,\s*(-?[\d.])\s*)?\)/i // 兜底匹配 matrix(a,b,c,d,e,f)scale sqrt(a² b²) const MATRIX_RE /matrix\(([^)])\)/i // v-scale-screen 的缩放容器优先类名其次外层 .v-screen-box 的直接子节点 const WRAPPER_SELECTOR .screen-wrapper, .v-screen-box div const DEBUG Boolean(import.meta.env?.DEV) const log (...args: unknown[]): void { if (DEBUG) console.debug([useScreenScale], ...args) } export function useScreenScale(): void { let currentScale 1 let wrapperObserver: MutationObserver | null null let bodyObserver: MutationObserver | null null let observedWrapper: Element | null null let pollTimer: ReturnTypetypeof setInterval | null null let resizeRaf 0 /** 从一段 transform 文本中解析出 scale 数值 */ const parseScale (transform: string): number { if (!transform || transform none) return 1 const sm transform.match(SCALE_VALUE_RE) if (sm) { const x parseFloat(sm[1]) const y sm[2] ? parseFloat(sm[2]) : x return Number.isFinite(x) Number.isFinite(y) ? Math.min(x, y) : x } const m transform.match(MATRIX_RE) if (m) { const parts m[1].split(,).map((s) parseFloat(s.trim())) const a parts[0] ?? 1 const b parts[1] ?? 0 const d Math.hypot(a, b) if (Number.isFinite(d) d 0) return d } return 1 } /** 查找 v-scale-screen 的缩放容器节点 */ const findWrapper (): Element | null { const el document.querySelector(WRAPPER_SELECTOR) if (el) return el // 兜底扫描 body 下 inline transform 含 scale( / matrix( 的元素 const all document.querySelectorAll(body *) for (const node of all) { const t (node as HTMLElement).style.transform || if (SCALE_VALUE_RE.test(t) || MATRIX_RE.test(t)) return node } return null } /** 读取 wrapper 的 scaleinline 优先计算样式兜底同步到 CSS 变量 */ const readScale (wrapper: Element): void { const el wrapper as HTMLElement let transform el.style.transform || if (!transform || transform none) { // v-scale-screen 也可能通过 class 写 transform用计算样式兜底 transform getComputedStyle(el).transform || } const next parseScale(transform) if (next ! currentScale) { currentScale next document.documentElement.style.setProperty(--screen-scale, String(currentScale)) log(scale 更新, currentScale) } } /** 接管 wrapper读取初始 scale 监听其 style 变化v-scale-screen resize 时会改写 transform */ const observeWrapper (): void { const wrapper findWrapper() if (!wrapper) { log(未找到 wrapper) return } if (observedWrapper ! wrapper) { wrapperObserver?.disconnect() observedWrapper wrapper wrapperObserver new MutationObserver(() readScale(wrapper)) wrapperObserver.observe(wrapper, { attributes: true, attributeFilter: [style] }) log(已接管 wrapper, wrapper) } readScale(wrapper) } /** * 监听 document.body 子树节点增删仅用于识别 .screen-wrapper / .v-screen-box 被替换 * App.vue 登录/非登录页切换 v-if/v-else 会整体替换 v-scale-screen 节点。 */ const observeBody (): void { const isWrapperNode (node: Node | null): boolean { if (!node || node.nodeType ! Node.ELEMENT_NODE) return false const el node as Element return ( el.classList?.contains(screen-wrapper) || el.classList?.contains(v-screen-box) ) } bodyObserver new MutationObserver((mutations) { let needRescan false for (const m of mutations) { m.addedNodes.forEach((node) { if (isWrapperNode(node)) needRescan true }) m.removedNodes.forEach((node) { if (!isWrapperNode(node)) return // 旧 wrapper 被移除断开其 observer等待 addedNodes 里的新 wrapper 触发重新接管 wrapperObserver?.disconnect() observedWrapper null needRescan true }) } if (needRescan) observeWrapper() }) bodyObserver.observe(document.body, { childList: true, subtree: true }) } /** 轮询兜底v-scale-screen 的 transform 写入晚于初次 setupobserver 理论上可能漏触发 */ const startPolling (): void { let ticks 0 const maxTicks 20 // 前 10 秒内每 500ms 检查一次 pollTimer setInterval(() { ticks 1 observeWrapper() if (ticks maxTicks pollTimer) { clearInterval(pollTimer) pollTimer null log(轮询兜底结束) } }, 500) } const onResize (): void { if (resizeRaf) cancelAnimationFrame(resizeRaf) resizeRaf requestAnimationFrame(observeWrapper) } const setup (): void { observeWrapper() observeBody() startPolling() window.addEventListener(resize, onResize) } const teardown (): void { wrapperObserver?.disconnect() bodyObserver?.disconnect() if (pollTimer) { clearInterval(pollTimer) pollTimer null } if (resizeRaf) cancelAnimationFrame(resizeRaf) window.removeEventListener(resize, onResize) wrapperObserver null bodyObserver null observedWrapper null document.documentElement.style.removeProperty(--screen-scale) } onMounted(() { nextTick(setup) // 双保险再延后一帧执行覆盖 nextTick 仍早于 v-scale-screen 写入的情形 requestAnimationFrame(setup) }) onBeforeUnmount(teardown) }App.vuescript langts setup import { useScreenScale } from ts文件路径 // 同步 v-scale-screen 的缩放比例到 CSS 变量 --screen-scale // 供脱离 .screen-wrapper 的弹层菜单子菜单 popper 等自行缩放 useScreenScale() /scriptindex.scss/* * 菜单子菜单 popper 的“布局级”缩放。 * * 子菜单 popper 被 teleport 到 body脱离 v-scale-screen 的 .screen-wrapper * 默认不会被缩放。useScreenScale 已把比例同步到全局变量 --screen-scale。 * * 这里把 zoom 作用在子菜单的内容层 .el-menu--popupul上而不是外层 .el-popper * - .el-popper 由 Popper.js 用 top/left 定位gpuAcceleration 已关闭 * 给它加 zoom 会改变 Popper.js 测量到的尺寸触发视口守卫/与 el-zoom-in-top * 进场动画冲突导致子菜单不显示。内容层不参与定位加 zoom 安全。 * - zoom 会缩放“布局”min-width: 200px、padding、字号一起按比例缩并重新排版 * 不像 transform 那样只缩放绘制像素能真正实现内容变紧凑。 * 配合 useScreenScale 中对子菜单 popper 跳过 transform 注入避免 transformzoom 双重叠加。 */ .el-menu--popup { zoom: var(--screen-scale, 1); } /* * el-select 下拉框的“布局级”缩放。 * * 与子菜单不同el-select 的 gpuAcceleration 走默认值 truePopper.js 用 transform 定位 * 不是 top/left所以可以把 zoom 直接作用在 popper 根节点 .el-select-dropdown 上—— * zoom 与 transform 是两个独立 CSS 属性可共存不会像子菜单那样因破坏 Popper.js 的 * top/left 测量而隐藏。 * * zoom 作用在 popper 根节点的收益 * - 连同 Element Plus 写在根节点上的 inline min-width触发器 offsetWidth未缩放逻辑值 * 一起按比例缩下拉框宽度自动贴合“缩放后的输入框” * - 内容列表项的字、padding、行高一并缩放并重新排版。 * 配合 useScreenScale 中对 .el-select-dropdown 跳过 transform 注入避免 transformzoom 双重叠加。 */ .el-select-dropdown { zoom: var(--screen-scale, 1); }说明el-select下拉框以及其它出现这个不随着缩放插件进行缩放的基本上都是这个情况可以根据情况添加以上代码我只添加了子菜单和下拉框