使用 provider 使用 deviceType

This commit is contained in:
北枳 2025-10-17 19:56:37 +08:00
parent f680e7c002
commit 236051a91c
4 changed files with 193 additions and 99 deletions

View File

@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useRef, useCallback } from "react";
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
import {
ChevronDown,
ChevronUp,
@ -182,7 +183,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
});
// 从 localStorage 初始化配置
useEffect(() => {
useUpdateEffect(() => {
const savedConfig = localStorage.getItem('videoFlowConfig');
if (savedConfig) {
try {

View File

@ -8,6 +8,7 @@ import AuthGuard from './auth/auth-guard';
import dynamic from 'next/dynamic';
import { registerGlobalMessage } from '@/components/common/GlobalMessage';
import { useEffect } from 'react';
import { DeviceTypeProvider } from '@/hooks/useDeviceType';
const DevHelper = dynamic(
() => import('@/utils/dev-helper').then(mod => (mod as any).default),
@ -22,6 +23,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
return (
<Provider store={store}>
<DeviceTypeProvider>
<ThemeProvider
attribute="class"
defaultTheme="dark"
@ -50,6 +52,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
/>
{process.env.NODE_ENV === 'development' && <DevHelper />}
</ThemeProvider>
</DeviceTypeProvider>
</Provider>
);
}

View File

@ -1,70 +0,0 @@
import { useState, useEffect } from 'react';
// 定义设备类型枚举
export enum DeviceType {
MOBILE = 'mobile',
TABLET = 'tablet',
DESKTOP = 'desktop'
}
// 定义屏幕断点
const BREAKPOINTS = {
MOBILE: 480,
TABLET: 1024,
DESKTOP: 1025
};
export function useDeviceType() {
const [deviceType, setDeviceType] = useState<DeviceType>(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;
};
const handleResize = () => {
const width = window.innerWidth;
const height = window.innerHeight;
setWindowSize({ width, height });
setDeviceType(getDeviceType(width));
// 移动端动态视口高度处理
if (width <= BREAKPOINTS.TABLET) {
const vh = height * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
};
// 初始化
handleResize();
// 添加事件监听
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', () => {
// 延迟处理以确保获取正确的视口尺寸
setTimeout(handleResize, 100);
});
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleResize);
};
}, []);
return {
deviceType,
windowSize,
isMobile: deviceType === DeviceType.MOBILE,
isTablet: deviceType === DeviceType.TABLET,
isDesktop: deviceType === DeviceType.DESKTOP,
/** 是否为移动端设备(包括平板) */
isMobileDevice: deviceType === DeviceType.MOBILE || deviceType === DeviceType.TABLET
};
}

160
hooks/useDeviceType.tsx Normal file
View File

@ -0,0 +1,160 @@
'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;
}