/** * rem 适配工具 - 全终端适配方案(防御式设计) * 设计稿基准: 393px(与 postcss / px2vw 一致) * 1rem = 100px (便于计算,设计稿 100px = 1rem) * * 适配范围: 280px - 768px (支持手机到平板) * - 280px 屏幕: 根字体 75px (iPhone SE 竖屏) * - 320px 屏幕: 根字体 85px (小屏手机) * - 393px 屏幕: 根字体约 100px (标准手机) * - 430px 屏幕: 根字体 114px (大屏手机) * - 768px 屏幕: 根字体 100px (平板,固定标准大小) * - 1024px+ 屏幕: 根字体 100px (桌面,居中显示) * * 兼容性: iOS 8+, Android 4.4+, 所有现代浏览器 */ const BASE_WIDTH = 393; // 设计稿宽度 const BASE_FONT_SIZE = 100; // 1rem = 100px const MIN_WIDTH = 280; // 最小宽度(支持极小屏幕) const MOBILE_MAX_WIDTH = 430; // 移动端最大宽度(超过则按平板处理) const TABLET_WIDTH = 768; // 平板宽度(超过则固定大小) const MIN_FONT_SIZE = 75; // 最小根字体(防止元素过小不可用) const TABLET_FONT_SIZE = 100; // 平板/桌面固定字体(保持设计稿标准大小) /** * 检测设备类型 */ const getDeviceType = (width: number): 'mobile' | 'tablet' | 'desktop' => { if (width < MOBILE_MAX_WIDTH) return 'mobile'; if (width < TABLET_WIDTH) return 'tablet'; return 'desktop'; }; /** * 设置根元素字体大小(三层防护策略) */ const setRootFontSize = () => { const screenWidth = window.innerWidth; const screenHeight = window.innerHeight; const deviceType = getDeviceType(screenWidth); let fontSize: number; // 第一层:移动端(280px - 430px)- 动态缩放 if (deviceType === 'mobile') { const clampedWidth = Math.min(Math.max(screenWidth, MIN_WIDTH), MOBILE_MAX_WIDTH); fontSize = (clampedWidth / BASE_WIDTH) * BASE_FONT_SIZE; fontSize = Math.max(fontSize, MIN_FONT_SIZE); // 防止过小 } // 第二层:平板(430px - 768px)- 渐进式缩放 else if (deviceType === 'tablet') { // 平板采用渐进式缩放,从 114px 到 100px const progress = (screenWidth - MOBILE_MAX_WIDTH) / (TABLET_WIDTH - MOBILE_MAX_WIDTH); const startSize = (MOBILE_MAX_WIDTH / BASE_WIDTH) * BASE_FONT_SIZE; // 114px fontSize = startSize - (progress * (startSize - TABLET_FONT_SIZE)); } // 第三层:桌面(768px+)- 固定大小 + 居中 else { fontSize = TABLET_FONT_SIZE; // 固定 100px,内容居中显示 } // 设置 html 根字体大小 document.documentElement.style.fontSize = `${fontSize}px`; // 设置设备类型标识(用于 CSS 媒体查询增强) document.documentElement.setAttribute('data-device-type', deviceType); // 设置屏幕方向标识 const orientation = screenWidth > screenHeight ? 'landscape' : 'portrait'; document.documentElement.setAttribute('data-orientation', orientation); // 极小屏幕检测:设置滚动条降级标记 const isTooSmall = screenWidth < MIN_WIDTH || screenHeight < 500; document.documentElement.setAttribute('data-too-small', String(isTooSmall)); document.body.setAttribute('data-too-small', String(isTooSmall)); // rem 适配完成 }; /** * 防抖函数(优化 resize 性能) */ const debounce = (fn: () => void, delay: number) => { let timer: number | null = null; return () => { if (timer) clearTimeout(timer); timer = window.setTimeout(fn, delay); }; }; /** * 初始化 rem 适配(全终端支持) */ export const initRem = () => { // 立即设置一次 setRootFontSize(); // 监听窗口变化(防抖优化性能) const debouncedResize = debounce(setRootFontSize, 100); window.addEventListener('resize', debouncedResize); // 监听横竖屏切换(立即响应) window.addEventListener('orientationchange', () => { // orientationchange 后需要延迟执行,等待实际尺寸变化 setTimeout(setRootFontSize, 100); }); // 监听 DOMContentLoaded(确保 DOM 加载完成) if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setRootFontSize); } // 监听页面可见性变化(切换标签页后重新计算) document.addEventListener('visibilitychange', () => { if (!document.hidden) { setRootFontSize(); } }); // iOS Safari 特殊处理:监听滚动条显示/隐藏导致的宽度变化 if (/iPhone|iPad|iPod/.test(navigator.userAgent)) { let lastWidth = window.innerWidth; window.addEventListener('scroll', debounce(() => { const currentWidth = window.innerWidth; if (Math.abs(currentWidth - lastWidth) > 5) { // 宽度变化超过 5px 才重新计算 lastWidth = currentWidth; setRootFontSize(); } }, 200)); } // 微信浏览器特殊处理:监听软键盘弹出/收起 if (/MicroMessenger/.test(navigator.userAgent)) { let lastHeight = window.innerHeight; window.addEventListener('resize', debounce(() => { const currentHeight = window.innerHeight; // 高度变化超过 100px 认为是键盘弹出/收起 if (Math.abs(currentHeight - lastHeight) > 100) { lastHeight = currentHeight; setRootFontSize(); } }, 100)); } // rem 适配已启用 }; /** * px 转 rem 辅助函数(手动计算时使用) * @param px 设计稿上的 px 值 * @returns rem 字符串 * @example px2rem(100) => '1rem' */ export const px2rem = (px: number): string => { return `${px / BASE_FONT_SIZE}rem`; }; /** * rem 转 px 辅助函数(用于 JS 计算) * @param rem rem 值 * @returns 实际像素值 */ export const rem2px = (rem: number): number => { const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize); return rem * fontSize; };