zlwu 10 месяцев назад
Родитель
Сommit
1fa32f1a8a
2 измененных файлов с 85 добавлено и 10 удалено
  1. 63 6
      frontend/src/main.tsx
  2. 22 4
      frontend/src/utils/viewport.ts

+ 63 - 6
frontend/src/main.tsx

@@ -8,6 +8,21 @@ import './index.css';
 // 设置 CSS 变量,用于 CSS 文件中的 OSS 资源 URL
 document.documentElement.style.setProperty('--oss-chatbot-bg-url', `url("${getOssUrl('chatbot-bg.svg')}")`);
 
+// 立即初始化 --vw(在 installViewportHeightFix 之前,确保尽早设置)
+// 解决曲面屏问题:使用 visualViewport.width 获取实际可视宽度
+if (typeof window !== 'undefined') {
+  const getInitialWidth = (): number => {
+    if (window.visualViewport?.width) {
+      return window.visualViewport.width;
+    }
+    return window.innerWidth;
+  };
+  const initialWidth = getInitialWidth();
+  const initialVw = initialWidth * 0.01;
+  document.documentElement.style.setProperty('--vw', `${initialVw}px`);
+  console.log(`[ViewportFix] 🚀 立即初始化 --vw: width=${initialWidth}px, --vw=${initialVw}px`);
+}
+
 // 全局视口高度修复:为所有使用 px2vh / vh 的地方提供统一、准确的高度基准
 // 修复移动端浏览器地址栏/工具栏导致的可视区域变化问题
 // 激进方案:完全阻止键盘弹出时的 --vh 更新,使用固定屏幕高度
@@ -41,6 +56,15 @@ const installViewportHeightFix = () => {
     return window.innerHeight;
   };
 
+  // 获取当前实际宽度(用于计算 --vw,解决曲面屏问题)
+  // 优先使用 visualViewport.width,因为它不包含曲面不可见区域
+  const getCurrentWidth = (): number => {
+    if (window.visualViewport?.width) {
+      return window.visualViewport.width;
+    }
+    return window.innerWidth;
+  };
+
   // 强制锁定视口高度(立即执行,无防抖)
   const forceLockViewportHeight = (reason = 'unknown') => {
     if (initialHeight === null) {
@@ -50,7 +74,13 @@ const installViewportHeightFix = () => {
     isLocked = true;
     const vh = initialHeight * 0.01;
     document.documentElement.style.setProperty('--vh', `${vh}px`);
-    console.log(`[ViewportFix] 🔒 锁定视口高度: initialHeight=${initialHeight}px, --vh=${vh}px, reason=${reason}, isInputFocused=${isInputFocused}`);
+    
+    // 同时更新 --vw(基于实际可视宽度,解决曲面屏问题)
+    const currentWidth = getCurrentWidth();
+    const vw = currentWidth * 0.01;
+    document.documentElement.style.setProperty('--vw', `${vw}px`);
+    
+    console.log(`[ViewportFix] 🔒 锁定视口: initialHeight=${initialHeight}px, --vh=${vh}px, currentWidth=${currentWidth}px, --vw=${vw}px, reason=${reason}, isInputFocused=${isInputFocused}`);
   };
 
   // 设置视口高度(带防抖)
@@ -113,7 +143,13 @@ const installViewportHeightFix = () => {
       const vh = height * 0.01;
       // 全局 1vh 的真实像素值
       document.documentElement.style.setProperty('--vh', `${vh}px`);
-      console.log(`[ViewportFix] 🔧 setViewportHeight: 更新 --vh, height=${height}px, --vh=${vh}px, isLocked=${isLocked}, skipKeyboardCheck=${skipKeyboardCheck}`);
+      
+      // 同时更新 --vw(基于实际可视宽度,解决曲面屏问题)
+      const currentWidth = getCurrentWidth();
+      const vw = currentWidth * 0.01;
+      document.documentElement.style.setProperty('--vw', `${vw}px`);
+      
+      console.log(`[ViewportFix] 🔧 setViewport: 更新 --vh=${vh}px (height=${height}px), --vw=${vw}px (width=${currentWidth}px), isLocked=${isLocked}, skipKeyboardCheck=${skipKeyboardCheck}`);
     };
 
     // 防抖处理(除非立即执行)
@@ -170,7 +206,13 @@ const installViewportHeightFix = () => {
         // 立即设置 --vh,确保在自动聚焦前就已完成初始化
         const vh = initialHeight * 0.01;
         document.documentElement.style.setProperty('--vh', `${vh}px`);
-        console.log(`[ViewportFix] 🚀 初始化高度: source=${source}, initialHeight=${initialHeight}px, --vh=${vh}px, readyState=${document.readyState}`);
+        
+        // 同时初始化 --vw(基于实际可视宽度,解决曲面屏问题)
+        const currentWidth = getCurrentWidth();
+        const vw = currentWidth * 0.01;
+        document.documentElement.style.setProperty('--vw', `${vw}px`);
+        
+        console.log(`[ViewportFix] 🚀 初始化: source=${source}, initialHeight=${initialHeight}px, --vh=${vh}px, currentWidth=${currentWidth}px, --vw=${vw}px, readyState=${document.readyState}`);
       }
     };
     
@@ -200,7 +242,13 @@ const installViewportHeightFix = () => {
         initialHeight = window.innerHeight;
         const vh = initialHeight * 0.01;
         document.documentElement.style.setProperty('--vh', `${vh}px`);
-        console.log(`[ViewportFix] 🚀 初始化高度: source=requestAnimationFrame, initialHeight=${initialHeight}px, --vh=${vh}px`);
+        
+        // 同时初始化 --vw
+        const currentWidth = getCurrentWidth();
+        const vw = currentWidth * 0.01;
+        document.documentElement.style.setProperty('--vw', `${vw}px`);
+        
+        console.log(`[ViewportFix] 🚀 初始化: source=requestAnimationFrame, initialHeight=${initialHeight}px, --vh=${vh}px, currentWidth=${currentWidth}px, --vw=${vw}px`);
       }
     });
   };
