|
@@ -6,67 +6,256 @@ import './index.css';
|
|
|
|
|
|
|
|
// 全局视口高度修复:为所有使用 px2vh / vh 的地方提供统一、准确的高度基准
|
|
// 全局视口高度修复:为所有使用 px2vh / vh 的地方提供统一、准确的高度基准
|
|
|
// 修复移动端浏览器地址栏/工具栏导致的可视区域变化问题
|
|
// 修复移动端浏览器地址栏/工具栏导致的可视区域变化问题
|
|
|
-// 使用 visualViewport API 获取真正的可视区域高度(排除浏览器UI)
|
|
|
|
|
|
|
+// 激进方案:完全阻止键盘弹出时的 --vh 更新,使用固定屏幕高度
|
|
|
const installViewportHeightFix = () => {
|
|
const installViewportHeightFix = () => {
|
|
|
if (typeof window === 'undefined') return;
|
|
if (typeof window === 'undefined') return;
|
|
|
|
|
|
|
|
// 记录初始视口高度(键盘弹出前的高度,用于键盘弹出时保持界面不变)
|
|
// 记录初始视口高度(键盘弹出前的高度,用于键盘弹出时保持界面不变)
|
|
|
|
|
+ // 使用 window.innerHeight 作为基准,因为它不受键盘影响(在某些浏览器中)
|
|
|
let initialHeight: number | null = null;
|
|
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) {
|
|
if (window.visualViewport?.height) {
|
|
|
return window.visualViewport.height;
|
|
return window.visualViewport.height;
|
|
|
}
|
|
}
|
|
|
- // 降级方案:使用 window.innerHeight(某些浏览器可能仍包含地址栏)
|
|
|
|
|
return 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;
|
|
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 变化(移动端浏览器地址栏显示/隐藏时会触发)
|
|
|
|
|
+ // 关键优化:在 visualViewport.resize 时,如果检测到高度大幅减小,立即锁定
|
|
|
if (window.visualViewport) {
|
|
if (window.visualViewport) {
|
|
|
window.visualViewport.addEventListener('resize', () => {
|
|
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();
|
|
setViewportHeight();
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
window.visualViewport.addEventListener('scroll', () => {
|
|
window.visualViewport.addEventListener('scroll', () => {
|
|
|
// 滚动时也可能导致地址栏显示/隐藏,需要更新
|
|
// 滚动时也可能导致地址栏显示/隐藏,需要更新
|
|
|
- setViewportHeight();
|
|
|
|
|
|
|
+ // 但如果已锁定,不更新
|
|
|
|
|
+ if (!isLocked) {
|
|
|
|
|
+ setViewportHeight();
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -75,24 +264,32 @@ const installViewportHeightFix = () => {
|
|
|
// 延迟执行,等待实际尺寸变化
|
|
// 延迟执行,等待实际尺寸变化
|
|
|
setTimeout(() => {
|
|
setTimeout(() => {
|
|
|
initialHeight = null; // 重置初始高度
|
|
initialHeight = null; // 重置初始高度
|
|
|
- isKeyboardVisible = false; // 重置键盘状态
|
|
|
|
|
- setViewportHeight(true); // 强制更新
|
|
|
|
|
|
|
+ isLocked = false; // 重置锁定状态
|
|
|
|
|
+ isInputFocused = false; // 重置焦点状态
|
|
|
|
|
+ if (blurRecoveryTimer) clearTimeout(blurRecoveryTimer);
|
|
|
|
|
+ if (debounceTimer) clearTimeout(debounceTimer);
|
|
|
|
|
+ setViewportHeight(true, true); // 强制更新
|
|
|
}, 100);
|
|
}, 100);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 监听窗口大小变化(仅处理真正的视口变化,如浏览器窗口调整)
|
|
// 监听窗口大小变化(仅处理真正的视口变化,如浏览器窗口调整)
|
|
|
- // 使用防抖避免频繁更新
|
|
|
|
|
- let resizeTimer: NodeJS.Timeout | null = null;
|
|
|
|
|
window.addEventListener('resize', () => {
|
|
window.addEventListener('resize', () => {
|
|
|
- if (resizeTimer) clearTimeout(resizeTimer);
|
|
|
|
|
- resizeTimer = setTimeout(() => {
|
|
|
|
|
|
|
+ // 如果已锁定,不处理(防止键盘弹出时的误判)
|
|
|
|
|
+ if (isLocked && initialHeight !== null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (debounceTimer) clearTimeout(debounceTimer);
|
|
|
|
|
+ debounceTimer = setTimeout(() => {
|
|
|
// 判断是否为真正的视口变化(高度变化超过 200px 认为是横竖屏切换)
|
|
// 判断是否为真正的视口变化(高度变化超过 200px 认为是横竖屏切换)
|
|
|
- const currentHeight = getActualViewportHeight();
|
|
|
|
|
|
|
+ const currentHeight = getCurrentHeight();
|
|
|
if (initialHeight !== null && Math.abs(currentHeight - initialHeight) > 200) {
|
|
if (initialHeight !== null && Math.abs(currentHeight - initialHeight) > 200) {
|
|
|
initialHeight = null; // 重置初始高度
|
|
initialHeight = null; // 重置初始高度
|
|
|
- isKeyboardVisible = false; // 重置键盘状态
|
|
|
|
|
- setViewportHeight(true); // 强制更新
|
|
|
|
|
- } else {
|
|
|
|
|
|
|
+ isLocked = false; // 重置锁定状态
|
|
|
|
|
+ isInputFocused = false; // 重置焦点状态
|
|
|
|
|
+ if (blurRecoveryTimer) clearTimeout(blurRecoveryTimer);
|
|
|
|
|
+ setViewportHeight(true, true); // 强制更新
|
|
|
|
|
+ } else if (!isLocked) {
|
|
|
// 普通的高度变化(可能是地址栏显示/隐藏),正常更新
|
|
// 普通的高度变化(可能是地址栏显示/隐藏),正常更新
|
|
|
setViewportHeight();
|
|
setViewportHeight();
|
|
|
}
|
|
}
|