forked from 77media/video-flow
使用 provider 使用 deviceType
This commit is contained in:
parent
f680e7c002
commit
236051a91c
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
|
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
@ -182,7 +183,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 从 localStorage 初始化配置
|
// 从 localStorage 初始化配置
|
||||||
useEffect(() => {
|
useUpdateEffect(() => {
|
||||||
const savedConfig = localStorage.getItem('videoFlowConfig');
|
const savedConfig = localStorage.getItem('videoFlowConfig');
|
||||||
if (savedConfig) {
|
if (savedConfig) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import AuthGuard from './auth/auth-guard';
|
|||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { registerGlobalMessage } from '@/components/common/GlobalMessage';
|
import { registerGlobalMessage } from '@/components/common/GlobalMessage';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { DeviceTypeProvider } from '@/hooks/useDeviceType';
|
||||||
|
|
||||||
const DevHelper = dynamic(
|
const DevHelper = dynamic(
|
||||||
() => import('@/utils/dev-helper').then(mod => (mod as any).default),
|
() => import('@/utils/dev-helper').then(mod => (mod as any).default),
|
||||||
@ -22,34 +23,36 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ThemeProvider
|
<DeviceTypeProvider>
|
||||||
attribute="class"
|
<ThemeProvider
|
||||||
defaultTheme="dark"
|
attribute="class"
|
||||||
enableSystem
|
defaultTheme="dark"
|
||||||
disableTransitionOnChange
|
enableSystem
|
||||||
>
|
disableTransitionOnChange
|
||||||
<AuthGuard>
|
>
|
||||||
{children}
|
<AuthGuard>
|
||||||
</AuthGuard>
|
{children}
|
||||||
<Toaster
|
</AuthGuard>
|
||||||
position="bottom-left"
|
<Toaster
|
||||||
theme="dark"
|
position="bottom-left"
|
||||||
richColors
|
theme="dark"
|
||||||
toastOptions={{
|
richColors
|
||||||
// 统一配置所有 toast 的样式
|
toastOptions={{
|
||||||
classNames: {
|
// 统一配置所有 toast 的样式
|
||||||
toast: "dark:bg-zinc-900 dark:text-zinc-100 dark:border-zinc-800",
|
classNames: {
|
||||||
title: "dark:text-zinc-100 font-medium",
|
toast: "dark:bg-zinc-900 dark:text-zinc-100 dark:border-zinc-800",
|
||||||
description: "dark:text-zinc-400",
|
title: "dark:text-zinc-100 font-medium",
|
||||||
actionButton: "dark:bg-zinc-700 dark:text-zinc-100",
|
description: "dark:text-zinc-400",
|
||||||
cancelButton: "dark:bg-zinc-600 dark:text-zinc-100",
|
actionButton: "dark:bg-zinc-700 dark:text-zinc-100",
|
||||||
closeButton: "dark:text-zinc-300 hover:text-zinc-100"
|
cancelButton: "dark:bg-zinc-600 dark:text-zinc-100",
|
||||||
},
|
closeButton: "dark:text-zinc-300 hover:text-zinc-100"
|
||||||
duration: 3000,
|
},
|
||||||
}}
|
duration: 3000,
|
||||||
/>
|
}}
|
||||||
{process.env.NODE_ENV === 'development' && <DevHelper />}
|
/>
|
||||||
</ThemeProvider>
|
{process.env.NODE_ENV === 'development' && <DevHelper />}
|
||||||
|
</ThemeProvider>
|
||||||
|
</DeviceTypeProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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
160
hooks/useDeviceType.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user