forked from 77media/video-flow
161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
'use client';
|
|
|
|
import { createContext, useContext, useState, useEffect, useMemo, ReactNode } from 'react';
|
|
|
|
/** 设备类型枚举 */
|
|
export enum DeviceType {
|
|
MOBILE = 'mobile',
|
|
TABLET = 'tablet',
|
|
DESKTOP = 'desktop'
|
|
}
|
|
|
|
/** 屏幕断点配置 */
|
|
const BREAKPOINTS = {
|
|
MOBILE: 480,
|
|
TABLET: 1024,
|
|
DESKTOP: 1025
|
|
};
|
|
|
|
/** 设备类型上下文数据接口 */
|
|
interface DeviceTypeContextValue {
|
|
deviceType: DeviceType;
|
|
windowSize: {
|
|
width: number;
|
|
height: number;
|
|
};
|
|
isMobile: boolean;
|
|
isTablet: boolean;
|
|
isDesktop: boolean;
|
|
/** 是否为移动端设备(包括平板) */
|
|
isMobileDevice: boolean;
|
|
}
|
|
|
|
const DeviceTypeContext = createContext<DeviceTypeContextValue | null>(null);
|
|
|
|
/**
|
|
* Device type provider component
|
|
* Centralized device type detection and window size management
|
|
* @param {ReactNode} children - Child components
|
|
*/
|
|
export function DeviceTypeProvider({ children }: { children: ReactNode }) {
|
|
|
|
const [deviceType, setDeviceType] = useState<DeviceType>(() => {
|
|
if (typeof window === 'undefined') return DeviceType.DESKTOP;
|
|
const width = window.innerWidth;
|
|
if (width <= BREAKPOINTS.MOBILE) return DeviceType.MOBILE;
|
|
if (width <= BREAKPOINTS.TABLET) return DeviceType.TABLET;
|
|
return DeviceType.DESKTOP;
|
|
});
|
|
|
|
const [windowSize, setWindowSize] = useState({
|
|
width: typeof window !== 'undefined' ? window.innerWidth : 0,
|
|
height: typeof window !== 'undefined' ? window.innerHeight : 0
|
|
});
|
|
|
|
useEffect(() => {
|
|
|
|
const getDeviceType = (width: number): DeviceType => {
|
|
if (width <= BREAKPOINTS.MOBILE) return DeviceType.MOBILE;
|
|
if (width <= BREAKPOINTS.TABLET) return DeviceType.TABLET;
|
|
return DeviceType.DESKTOP;
|
|
};
|
|
|
|
let timeoutId: NodeJS.Timeout | null = null;
|
|
let isInitialized = false;
|
|
|
|
const handleResize = () => {
|
|
const width = window.innerWidth;
|
|
const height = window.innerHeight;
|
|
const newDeviceType = getDeviceType(width);
|
|
|
|
|
|
// Only update if values actually changed
|
|
setWindowSize(prev => {
|
|
const changed = prev.width !== width || prev.height !== height;
|
|
if (changed) {
|
|
return { width, height };
|
|
}
|
|
return prev;
|
|
});
|
|
|
|
setDeviceType(prev => {
|
|
const changed = prev !== newDeviceType;
|
|
if (changed) {
|
|
return newDeviceType;
|
|
}
|
|
return prev;
|
|
});
|
|
|
|
// 移动端动态视口高度处理
|
|
if (width <= BREAKPOINTS.TABLET) {
|
|
const vh = height * 0.01;
|
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
|
}
|
|
|
|
if (!isInitialized) {
|
|
isInitialized = true;
|
|
}
|
|
};
|
|
|
|
const debouncedHandleResize = () => {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
timeoutId = setTimeout(handleResize, 100);
|
|
};
|
|
|
|
// 初始化 - 因为 state 已经在初始化时设置了正确的值,这里只需要设置 vh 变量
|
|
const width = window.innerWidth;
|
|
const height = window.innerHeight;
|
|
if (width <= BREAKPOINTS.TABLET) {
|
|
const vh = height * 0.01;
|
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
|
}
|
|
isInitialized = true;
|
|
|
|
// 添加事件监听
|
|
window.addEventListener('resize', debouncedHandleResize);
|
|
window.addEventListener('orientationchange', () => {
|
|
// 延迟处理以确保获取正确的视口尺寸
|
|
setTimeout(handleResize, 100);
|
|
});
|
|
|
|
return () => {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
window.removeEventListener('resize', debouncedHandleResize);
|
|
window.removeEventListener('orientationchange', handleResize);
|
|
};
|
|
}, []);
|
|
|
|
const value = useMemo<DeviceTypeContextValue>(() => ({
|
|
deviceType,
|
|
windowSize,
|
|
isMobile: deviceType === DeviceType.MOBILE,
|
|
isTablet: deviceType === DeviceType.TABLET,
|
|
isDesktop: deviceType === DeviceType.DESKTOP,
|
|
isMobileDevice: deviceType === DeviceType.MOBILE || deviceType === DeviceType.TABLET
|
|
}), [deviceType, windowSize]);
|
|
|
|
return (
|
|
<DeviceTypeContext.Provider value={value}>
|
|
{children}
|
|
</DeviceTypeContext.Provider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Hook to access device type context
|
|
* @returns {DeviceTypeContextValue} Device type information
|
|
* @throws {Error} If used outside DeviceTypeProvider
|
|
*/
|
|
export function useDeviceType(): DeviceTypeContextValue {
|
|
const context = useContext(DeviceTypeContext);
|
|
if (!context) {
|
|
throw new Error('useDeviceType must be used within DeviceTypeProvider');
|
|
}
|
|
return context;
|
|
}
|
|
|