rem.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. /**
  2. * rem 适配工具 - 全终端适配方案(防御式设计)
  3. * 设计稿基准: 393px(与 postcss / px2vw 一致)
  4. * 1rem = 100px (便于计算,设计稿 100px = 1rem)
  5. *
  6. * 适配范围: 280px - 768px (支持手机到平板)
  7. * - 280px 屏幕: 根字体 75px (iPhone SE 竖屏)
  8. * - 320px 屏幕: 根字体 85px (小屏手机)
  9. * - 393px 屏幕: 根字体约 100px (标准手机)
  10. * - 430px 屏幕: 根字体 114px (大屏手机)
  11. * - 768px 屏幕: 根字体 100px (平板,固定标准大小)
  12. * - 1024px+ 屏幕: 根字体 100px (桌面,居中显示)
  13. *
  14. * 兼容性: iOS 8+, Android 4.4+, 所有现代浏览器
  15. */
  16. const BASE_WIDTH = 393; // 设计稿宽度
  17. const BASE_FONT_SIZE = 100; // 1rem = 100px
  18. const MIN_WIDTH = 280; // 最小宽度(支持极小屏幕)
  19. const MOBILE_MAX_WIDTH = 430; // 移动端最大宽度(超过则按平板处理)
  20. const TABLET_WIDTH = 768; // 平板宽度(超过则固定大小)
  21. const MIN_FONT_SIZE = 75; // 最小根字体(防止元素过小不可用)
  22. const TABLET_FONT_SIZE = 100; // 平板/桌面固定字体(保持设计稿标准大小)
  23. /**
  24. * 检测设备类型
  25. */
  26. const getDeviceType = (width: number): 'mobile' | 'tablet' | 'desktop' => {
  27. if (width < MOBILE_MAX_WIDTH) return 'mobile';
  28. if (width < TABLET_WIDTH) return 'tablet';
  29. return 'desktop';
  30. };
  31. /**
  32. * 设置根元素字体大小(三层防护策略)
  33. */
  34. const setRootFontSize = () => {
  35. const screenWidth = window.innerWidth;
  36. const screenHeight = window.innerHeight;
  37. const deviceType = getDeviceType(screenWidth);
  38. let fontSize: number;
  39. // 第一层:移动端(280px - 430px)- 动态缩放
  40. if (deviceType === 'mobile') {
  41. const clampedWidth = Math.min(Math.max(screenWidth, MIN_WIDTH), MOBILE_MAX_WIDTH);
  42. fontSize = (clampedWidth / BASE_WIDTH) * BASE_FONT_SIZE;
  43. fontSize = Math.max(fontSize, MIN_FONT_SIZE); // 防止过小
  44. }
  45. // 第二层:平板(430px - 768px)- 渐进式缩放
  46. else if (deviceType === 'tablet') {
  47. // 平板采用渐进式缩放,从 114px 到 100px
  48. const progress = (screenWidth - MOBILE_MAX_WIDTH) / (TABLET_WIDTH - MOBILE_MAX_WIDTH);
  49. const startSize = (MOBILE_MAX_WIDTH / BASE_WIDTH) * BASE_FONT_SIZE; // 114px
  50. fontSize = startSize - (progress * (startSize - TABLET_FONT_SIZE));
  51. }
  52. // 第三层:桌面(768px+)- 固定大小 + 居中
  53. else {
  54. fontSize = TABLET_FONT_SIZE; // 固定 100px,内容居中显示
  55. }
  56. // 设置 html 根字体大小
  57. document.documentElement.style.fontSize = `${fontSize}px`;
  58. // 设置设备类型标识(用于 CSS 媒体查询增强)
  59. document.documentElement.setAttribute('data-device-type', deviceType);
  60. // 设置屏幕方向标识
  61. const orientation = screenWidth > screenHeight ? 'landscape' : 'portrait';
  62. document.documentElement.setAttribute('data-orientation', orientation);
  63. // 极小屏幕检测:设置滚动条降级标记
  64. const isTooSmall = screenWidth < MIN_WIDTH || screenHeight < 500;
  65. document.documentElement.setAttribute('data-too-small', String(isTooSmall));
  66. document.body.setAttribute('data-too-small', String(isTooSmall));
  67. // rem 适配完成
  68. };
  69. /**
  70. * 防抖函数(优化 resize 性能)
  71. */
  72. const debounce = (fn: () => void, delay: number) => {
  73. let timer: number | null = null;
  74. return () => {
  75. if (timer) clearTimeout(timer);
  76. timer = window.setTimeout(fn, delay);
  77. };
  78. };
  79. /**
  80. * 初始化 rem 适配(全终端支持)
  81. */
  82. export const initRem = () => {
  83. // 立即设置一次
  84. setRootFontSize();
  85. // 监听窗口变化(防抖优化性能)
  86. const debouncedResize = debounce(setRootFontSize, 100);
  87. window.addEventListener('resize', debouncedResize);
  88. // 监听横竖屏切换(立即响应)
  89. window.addEventListener('orientationchange', () => {
  90. // orientationchange 后需要延迟执行,等待实际尺寸变化
  91. setTimeout(setRootFontSize, 100);
  92. });
  93. // 监听 DOMContentLoaded(确保 DOM 加载完成)
  94. if (document.readyState === 'loading') {
  95. document.addEventListener('DOMContentLoaded', setRootFontSize);
  96. }
  97. // 监听页面可见性变化(切换标签页后重新计算)
  98. document.addEventListener('visibilitychange', () => {
  99. if (!document.hidden) {
  100. setRootFontSize();
  101. }
  102. });
  103. // iOS Safari 特殊处理:监听滚动条显示/隐藏导致的宽度变化
  104. if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
  105. let lastWidth = window.innerWidth;
  106. window.addEventListener('scroll', debounce(() => {
  107. const currentWidth = window.innerWidth;
  108. if (Math.abs(currentWidth - lastWidth) > 5) { // 宽度变化超过 5px 才重新计算
  109. lastWidth = currentWidth;
  110. setRootFontSize();
  111. }
  112. }, 200));
  113. }
  114. // 微信浏览器特殊处理:监听软键盘弹出/收起
  115. if (/MicroMessenger/.test(navigator.userAgent)) {
  116. let lastHeight = window.innerHeight;
  117. window.addEventListener('resize', debounce(() => {
  118. const currentHeight = window.innerHeight;
  119. // 高度变化超过 100px 认为是键盘弹出/收起
  120. if (Math.abs(currentHeight - lastHeight) > 100) {
  121. lastHeight = currentHeight;
  122. setRootFontSize();
  123. }
  124. }, 100));
  125. }
  126. // rem 适配已启用
  127. };
  128. /**
  129. * px 转 rem 辅助函数(手动计算时使用)
  130. * @param px 设计稿上的 px 值
  131. * @returns rem 字符串
  132. * @example px2rem(100) => '1rem'
  133. */
  134. export const px2rem = (px: number): string => {
  135. return `${px / BASE_FONT_SIZE}rem`;
  136. };
  137. /**
  138. * rem 转 px 辅助函数(用于 JS 计算)
  139. * @param rem rem 值
  140. * @returns 实际像素值
  141. */
  142. export const rem2px = (rem: number): number => {
  143. const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
  144. return rem * fontSize;
  145. };