Vue项目125%缩放适配:rem与transform scale方案详解

发布时间:2026/10/1 9:11:23
Vue项目125%缩放适配:rem与transform scale方案详解 上周同事甩给我一张截图说他的 Vue 后台在办公室台式机上看得好好的换到自己那台 1920×1080 的笔记本上右侧栏直接消失侧边菜单挤成一条缝表格最后一列被硬生生截断。我问他系统缩放是不是 125%他愣了一下说对啊出厂默认就这样。问题到这里基本就锁定了——不是代码写错了是1920×1080 分辨率配上 125% 系统缩放浏览器拿到的 CSS 视口宽度根本不是 1920。这篇内容就是围绕这个场景写的一个 Vue 项目的页面怎么才能在 1920×1080 加 125% 缩放的笔记本上正常显示同时又不牺牲在标准 1920 或者 1366 屏幕上的观感。我会把问题成因、三种主流适配路线的取舍、rem 与 transform scale 两套完整可抄的代码、以及我自己踩过的一堆坑全部摊开讲。适合正在做后台管理系统、数据大屏、监控看板的 Vue 开发看也适合刚接手别人项目、被为什么我这里排版全乱了折磨过的同学。1. 先把问题看透125%缩放到底改变了什么1.1 物理像素、CSS像素和设备像素比这三个概念必须分清很多人调布局时盯着我的屏幕是 1920×1080这个 1920 是物理像素是屏幕面板上真实存在的发光点数量。但浏览器排版用的是CSS 像素两者之间隔着一个缩放系数。Windows 在 125% 缩放下等于告诉系统和所有应用把 1 个 CSS 像素画成 1.25 个物理像素。于是浏览器能用的 CSS 空间变成 1920 ÷ 1.25 1536。这个换算关系对应的就是window.devicePixelRatio在 125% 缩放下它等于 1.25。你可以打开控制台敲一行window.devicePixelRatio再敲一行document.documentElement.clientWidth看看实际值。在我手上的机器里1920×1080 加 125% 缩放devicePixelRatio是 1.25clientWidth是 1536如果有竖向滚动条还会更少通常是 1536 减去滚动条宽度。同时screen.width返回的也是 1536不是 1920——这一点很关键很多人以为screen.width是物理分辨率其实 Chrome 在 Windows 上返回的是 CSS 像素值。顺便说一句浏览器自身的缩放Ctrl 加号减号和系统缩放会叠加。用户把浏览器调到 110%再叠加系统 125%实际devicePixelRatio会变成 1.375clientWidth进一步缩到 1396 左右。所以做布局自适应时永远别假设视口宽度是某个固定值。1.2 常见缩放组合下的真实视口对照光说理论不够直观我整理了一张表覆盖了目前市面上最常见的几种笔记本配置你可以对照自己的机器看一眼物理分辨率系统缩放devicePixelRatio实际 CSS 视口宽常见机型定位1920×1080100%11920外接显示器、老款台式1920×1080125%1.251536主流 14/15 寸笔记本默认1920×1080150%1.51280部分 14 寸高分屏出厂设置2560×1440150%1.517062K 屏笔记本2880×1800200%21440高端轻薄本3840×2160250%2.515364K 屏笔记本看最后一行4K 屏 250% 缩放和 1080P 屏 125% 缩放最终 CSS 视口宽度居然都是 1536。这件事的意义是你不需要为每一种分辨率单独适配你真正要对齐的是 CSS 视口宽度这个维度。把适配的靶心从1920改成1536 到 1920 这个区间思路会立刻清晰很多。1.3 三类典型的翻车现场理解了上面的换算再看下面这些现象就不会觉得莫名其妙了。第一类是媒体查询永远不生效。很多人习惯这么写断点media (min-width: 1920px) { ... }。在 125% 缩放的笔记本上视口只有 1536这个查询永远命中不了你为大屏写的两栏布局、更大的字号、更宽松的间距全部失效页面退回默认样式看起来就是排版乱了。这个坑我自己踩过两次第二次才发现问题根本不在 CSS 写得对不对。第二类是固定像素宽度导致溢出。侧边栏写死 240px主内容区写死min-width: 1400px加一起 1640px 已经超过 1536于是底部横出一条滚动条或者主内容被 flex 压缩。flex 的默认flex-shrink: 1会把子项往死里压表格单元格被挤到只剩几十像素文字疯狂换行看起来像布局崩塌。第三类是绝对定位元素被裁掉。顶部固定的用户信息区、右上角的消息弹层、右下角的悬浮按钮如果用了right: 0加固定宽度在 1536 视口下位置会整体左移和左侧内容叠在一起。这一类和前面两类不同它不是溢出而是位置算错了。还有一种更隐蔽的UI 组件库内部尺寸和你的布局打架。以 Element Plus 为例它自己的表格、日期选择器内部有大量固定 px你的全局缩放如果只作用于自己写的元素就会出现菜单缩了、表格没缩的错位感。这类问题在后面第 3 节会专门讲怎么处理。2. 适配方案选型别一上来就改代码2.1 方案Arem 动态根字号后台管理系统的首选思路很朴素让页面上所有尺寸都以rem为单位然后用 JS 根据当前视口宽度动态设置html的font-size。视口 1920 时根字号设成 100px视口 1536 时根字号设成 80px那么一个设计稿里 300px 宽的卡片写成3rem在两种视口下会分别渲染成 300px 和 240px比例完全一致。这个方案的最大优势是整个页面的所有元素等比缩放不需要你写任何断点一套代码从 1366 到 1920 无缝过渡。缺点也很明确依赖构建工具做 px 到 rem 的转换内联样式和 canvas 绘制的内容要单独处理另外根字号变小后字号会触及浏览器最小字号限制这个坑值得单开一节讲。适合后台管理系统、表单密集的业务系统、需要严格还原设计稿比例的项目。2.2 方案Btransform scale 整体缩放大屏项目的标准答案大屏看板这类项目有个特点设计稿就是 1920×1080 一张大图元素位置全是绝对定位比例不能有任何变形。这种场景下 rem 反而不好用因为你很难把每个绝对定位的坐标都换算成 rem。transform: scale()的做法是外层容器 100% 撑满内层容器写死设计稿尺寸 1920×1080然后用scale min(视口宽/1920, 视口高/1080)整体缩放内层。因为缩放是基于容器的所有子元素、文字、图片、canvas 一起缩放视觉上和设计稿完全等比。这里有个额外选择CSS 的zoom属性。现代浏览器已经普遍支持它而且zoom参与布局计算——也就是说zoom: 0.8之后元素的实际占位宽度就是原来的 0.8 倍鼠标事件的坐标也自动换算好了不像transform: scale()那样需要你手动反算。我在一个中等规模的大屏项目里用过zoom效果不错代价是 ECharts 这类 canvas 库在zoom下的渲染会和鼠标位置有一些微妙偏差需要多测几个交互。适合数据大屏、数字孪生、监控看板、展厅一体机。2.3 方案C纯弹性布局加断点内容型站点的稳妥路线如果你的项目是官网、文档站、内容页这类以文字阅读为主的东西那其实不需要等比缩放。用户在小屏上本来就更希望文字大一点、内容少一点而不是所有东西一起缩小。这种场景用 flex、grid、minmax()、clamp()组合就够了。比如网格写成grid-template-columns: repeat(auto-fill, minmax(280px, 1fr))字号写成font-size: clamp(14px, 0.9vw, 18px)再配合 1366、1536、1920 三档断点微调。代码干净、无障碍友好、不依赖 JS。适合官网、博客、文档、营销页。2.4 三种方案的横向对比与组合策略对比维度rem 动态根字号transform scale弹性布局加断点适配方式等比缩放等比缩放流式重排是否依赖 JS是是否内联样式是否生效否需手动换算是是是否触发最小字号限制会不会会事件坐标是否需换算否是否文字清晰度正常非整数倍缩放时略糊正常学习成本中低低典型场景后台管理系统大屏看板内容站点实际项目里最常见的做法是组合后台管理系统用 rem 做整体比例局部列表页用弹性布局兜底大屏项目用 transform scale 做整体缩放图表内部用 ECharts 自己的 resize 逻辑处理。别追求一个方案通吃所有页面这不现实。3. 动手实操rem 方案从零配置3.1 依赖安装与版本选择需要两个东西一个负责把 CSS 里的 px 自动转成 rem一个负责在运行时动态设置根字号。前者现在主流选择是postcss-pxtorem后者很多人用amfe-flexible但那个库是为移动端设计的默认按 750 设计稿算用在大屏上要改一堆参数我建议直接手写一个十几行的脚本可控性更高。# 安装转换插件构建工具用 Vite / Webpack 都一样 npm i -D postcss-pxtorem这里提醒一句网上不少教程推荐px2rem-loader那是 Webpack 时代的产物在 Vite 项目里用不了或者要绕很多弯。用 Vite 就老老实实走 PostCSS 插件配置更简单。3.2 postcss 配置的三个关键参数在项目根目录建postcss.config.js// postcss.config.js export default { plugins: { postcss-pxtorem: { // 设计稿 1920 宽约定 1rem 100px所以 rootValue 填 100 rootValue: 100, // 转换所有属性包括 font、border、box-shadow propList: [*], // 类名包含 no-rem 或 ignore- 的元素不转换用于兜底 selectorBlackList: [no-rem, ignore-], // 小于 2px 的值不转换避免 1px 边框被转成 0.01rem 后渲染消失 minPixelValue: 2, // 第三方 UI 库不转换否则组件内部尺寸和布局会对不上 exclude: /node_modules/i } } }四个参数里rootValue和exclude最要命。rootValue的算法是这样的设计稿宽 1920你希望 1rem 在设计稿里代表多少像素我选 100因为换算最直观——设计稿上量出来 320px写成 3.2rem 就行心算一秒完成。那么rootValue就等于 100。如果你习惯 1rem 等于 16px 那种方案rootValue填 16但设计稿量出来的 320px 要写成 20rem笔算容易出错。exclude排除node_modules这件事网上争议挺大。我的观点是如果项目里 UI 库用得不多、且视觉要求高排除掉更安全。因为 Element Plus、Ant Design Vue 这些库内部有大量的固定像素逻辑比如下拉框的定位偏移、表格列的宽度计算你把这些 px 转成 rem一旦根字号变化组件内部算出来的位置和实际渲染位置就可能不一致出现浮层位置偏了一截这种诡异现象。排除之后UI 库保持固定尺寸你自己的业务代码等比缩放视觉上会有一点不一致但至少功能是好的。反过来如果你追求完全等比那就得把 UI 库也纳入转换但要留足测试时间。3.3 动态根字号脚本一定要加区间钳制在src/utils/下新建flexible.js// src/utils/flexible.js const DESIGN_WIDTH 1920 // 设计稿里 1rem 对应 100 个设计像素和 postcss 的 rootValue 保持一致 const BASE_FONT_SIZE 100 // 缩放比例下限对应 1366 宽屏幕1366 / 1920 ≈ 0.71 const MIN_SCALE 0.7 // 缩放比例上限防止 2K、4K 屏上元素被放得过大 const MAX_SCALE 1.3 let resizeTimer null function setRootFontSize() { const clientWidth document.documentElement.clientWidth if (!clientWidth) return let scale clientWidth / DESIGN_WIDTH // 区间钳制这一步是整个脚本的灵魂 scale Math.min(Math.max(scale, MIN_SCALE), MAX_SCALE) const fontSize BASE_FONT_SIZE * scale document.documentElement.style.fontSize fontSize px } function handleResize() { // 防抖 200ms避免拖动窗口时根字号高频跳变导致页面抖动 clearTimeout(resizeTimer) resizeTimer setTimeout(setRootFontSize, 200) } // 首次执行要在 DOM 内容加载后避免 clientWidth 为 0 if (document.readyState loading) { document.addEventListener(DOMContentLoaded, setRootFontSize) } else { setRootFontSize() } window.addEventListener(resize, handleResize) window.addEventListener(orientationchange, handleResize)然后在main.js里第一行引入// src/main.js import ./utils/flexible import { createApp } from vue import App from ./App.vue createApp(App).mount(#app)为什么必须加MIN_SCALE和MAX_SCALE因为不做钳制的话有人在 3840 宽的显示器上打开你的后台根字号会变成 200px整个页面元素放大一倍一个按钮占半个屏幕没法用。反过来在小窗口里根字号会掉到 40px 以下文字小到看不清。钳制区间把适配范围锁定在合理范围内超出部分交给滚动条这才是真实可用的方案。3.4 在 Vue 里怎么用、哪些地方不能用配置完成后你写在style里的 px 会被自动转换这部分基本无感。但有三个地方要注意。内联样式不会转换。像div :style{ width: 320px }这种写法PostCSS 处理不到。解决办法是提前算出 rem 值写一个工具函数// src/utils/px2rem.js const BASE_FONT_SIZE 100 // 把设计稿上的 px 值转成 rem 字符串用于内联样式 export function px2rem(px) { return (Number(px) / BASE_FONT_SIZE).toFixed(4) rem }用的时候:style{ width: px2rem(320) }虽然麻烦一点但至少不会漏。Canvas 和 SVG 不吃 rem。ECharts 的fontSize、grid间距、symbolSize这些参数都是纯数字单位是 canvas 内部像素。你得在初始化图表时读一次根字号自己乘系数// 读取当前根字号按比例换算图表参数 function getScaleFactor() { const rootFontSize parseFloat( getComputedStyle(document.documentElement).fontSize ) return rootFontSize / 100 } const factor getScaleFactor() const option { textStyle: { fontSize: Math.round(14 * factor) }, grid: { left: Math.round(40 * factor), right: Math.round(40 * factor) } }1px细线要特殊对待。在minPixelValue: 2的保护下所有 1px 边框都不会被转成 rem保持固定。听着是好事但在 125% 缩放下 1px 会被渲染成 1.25 个物理像素浏览器做抗锯齿后可能看起来比实际浅某些深色主题下甚至像缺了一块。如果你很在意这条线可以写一个全局工具类/* 高 DPR 下用缩放模拟更细的线条 */ .hairline-bottom { position: relative; } .hairline-bottom::after { content: ; position: absolute; left: 0; bottom: 0; width: 100%; height: 1px; background: currentColor; transform: scaleY(0.8); transform-origin: bottom; }4. 动手实操大屏项目的 transform scale 方案4.1 容器结构与 transform-origin 的讲究大屏适配的核心是两句话外层容器撑满视口内层容器保持设计稿尺寸并用scale缩放。transform-origin必须设成left top因为默认值是center center缩放后元素会围绕中心点收缩四周留出空白位置全乱。/* 外层容器撑满整个视口隐藏溢出 */ .screen-wrapper { position: relative; width: 100vw; height: 100vh; overflow: hidden; background: #0b1020; } /* 内层容器固定设计稿尺寸左上角对齐 */ .screen-content { position: absolute; left: 0; top: 0; background: #0b1020; }缩放值由 JS 计算scale min(视口宽 / 1920, 视口高 / 1080)。为什么要取 min 而不是直接用宽度因为如果只按宽度算在一个宽而矮的窗口里比如用户手动拖成 1920×600内层高度 1080 缩放后仍然超过可视高度底部内容被裁掉。取 min 保证宽高都放得下代价是可能出现留白留白部分用背景色填充即可。4.2 封装成 Vue 组件一次配置全项目复用在src/components/下建ScreenAdapter.vue!-- src/components/ScreenAdapter.vue -- template div refwrapperRef classscreen-wrapper div classscreen-content :stylecontentStyle slot / /div /div /template script setup import { ref, reactive, onMounted, onBeforeUnmount, computed } from vue const props defineProps({ designWidth: { type: Number, default: 1920 }, designHeight: { type: Number, default: 1080 }, // 是否在缩放后发送全局事件方便图表组件联动 broadcast: { type: Boolean, default: true } }) const wrapperRef ref(null) const state reactive({ scale: 1, offsetX: 0, offsetY: 0 }) let resizeTimer null const contentStyle computed(() ({ width: props.designWidth px, height: props.designHeight px, transform: translate(${state.offsetX}px, ${state.offsetY}px) scale(${state.scale}), transformOrigin: left top })) function computeScale() { const el wrapperRef.value if (!el) return const { clientWidth, clientHeight } el // 取宽高比例的较小值保证内容完整可见 const scale Math.min( clientWidth / props.designWidth, clientHeight / props.designHeight ) state.scale scale // 计算居中偏移量避免宽高比不一致时内容贴左上角 state.offsetX (clientWidth - props.designWidth * scale) / 2 state.offsetY (clientHeight - props.designHeight * scale) / 2 if (props.broadcast) { window.dispatchEvent( new CustomEvent(screen-scale-change, { detail: { ...state } }) ) } } function handleResize() { clearTimeout(resizeTimer) resizeTimer setTimeout(computeScale, 200) } onMounted(() { computeScale() window.addEventListener(resize, handleResize) }) onBeforeUnmount(() { clearTimeout(resizeTimer) window.removeEventListener(resize, handleResize) }) /script注意我用的是translate加scale的组合而不是单纯的scale。这样在宽高比不一致时比如 1920×1080 的设计稿放在 1536×864 的视口里比例其实一样但如果放在 1536×700 的视口里就不一样了内容会自动居中视觉上更舒服。4.3 事件坐标换算这个坑必须填transform: scale()最容易出问题的地方是鼠标事件。浏览器的事件对象返回的clientX、clientY是视口坐标而你的元素实际渲染在缩放后的位置。假设缩放比是 0.8你在屏幕上看到元素在 (800, 400) 的位置但点上去clientX是 800而元素在设计稿坐标系里的位置是 800 ÷ 0.8 1000。如果你的大屏里有可拖拽的面板、可点击的热区、自定义的 tooltip 定位不做换算就会点左边触发右边。// 把视口坐标换算成设计稿坐标 function toDesignCoord(event, wrapperEl, scale) { const rect wrapperEl.getBoundingClientRect() return { x: (event.clientX - rect.left) / scale, y: (event.clientY - rect.top) / scale } }这里有个细节getBoundingClientRect()返回的坐标本身已经是缩放后的值所以先减掉容器的左偏移再除以缩放比得到的才是设计稿坐标系里的位置。如果你的容器有translate偏移rect.left会把它算进去逻辑是对的。这一套我在触控一体机上验证过手指拖拽悬浮窗的位置精度完全够用。如果你嫌手动换算麻烦把.screen-content的transform换成zoom: 0.8也能达到类似效果而且事件坐标由浏览器自动处理。代价是zoom在某些 canvas 库下渲染会有点糊而且和position: fixed的元素配合时行为比较怪。两种都试试看你的项目里哪种问题更少。5. 常见问题与排查速查表5.1 高频问题速查表下面这张表是我这几年处理适配问题时积累的遇到问题先对照查一遍能省掉大量调试时间现象大概率原因排查方法解决方向断点样式不生效视口宽度被缩放压缩控制台看clientWidth断点值下调到 1536 或改用区间页面出现横向滚动条子元素固定宽度总和超标用审查元素看谁超宽写min-width: 0或改百分比文字大小不一部分字号被最小字号限制算根字号 × rem 值提高根字号下限或改用 scale图表不随窗口变化未监听 resize手动拖窗口观察加resize监听并防抖拖拽位置偏移transform 缩放未换算坐标打印事件坐标对比除以缩放比反算UI 库浮层错位库内部 px 被误转 rem检查 exclude 配置排除 node_modules图片发虚位图按 CSS 像素拉伸检查图片实际尺寸用 2 倍图或image-set页面整体偏左transform-origin 默认值检查样式改成left top5.2 最小字号限制这是 rem 方案最隐蔽的坑浏览器有一个最小字号设置Chrome 中文环境下默认是 12px。意思是无论你 CSS 写多小渲染出来的文字都不会低于 12px。这个限制在 rem 方案里会引发连锁反应。假设你的设计稿里有一段辅助文字是 12px转换成 rem 是0.12rem。在 1536 视口下根字号是 80px0.12rem等于 9.6px。浏览器把它强制渲染成 12px。结果就是文字比预期大了 25%原来一行能放下的内容换行了容器高度没变文字直接溢出。更糟的是这个问题只在小视口下出现在大视口上完全正常所以测试的时候很容易漏。我的处理办法有两个一是把辅助文字的设计稿字号提高到 14px留出缩放余量二是在项目的全局样式里明确声明/* 明确设置基准字号避免继承到意外值 */ html { font-size: 100px; } body { font-size: 0.14rem; /* 设计稿 14px */ /* 在 1366 视口下根字号约 71px14px 设计稿 ≈ 9.9px仍会被限制 */ }说实话第二种办法治标不治本。如果项目里大量使用 12px 到 14px 的小字我更建议用 transform scale 方案因为它通过视觉缩放实现不触发最小字号限制。这是我在两个大屏项目对比之后得出的结论比任何理论分析都有说服力。5.3 滚动条宽度引起的 resize 死循环这个坑很有意思。你的适配脚本监听resize在回调里改根字号。根字号一改页面内容高度变了竖向滚动条可能出现或消失。滚动条一出现clientWidth减少大约 15 到 17 像素。这个变化又会触发一次resize于是根字号再变内容高度再变滚动条再来一次……看起来就像页面在无限抖动。解决办法是加防抖并且给根字号的变化加一个阈值判断let lastWidth 0 function setRootFontSize() { const clientWidth document.documentElement.clientWidth // 宽度变化小于 2px 直接忽略躲开滚动条抖动 if (Math.abs(clientWidth - lastWidth) 2) return lastWidth clientWidth // 后续缩放逻辑... }防抖时间我一般给 200ms太短了挡不住滚动条抖动太长了用户拖窗口时会看到明显的延迟跳变。6. 把适配做成基础设施检测、监控与回归6.1 用 matchMedia 精确监听缩放变化resize事件只能告诉你尺寸变了但分不清是窗口被拖动还是用户改了系统缩放。想精确监听缩放可以用matchMedia配合dppx单位// 监听设备像素比变化也就是系统缩放变化 function watchDpr(callback) { let mediaQuery null function listen() { const dpr window.devicePixelRatio if (mediaQuery) mediaQuery.removeEventListener(change, onChange) // resolution 查询支持小数如 1.25dppx mediaQuery window.matchMedia((resolution: ${dpr}dppx)) mediaQuery.addEventListener(change, onChange) } function onChange() { callback(window.devicePixelRatio) // 变化后重新绑定新的查询值 listen() } listen() } watchDpr((dpr) { console.log(设备像素比变为, dpr) // 这里可以触发一次根字号重算或图表重绘 })这个技巧在需要区分用户调整了系统缩放和用户拖动了窗口的场景下非常有用。比如你想在缩放变化时重新加载某些高分辨率资源根据 dpr 选 1x 还是 2x 图就可以挂在这上面。6.2 图片和图标的高清资源策略在 125% 缩放下一个设计稿上 24×24 的图标实际会占用 30 个物理像素。如果你只有一张 24×24 的 PNG浏览器放大后边缘会发虚。解决办法有几个层次能用 SVG 就用 SVG这是最省事的矢量图在任何缩放下都清晰。如果必须用位图用image-set()让浏览器按 DPR 自动选图.logo { background-image: image-set( url(./logo.png) 1x, url(./logo2x.png) 2x ); background-size: contain; }如果项目用的是 uni-app 或者老浏览器需要兼容退回到媒体查询也是可以的media (min-resolution: 1.3dppx) { .logo { background-image: url(./logo2x.png); background-size: contain; } }这里注意1.3dppx这个阈值它比 1.25 略大一点是为了覆盖 125% 和 150% 缩放两种情况同时不误伤 100% 缩放的机器。6.3 适配回归测试清单上线前必过一遍适配这种东西改的时候觉得没问题一上线就出各种幺蛾子。我在项目里固定了一份回归清单每次改完布局都要过一遍检查项检查方式通过标准视口 1920缩放 100%直接看无滚动条布局舒展视口 1536缩放 125%改系统缩放后看无横向滚动条无元素重叠视口 1366缩放 100%浏览器窗口拖窄关键功能可操作内容可读视口 1280缩放 100%拖到最窄允许值无内容被裁切浏览器缩放 80% / 125%Ctrl 加号减号布局不崩文字不重叠侧边栏折叠展开手动点击主内容区宽度正确重算图表随窗口变化拖动窗口图表重新渲染坐标正确弹窗和下拉浮层定位逐个打开位置贴合触发元素这份清单里第二项是最容易被忽略的也是这次要解决的核心问题。建议在开发环境里装一个 Chrome 扩展或者直接用 Chrome DevTools 的设备模拟器手动把宽度设成 1536 来长期自测比每次改系统缩放重启浏览器高效得多。6.4 一个实用的调试小工具最后分享一个我自己写的调试浮层开发阶段挂在 App 根组件里一眼就能看到当前的真实视口参数排查适配问题的时候能省很多来回切换的功夫!-- src/components/DebugViewport.vue -- template div v-ifvisible classdebug-viewport span视口{{ width }} × {{ height }}/span spanDPR{{ dpr }}/span span根字号{{ rootFontSize }}px/span button clickvisible false关闭/button /div /template script setup import { ref, onMounted, onBeforeUnmount } from vue const visible ref(import.meta.env.DEV) const width ref(0) const height ref(0) const dpr ref(1) const rootFontSize ref(0) function update() { width.value document.documentElement.clientWidth height.value document.documentElement.clientHeight dpr.value window.devicePixelRatio rootFontSize.value parseFloat( getComputedStyle(document.documentElement).fontSize ).toFixed(1) } onMounted(() { update() window.addEventListener(resize, update) }) onBeforeUnmount(() { window.removeEventListener(resize, update) }) /script style scoped .debug-viewport { position: fixed; right: 12px; bottom: 12px; z-index: 99999; display: flex; gap: 12px; padding: 8px 12px; font-size: 12px; color: #fff; background: rgba(0, 0, 0, 0.75); border-radius: 4px; } /style这个组件的visible默认值是import.meta.env.DEV也就是只在开发环境显示打包上线自动隐藏不需要额外配置。我把这三个数值放在最显眼的位置是因为适配问题十有八九就是这三个值和你预期的不一样先把它们看清楚再动手改代码。我在实际项目里的体会是适配这件事最难的部分从来不是写代码而是搞清楚到底发生了什么。刚开始做适配的时候我也是一看到排版乱了就去调 CSS改了半天发现只在自己电脑上有效换台机器又崩。后来养成习惯先打开控制台确认视口宽度和设备像素比再决定用哪套方案效率提升了不止一个档次。你现在遇到的这个 1920×1080 加 125% 缩放的场景本质就是视口宽度从 1920 变成了 1536把这个数字记牢剩下的都是执行细节。