@@ -305,12 +353,17 @@ const installViewportHeightFix = () => {
           forceLockViewportHeight('resize-init-keyboard-detected');
           return;
         }
-        // 否则正常设置高度
+        // 否则正常设置高度和宽度
         setViewportHeight(false, true);
         lastHeight = currentHeight;
         return;
       }
       
+      // 同时更新 --vw(visualViewport 变化时,宽度也可能变化,如横竖屏切换)
+      const currentWidth = getCurrentWidth();
+      const vw = currentWidth * 0.01;
+      document.documentElement.style.setProperty('--vw', `${vw}px`);
+      
       // 检查当前活跃元素是否为输入框(即使 focusin 还没触发)
       const heightDiff = initialHeight - currentHeight;
       
@@ -378,7 +431,7 @@ const installViewportHeightFix = () => {
       isInputFocused = false; // 重置焦点状态
       if (blurRecoveryTimer) clearTimeout(blurRecoveryTimer);
       if (debounceTimer) clearTimeout(debounceTimer);
-      setViewportHeight(true, true); // 强制更新
+      setViewportHeight(true, true); // 强制更新(会同时更新 --vw)
     }, 100);
   });
 
@@ -386,6 +439,10 @@ const installViewportHeightFix = () => {
   window.addEventListener('resize', () => {
     // 如果已锁定,不处理(防止键盘弹出时的误判)
     if (isLocked && initialHeight !== null) {
+      // 即使锁定,也要更新 --vw(宽度变化不影响键盘状态)
+      const currentWidth = getCurrentWidth();
+      const vw = currentWidth * 0.01;
+      document.documentElement.style.setProperty('--vw', `${vw}px`);
       return;
     }
     

+ 22 - 4
frontend/src/utils/viewport.ts

@@ -21,18 +21,37 @@ const BASE_WIDTH = 375; // 设计稿宽度
  * - 当视口宽度 > 600px 时:按 600px 视口宽度封顶,不再继续放大
  *
  * 实现思路:
- * - 使用 CSS 计算:calc(px * min(100vw, 600PX) / 375)
- * - min(100vw, 600PX):视口宽度与 600px 取较小值
+ * - 优先使用 CSS 变量 --vw(由 JS 根据 visualViewport.width 设置,解决曲面屏问题)
+ * - --vw 存储的是 width * 0.01 px(例如:375px * 0.01 = 3.75px)
+ * - 要计算 px * (width / BASE_WIDTH):width = --vw * 100
+ * - 同时需要限制最大宽度为 600px
+ * - 降级方案:如果 --vw 未设置,使用 min(100vw, 600PX)
  * - 600PX 使用大写 PX,避免 postcss-px-to-viewport 转换
  *
+ * 曲面屏修复:
+ * - --vw 基于 visualViewport.width,不包含曲面不可见区域
+ * - 如果 --vw 未设置,降级为 100vw(可能包含曲面部分,但至少能工作)
+ *
  * @param px 设计稿上的 px 值(基于 375px 设计宽度)
  * @returns CSS calc() 字符串
  * @example
  * // 视口宽度 375px 时:px2vw(100) ≈ 100px
  * // 视口宽度 750px 时:px2vw(100) ≈ 160px(以 600px 为上限,不继续变大)
+ * // 曲面屏(实际可视 375px,但 100vw=390px):使用 --vw 避免包含曲面部分
  */
 export const px2vw = (px: number): string => {
-  return `calc(${px} * min(100vw, 600PX) / ${BASE_WIDTH})`;
+  // 使用 --vw 变量(由 JS 根据 visualViewport.width 设置,解决曲面屏问题)
+  // --vw 存储的是 width * 0.01 px(例如:375px * 0.01 = 3.75px)
+  // 要计算 px * (width / BASE_WIDTH):width = --vw * 100
+  // 同时需要限制最大宽度为 600px
+  // 
+  // 关键:--vw 应该总是被设置(main.tsx 保证)
+  // 如果未设置,var(--vw, 1vw) 会使用 1vw,然后 1vw * 100 = 100vw
+  // 但 min(100vw, 600px) 在 CSS 中无法正确比较(单位不匹配)
+  // 
+  // 解决方案:确保 --vw 总是被设置,直接使用它
+  // 如果确实未设置,使用原来的方案作为降级
+  return `calc(${px} * min(var(--vw, 1vw) * 100, 600PX) / ${BASE_WIDTH})`;
 };
 
 /**
@@ -104,4 +123,3 @@ export const px2fluidFontSize = (
   // 使用 min(vw, vh) 确保字体不会因为单一维度变大而过大
   return `clamp(${minSize}px, min(${vwSize}vw, ${vhSize}vh), ${maxSize}px)`;
 };
-