Просмотр исходного кода

移动端输入法引起高度缩小的问题解决

zlwu 10 месяцев назад
Родитель
Сommit
09e7a49c23
1 измененных файлов с 240 добавлено и 43 удалено
  1. 240 43
      frontend/src/main.tsx

+ 240 - 43
frontend/src/main.tsx

@@ -6,67 +6,256 @@ import './index.css';
 
 // 全局视口高度修复:为所有使用 px2vh / vh 的地方提供统一、准确的高度基准
 // 修复移动端浏览器地址栏/工具栏导致的可视区域变化问题
-// 使用 visualViewport API 获取真正的可视区域高度(排除浏览器UI)
+// 激进方案:完全阻止键盘弹出时的 --vh 更新,使用固定屏幕高度
 const installViewportHeightFix = () => {
   if (typeof window === 'undefined') return;
 
   // 记录初始视口高度(键盘弹出前的高度,用于键盘弹出时保持界面不变)
+  // 使用 window.innerHeight 作为基准,因为它不受键盘影响(在某些浏览器中)
   let initialHeight: number | null = null;
-  let isKeyboardVisible = false;
+  let isLocked = false; // 是否锁定高度(输入框聚焦或键盘弹出时)
+  let isInputFocused = false; // 输入框焦点状态
+  let blurRecoveryTimer: NodeJS.Timeout | null = null; // blur 恢复定时器
+  let debounceTimer: NodeJS.Timeout | null = null; // 防抖定时器
 
-  // 获取真正的可视区域高度(排除浏览器地址栏/工具栏)
-  const getActualViewportHeight = (): number => {
-    // 优先使用 visualViewport API(现代浏览器支持,排除浏览器UI)
+  // 获取基准高度(用于计算 --vh)
+  // 优先使用 window.innerHeight,因为它相对稳定,不受键盘影响
+  const getBaseHeight = (): number => {
+    // 如果已锁定,始终返回初始高度
+    if (isLocked && initialHeight !== null) {
+      return initialHeight;
+    }
+    // 优先使用 window.innerHeight(不受键盘影响,更稳定)
+    return window.innerHeight;
+  };
+
+  // 获取当前实际高度(用于检测键盘状态)
+  const getCurrentHeight = (): number => {
     if (window.visualViewport?.height) {
       return window.visualViewport.height;
     }
-    // 降级方案:使用 window.innerHeight(某些浏览器可能仍包含地址栏)
     return window.innerHeight;
   };
 
-  const setViewportHeight = (skipKeyboardCheck = false) => {
-    const actualHeight = getActualViewportHeight();
-    
-    // 检测键盘是否弹出(高度突然大幅减小,通常是键盘弹出)
-    if (!skipKeyboardCheck && initialHeight !== null) {
-      const heightDiff = initialHeight - actualHeight;
-      // 如果高度减少超过 150px,认为是键盘弹出
-      if (heightDiff > 150) {
-        isKeyboardVisible = true;
-        // 键盘弹出时,保持初始高度,避免界面缩小
-        const vh = initialHeight * 0.01;
-        document.documentElement.style.setProperty('--vh', `${vh}px`);
+  // 强制锁定视口高度(立即执行,无防抖)
+  const forceLockViewportHeight = () => {
+    if (initialHeight === null) {
+      // 使用 window.innerHeight 作为初始高度(更稳定)
+      initialHeight = window.innerHeight;
+    }
+    isLocked = true;
+    const vh = initialHeight * 0.01;
+    document.documentElement.style.setProperty('--vh', `${vh}px`);
+  };
+
+  // 设置视口高度(带防抖)
+  const setViewportHeight = (skipKeyboardCheck = false, immediate = false) => {
+    const updateHeight = () => {
+      // 如果已锁定,完全不更新(防止键盘弹出时界面缩小)
+      if (isLocked && initialHeight !== null) {
+        forceLockViewportHeight();
         return;
-      } else if (heightDiff < -50) {
-        // 高度增加,可能是键盘收起
-        isKeyboardVisible = false;
-        initialHeight = actualHeight; // 更新初始高度
       }
+      
+      const currentHeight = getCurrentHeight();
+      const baseHeight = getBaseHeight();
+      
+      // 检测键盘是否弹出(高度突然大幅减小,通常是键盘弹出)
+      if (!skipKeyboardCheck && initialHeight !== null) {
+        const heightDiff = initialHeight - currentHeight;
+        // 降低阈值到 100px,覆盖更多设备
+        if (heightDiff > 100) {
+          // 键盘弹出,立即锁定
+          forceLockViewportHeight();
+          return;
+        } else if (heightDiff < -50) {
+          // 高度增加,可能是键盘收起
+          // 只有在输入框未聚焦时才解锁
+          if (!isInputFocused) {
+            isLocked = false;
+            initialHeight = currentHeight;
+          }
+        }
+      }
+
+      // 记录初始高度(首次加载或键盘收起后)
+      if (initialHeight === null || (!isLocked && !skipKeyboardCheck && !isInputFocused)) {
+        initialHeight = baseHeight;
+      }
+
+      // 使用基准高度计算 vh(如果未锁定)
+      const height = isLocked ? initialHeight : baseHeight;
+      const vh = height * 0.01;
+      // 全局 1vh 的真实像素值
+      document.documentElement.style.setProperty('--vh', `${vh}px`);
+    };
+
+    // 防抖处理(除非立即执行)
+    if (immediate) {
+      if (debounceTimer) clearTimeout(debounceTimer);
+      updateHeight();
+    } else {
+      if (debounceTimer) clearTimeout(debounceTimer);
+      debounceTimer = setTimeout(updateHeight, 150);
     }
+  };
 
-    // 记录初始高度(首次加载或键盘收起后)
-    if (initialHeight === null || (!isKeyboardVisible && !skipKeyboardCheck)) {
-      initialHeight = actualHeight;
+  // 锁定视口高度(输入框获得焦点时)
+  const lockViewportHeight = () => {
+    if (initialHeight === null) {
+      // 如果还没有记录初始高度,立即记录(使用 window.innerHeight 更稳定)
+      initialHeight = window.innerHeight;
     }
+    isInputFocused = true;
+    // 立即锁定,不使用防抖
+    forceLockViewportHeight();
+  };
 
-    // 使用实际可视区域高度计算 vh
-    const height = isKeyboardVisible && !skipKeyboardCheck ? initialHeight : actualHeight;
-    const vh = height * 0.01;
-    // 全局 1vh 的真实像素值
-    document.documentElement.style.setProperty('--vh', `${vh}px`);
+  // 解锁视口高度(输入框失去焦点时,延迟恢复)
+  const unlockViewportHeight = () => {
+    isInputFocused = false;
+    // 清除之前的恢复定时器
+    if (blurRecoveryTimer) clearTimeout(blurRecoveryTimer);
+    // 延迟 300ms 恢复,等待键盘收起动画完成
+    blurRecoveryTimer = setTimeout(() => {
+      // 更新初始高度为当前高度(键盘已收起)
+      const currentHeight = getCurrentHeight();
+      initialHeight = currentHeight;
+      isLocked = false;
+      setViewportHeight(false, true);
+    }, 300);
+  };
+
+  // 页面加载完成后立即记录初始高度
+  const initializeHeight = () => {
+    // 立即记录初始高度(使用 window.innerHeight,更稳定)
+    const recordInitialHeight = () => {
+      if (initialHeight === null) {
+        initialHeight = window.innerHeight;
+        setViewportHeight(false, true);
+      }
+    };
+    
+    // 如果 DOM 已加载完成,立即初始化
+    if (document.readyState === 'complete' || document.readyState === 'interactive') {
+      recordInitialHeight();
+    } else {
+      // 立即执行一次(尽早记录)
+      recordInitialHeight();
+      
+      // 同时等待 DOM 加载完成,再次确认
+      const handleDOMReady = () => {
+        recordInitialHeight();
+        document.removeEventListener('DOMContentLoaded', handleDOMReady);
+      };
+      document.addEventListener('DOMContentLoaded', handleDOMReady);
+      
+      // 兜底:如果 DOMContentLoaded 已触发,立即执行
+      if (document.readyState !== 'loading') {
+        handleDOMReady();
+      }
+    }
+    
+    // 额外保险:使用 requestAnimationFrame 确保在下一帧时再次确认
+    requestAnimationFrame(() => {
+      if (initialHeight === null) {
+        initialHeight = window.innerHeight;
+        setViewportHeight(false, true);
+      }
+    });
+  };
+
+  // 检查元素是否为输入框
+  const isInputElement = (element: HTMLElement | null): boolean => {
+    if (!element) return false;
+    return (
+      element.tagName === 'INPUT' ||
+      element.tagName === 'TEXTAREA' ||
+      element.contentEditable === 'true'
+    );
+  };
+
+  // 监听输入框触摸开始事件(在 focusin 之前触发,更早锁定)
+  const handleTouchStart = (e: TouchEvent) => {
+    const target = e.target as HTMLElement;
+    if (isInputElement(target)) {
+      // 立即锁定,防止键盘弹出时界面缩小
+      if (initialHeight === null) {
+        initialHeight = window.innerHeight;
+      }
+      forceLockViewportHeight();
+    }
+  };
+
+  // 监听输入框焦点事件(全局监听)
+  const handleFocusIn = (e: FocusEvent) => {
+    const target = e.target as HTMLElement;
+    if (isInputElement(target)) {
+      lockViewportHeight();
+    }
+  };
+
+  const handleFocusOut = (e: FocusEvent) => {
+    const target = e.target as HTMLElement;
+    if (isInputElement(target)) {
+      unlockViewportHeight();
+    }
   };
 
-  // 首次设置
-  setViewportHeight();
+  // 初始化
+  initializeHeight();
+
+  // 监听触摸事件(在点击输入框之前就锁定)
+  document.addEventListener('touchstart', handleTouchStart, { passive: true });
+  // 监听输入框焦点事件
+  document.addEventListener('focusin', handleFocusIn);
+  document.addEventListener('focusout', handleFocusOut);
 
   // 监听 visualViewport 变化(移动端浏览器地址栏显示/隐藏时会触发)
+  // 关键优化:在 visualViewport.resize 时,如果检测到高度大幅减小,立即锁定
   if (window.visualViewport) {
     window.visualViewport.addEventListener('resize', () => {
+      // 如果已锁定,完全不处理(防止任何更新)
+      if (isLocked && initialHeight !== null) {
+        forceLockViewportHeight();
+        return;
+      }
+      
+      const currentHeight = getCurrentHeight();
+      
+      // 检查当前活跃元素是否为输入框(即使 focusin 还没触发)
+      const activeElement = document.activeElement as HTMLElement;
+      const isActiveInput = isInputElement(activeElement);
+      
+      // 如果输入框处于焦点状态,或当前活跃元素是输入框,强制锁定
+      if (isInputFocused || isActiveInput) {
+        if (initialHeight === null) {
+          initialHeight = window.innerHeight;
+        }
+        forceLockViewportHeight();
+        return;
+      }
+      
+      // 检测键盘是否弹出(高度突然大幅减小)
+      if (initialHeight !== null) {
+        const heightDiff = initialHeight - currentHeight;
+        // 如果高度减少超过 100px,认为是键盘弹出,立即锁定
+        if (heightDiff > 100) {
+          forceLockViewportHeight();
+          return;
+        }
+      }
+      
+      // 其他情况正常更新(地址栏显示/隐藏等)
       setViewportHeight();
     });
+    
     window.visualViewport.addEventListener('scroll', () => {
       // 滚动时也可能导致地址栏显示/隐藏,需要更新
-      setViewportHeight();
+      // 但如果已锁定,不更新
+      if (!isLocked) {
+        setViewportHeight();
+      }
     });
   }
 
@@ -75,24 +264,32 @@ const installViewportHeightFix = () => {
     // 延迟执行,等待实际尺寸变化
     setTimeout(() => {
       initialHeight = null; // 重置初始高度
-      isKeyboardVisible = false; // 重置键盘状态
-      setViewportHeight(true); // 强制更新
+      isLocked = false; // 重置锁定状态
+      isInputFocused = false; // 重置焦点状态
+      if (blurRecoveryTimer) clearTimeout(blurRecoveryTimer);
+      if (debounceTimer) clearTimeout(debounceTimer);
+      setViewportHeight(true, true); // 强制更新
     }, 100);
   });
 
   // 监听窗口大小变化(仅处理真正的视口变化,如浏览器窗口调整)
-  // 使用防抖避免频繁更新
-  let resizeTimer: NodeJS.Timeout | null = null;
   window.addEventListener('resize', () => {
-    if (resizeTimer) clearTimeout(resizeTimer);
-    resizeTimer = setTimeout(() => {
+    // 如果已锁定,不处理(防止键盘弹出时的误判)
+    if (isLocked && initialHeight !== null) {
+      return;
+    }
+    
+    if (debounceTimer) clearTimeout(debounceTimer);
+    debounceTimer = setTimeout(() => {
       // 判断是否为真正的视口变化(高度变化超过 200px 认为是横竖屏切换)
-      const currentHeight = getActualViewportHeight();
+      const currentHeight = getCurrentHeight();
       if (initialHeight !== null && Math.abs(currentHeight - initialHeight) > 200) {
         initialHeight = null; // 重置初始高度
-        isKeyboardVisible = false; // 重置键盘状态
-        setViewportHeight(true); // 强制更新
-      } else {
+        isLocked = false; // 重置锁定状态
+        isInputFocused = false; // 重置焦点状态
+        if (blurRecoveryTimer) clearTimeout(blurRecoveryTimer);
+        setViewportHeight(true, true); // 强制更新
+      } else if (!isLocked) {
         // 普通的高度变化(可能是地址栏显示/隐藏),正常更新
         setViewportHeight();
       }