Main #1
@@ -12,7 +12,11 @@
|
||||
* 采用 zoom 挂在 documentElement 上,可覆盖包括 Element UI 挂载到 body 的
|
||||
* 弹窗/提示/下拉选择在内的所有内容,保证整套界面一致缩放、不塌陷。
|
||||
*
|
||||
* 兼容性:modern Chromium / Edge / Safari 以及 Firefox(≥126) 均支持 zoom。
|
||||
* 兼容性:
|
||||
* - zoom:Chrome/Edge/Safari/Opera 全系、Firefox 126+(2024-05 起)、
|
||||
* 以及国内常见 Chromium 内核浏览器(360/QQ/微信等)均支持;
|
||||
* - 本方案对不支持 zoom 的旧浏览器(如 2024 前的 Firefox)做能力探测,
|
||||
* 自动降级为 transform: scale() 回退,保证不崩塌、可正常使用。
|
||||
*/
|
||||
|
||||
let initialized = false
|
||||
@@ -23,6 +27,18 @@ export const MIN_SCALE = 0.5
|
||||
// 最大缩放倍数:2 对应 4K(3840/1920=2),更高分辨率也封顶在此,避免过度放大
|
||||
export const MAX_SCALE = 2
|
||||
|
||||
/**
|
||||
* 探测当前浏览器是否支持 CSS zoom 属性。
|
||||
* 现代 Chromium/Safari/Firefox(≥126) 返回 true;旧 Firefox(<126) 返回 false。
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isZoomSupported() {
|
||||
const el = document.createElement('div')
|
||||
el.style.zoom = '0.5'
|
||||
// 能读回 "0.5" 说明浏览器真正支持
|
||||
return el.style.zoom === '0.5'
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化全局自适应。可在任意入口调用,重复调用幂等。
|
||||
* @param {number} [designWidth] 设计基准宽度,默认 1920
|
||||
@@ -33,28 +49,83 @@ export function initAdaptive(designWidth = DEFAULT_DESIGN_WIDTH) {
|
||||
}
|
||||
initialized = true
|
||||
|
||||
let timer = null
|
||||
const useZoom = isZoomSupported()
|
||||
|
||||
const applyScale = () => {
|
||||
// 首次:渲染前同步应用一次,避免首帧闪烁
|
||||
apply()
|
||||
window.addEventListener('resize', onResize)
|
||||
// 从后台标签切回来时,若期间窗口尺寸变化(因 hidden 被跳过)需补算一次
|
||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||
|
||||
/**
|
||||
* 真正的缩放计算与写入。
|
||||
* 比例基于 window.innerWidth(窗宽),它不受页面自身 zoom 缩放影响,
|
||||
* 可避免因滚动条宽度变化造成的“缩放-比例”联动回摆。
|
||||
*/
|
||||
function apply() {
|
||||
const root = document.documentElement
|
||||
if (!root) {
|
||||
return
|
||||
}
|
||||
const width = root.clientWidth || window.innerWidth
|
||||
const width = window.innerWidth || root.clientWidth || designWidth
|
||||
const ratio = width / designWidth
|
||||
// 收敛到 [MIN_SCALE, MAX_SCALE],规避极端宽高的异常比例
|
||||
const scale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, ratio))
|
||||
root.style.zoom = scale === 1 ? '' : String(scale)
|
||||
|
||||
if (scale === 1) {
|
||||
clearScale(root)
|
||||
return
|
||||
}
|
||||
|
||||
const onResize = () => {
|
||||
// 简单防抖,避免 resize 高频触发引起抖动
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
if (useZoom) {
|
||||
// 首选:zoom 参与布局计算,视觉与占位一致,最平滑
|
||||
root.style.zoom = String(scale)
|
||||
// 注入缩放与逆缩放变量:逆缩放用于把挂到 body 的 Element 浮层还原为 1:1,
|
||||
// 以抵消 zoom 对 getBoundingClientRect 坐标造成的二次缩放(否则下拉框位置错乱)
|
||||
root.style.setProperty('--adaptive-scale', String(scale))
|
||||
root.style.setProperty('--adaptive-scale-inv', String(1 / scale))
|
||||
} else {
|
||||
// 回退:transform 仅影响视觉;将布局宽度固定为设计宽并从左上角缩放,
|
||||
// 使其铺满视口且不改变内部结构,旧浏览器也能正常使用
|
||||
root.style.transformOrigin = 'top left'
|
||||
root.style.width = `${designWidth}px`
|
||||
root.style.transform = `scale(${scale})`
|
||||
}
|
||||
timer = setTimeout(applyScale, 100)
|
||||
}
|
||||
|
||||
applyScale()
|
||||
window.addEventListener('resize', onResize)
|
||||
function clearScale(root) {
|
||||
if (useZoom) {
|
||||
root.style.zoom = ''
|
||||
root.style.removeProperty('--adaptive-scale')
|
||||
root.style.removeProperty('--adaptive-scale-inv')
|
||||
} else {
|
||||
root.style.transform = ''
|
||||
root.style.transformOrigin = ''
|
||||
root.style.width = ''
|
||||
}
|
||||
}
|
||||
|
||||
// rAF 帧对齐节流:把同一帧内多次 resize 合并为一次应用,实时且顺滑
|
||||
let rafId = null
|
||||
|
||||
function onResize() {
|
||||
if (document.hidden) {
|
||||
return // 后台标签不执行,避免无谓开销
|
||||
}
|
||||
if (rafId) {
|
||||
return // 本帧内已有待执行任务,合并
|
||||
}
|
||||
rafId = window.requestAnimationFrame(() => {
|
||||
rafId = null
|
||||
if (!document.hidden) {
|
||||
apply()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onVisibilityChange() {
|
||||
if (!document.hidden) {
|
||||
apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user