feat: 增加打电话界面
This commit is contained in:
parent
859ef2d320
commit
debca09f6c
|
|
@ -17,7 +17,6 @@
|
|||
"axios": "^1.12.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"jotai": "^2.15.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "15.5.4",
|
||||
|
|
@ -27,7 +26,8 @@
|
|||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-virtuoso": "^4.14.1",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
|
|
|||
|
|
@ -26,9 +26,6 @@ importers:
|
|||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
jotai:
|
||||
specifier: ^2.15.0
|
||||
version: 2.15.0(@types/react@19.2.2)(react@19.1.0)
|
||||
js-cookie:
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
|
|
@ -59,6 +56,9 @@ importers:
|
|||
tailwind-merge:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
zustand:
|
||||
specifier: ^5.0.8
|
||||
version: 5.0.8(@types/react@19.2.2)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0))
|
||||
devDependencies:
|
||||
'@eslint/eslintrc':
|
||||
specifier: ^3
|
||||
|
|
@ -2110,24 +2110,6 @@ packages:
|
|||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
|
||||
jotai@2.15.0:
|
||||
resolution: {integrity: sha512-nbp/6jN2Ftxgw0VwoVnOg0m5qYM1rVcfvij+MZx99Z5IK13eGve9FJoCwGv+17JvVthTjhSmNtT5e1coJnr6aw==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': '>=7.0.0'
|
||||
'@babel/template': '>=7.0.0'
|
||||
'@types/react': '>=17.0.0'
|
||||
react: '>=17.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
'@babel/template':
|
||||
optional: true
|
||||
'@types/react':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
|
||||
js-cookie@3.0.5:
|
||||
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -2883,6 +2865,24 @@ packages:
|
|||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
zustand@5.0.8:
|
||||
resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=18.0.0'
|
||||
immer: '>=9.0.6'
|
||||
react: '>=18.0.0'
|
||||
use-sync-external-store: '>=1.2.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
use-sync-external-store:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
|
@ -5076,11 +5076,6 @@ snapshots:
|
|||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
jotai@2.15.0(@types/react@19.2.2)(react@19.1.0):
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.2
|
||||
react: 19.1.0
|
||||
|
||||
js-cookie@3.0.5: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
|
@ -5904,3 +5899,9 @@ snapshots:
|
|||
yallist@5.0.0: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zustand@5.0.8(@types/react@19.2.2)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)):
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.2
|
||||
react: 19.1.0
|
||||
use-sync-external-store: 1.6.0(react@19.1.0)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { isPortraitModeAtom } from '../atoms';
|
||||
import { useChatStore } from '../store';
|
||||
|
||||
import ChatMessageList from './components/ChatMessageList';
|
||||
import PortraitChat from './components/PortraitChat';
|
||||
|
||||
export default function ChatList() {
|
||||
const isPortraitMode = useAtomValue(isPortraitModeAtom);
|
||||
const isPortraitMode = useChatStore((state) => state.isPortraitMode);
|
||||
|
||||
return (
|
||||
<div className="mb-10 flex-1">
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { historyListOpenAtom } from '../../atoms';
|
||||
import { useChatStore } from '../../store';
|
||||
import Image from 'next/image';
|
||||
import IconFont from '@/components/ui/iconFont';
|
||||
|
||||
const ChatHistory = React.memo(() => {
|
||||
const setHistoryListOpen = useSetAtom(historyListOpenAtom);
|
||||
const setHistoryListOpen = useChatStore((state) => state.setHistoryListOpen);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col bg-[rgba(22,18,29,1)] pt-7.5 pr-5 pl-10">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { historyListOpenAtom, leftTabActiveKeyAtom } from '../../atoms';
|
||||
import { useChatStore } from '../../store';
|
||||
import { cn } from '@/lib';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
|
@ -11,8 +10,9 @@ import ArchiveHistory from './ArchiveHistory';
|
|||
import Info from './info';
|
||||
|
||||
export default function Side() {
|
||||
const [activeKey, setActiveKey] = useAtom(leftTabActiveKeyAtom);
|
||||
const setHistoryListOpen = useSetAtom(historyListOpenAtom);
|
||||
const activeKey = useChatStore((state) => state.leftTabActiveKey);
|
||||
const setActiveKey = useChatStore((state) => state.setLeftTabActiveKey);
|
||||
const setHistoryListOpen = useChatStore((state) => state.setHistoryListOpen);
|
||||
const Component = activeKey === 'info' ? Info : ArchiveHistory;
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
'use client';
|
||||
|
||||
import Side from './Side';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { historyListOpenAtom } from '../../atoms';
|
||||
import { useChatStore } from '../../store';
|
||||
import { memo } from 'react';
|
||||
import { Drawer } from '@/components';
|
||||
import ChatHistory from './ChatHisory';
|
||||
|
||||
const Left = memo(() => {
|
||||
const historyListOpen = useAtomValue(historyListOpenAtom);
|
||||
const historyListOpen = useChatStore((state) => state.historyListOpen);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-112">
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@ import {
|
|||
PhoneCallIcon,
|
||||
PortraitModeIcon,
|
||||
} from '@/assets/chatacter';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { isPhoneCallModeAtom, isPortraitModeAtom } from '../../atoms';
|
||||
import { useChatStore } from '../../store';
|
||||
import IconFont from '@/components/ui/iconFont';
|
||||
import { cn } from '@/lib';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function Actions() {
|
||||
const [isPortraitMode, setIsPortraitMode] = useAtom(isPortraitModeAtom);
|
||||
const setIsPhoneCallMode = useSetAtom(isPhoneCallModeAtom);
|
||||
const isPortraitMode = useChatStore((state) => state.isPortraitMode);
|
||||
const setIsPortraitMode = useChatStore((state) => state.setIsPortraitMode);
|
||||
const setIsPhoneCallMode = useChatStore((state) => state.setIsPhoneCallMode);
|
||||
const className = 'text-[#0066FF] cursor-pointer hover:text-[#4269D6]';
|
||||
|
||||
const suggestMessages = [
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
import Input from './input';
|
||||
import Actions from './actions';
|
||||
import ChatList from './ChatList';
|
||||
import { useAtom } from 'jotai';
|
||||
import { settingOpenAtom } from '../atoms';
|
||||
import { useChatStore } from '../store';
|
||||
import SettingForm from './Right';
|
||||
import { Drawer } from '@/components';
|
||||
import Left from './Left';
|
||||
|
|
@ -12,7 +11,8 @@ import { ExitFullScreenIcon, FullScreenIcon } from '@/assets/common';
|
|||
import { cn } from '@/lib';
|
||||
|
||||
export default function Main() {
|
||||
const [settingOpen, setSettingOpen] = useAtom(settingOpenAtom);
|
||||
const settingOpen = useChatStore((state) => state.settingOpen);
|
||||
const setSettingOpen = useChatStore((state) => state.setSettingOpen);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
interface ChatContextType {}
|
||||
|
||||
export const ChatContext = createContext<ChatContextType>({});
|
||||
|
||||
export default function ChatContextProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <ChatContext.Provider value={{}}>{children}</ChatContext.Provider>;
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
'use client';
|
||||
import {
|
||||
GenerateInputIcon,
|
||||
PhoneCallIcon,
|
||||
PortraitModeIcon,
|
||||
} from '@/assets/chatacter';
|
||||
import { useChatStore } from '../../store';
|
||||
import IconFont from '@/components/ui/iconFont';
|
||||
import { cn } from '@/lib';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function Actions() {
|
||||
const isPortraitMode = useChatStore((state) => state.isPortraitMode);
|
||||
const setIsPortraitMode = useChatStore((state) => state.setIsPortraitMode);
|
||||
const setIsPhoneCallMode = useChatStore((state) => state.setIsPhoneCallMode);
|
||||
const className = 'text-[#0066FF] cursor-pointer hover:text-[#4269D6]';
|
||||
|
||||
const suggestMessages = [
|
||||
'The threads of fate intertwine once more...The threads of fate intert',
|
||||
'The threads of fate intertwine once more.',
|
||||
'The threads of fate intertwine once more.',
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="relative mb-5 flex flex-col gap-2">
|
||||
{suggestMessages.map((message, index) => (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'rgba(36, 44, 80, 0.9)',
|
||||
width: '70%',
|
||||
}}
|
||||
className="flex h-10 cursor-pointer items-center rounded-full px-4"
|
||||
key={`message-${index}`}
|
||||
>
|
||||
<span className="line-clamp-2 text-xs">{message}</span>
|
||||
</div>
|
||||
))}
|
||||
<span
|
||||
style={{ left: '71%' }}
|
||||
className="flex-center absolute bottom-0 h-7 w-7 cursor-pointer rounded-full bg-black/40"
|
||||
>
|
||||
<IconFont type="icon-zhongxie" size={18} />
|
||||
</span>
|
||||
</div>
|
||||
{/* action */}
|
||||
<div className="flex justify-between">
|
||||
<div onClick={() => null} className="flex items-center gap-5">
|
||||
<div className={className}>
|
||||
<GenerateInputIcon />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setIsPhoneCallMode(true)}
|
||||
className={cn(className, 'relative')}
|
||||
>
|
||||
<PhoneCallIcon />
|
||||
<Image
|
||||
className="absolute right-0 bottom-0"
|
||||
src="/component/vip.svg"
|
||||
alt="phone call"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setIsPortraitMode(!isPortraitMode)}
|
||||
className="hover:cursor-pointer"
|
||||
>
|
||||
<PortraitModeIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,68 @@
|
|||
'use client';
|
||||
|
||||
import IconFont from '@/components/ui/iconFont';
|
||||
import { cn } from '@/lib';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { useChatStore } from '../store';
|
||||
|
||||
const message1 =
|
||||
'The threads of fate intertwine once more... I have been awaiting your arrival, seeker.The threads of fate intertwine once more... I have been awaiting your arrival, seeker.The threads of fate intertwine once more... I have been awaiting your arrival, seeker.';
|
||||
const message2 =
|
||||
'The threads of fate intertwine once more... I have been awaiting your arrival, seeker.The threads of fate intertwine once more';
|
||||
|
||||
export default function PhoneCallMode() {
|
||||
return <div>PhoneCallMode</div>;
|
||||
const setIsPhoneCallMode = useChatStore((state) => state.setIsPhoneCallMode);
|
||||
const [isTextVisible, setIsTextVisible] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-[calc(100vw-900px)] flex-col items-center">
|
||||
<div className="mt-5 flex flex-col items-center gap-6">
|
||||
<div className="flex gap-1">
|
||||
<Image
|
||||
className="flex-shrink-0 rounded-full object-cover"
|
||||
src="/avator.png"
|
||||
alt="avatar"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<span className="font-bold">{'Character 1 · 18'}</span>
|
||||
</div>
|
||||
<div className="text-xs font-black">{'00:01'}</div>
|
||||
</div>
|
||||
<div className="flex w-75 flex-1 flex-col justify-end gap-7.5 pb-25 text-sm">
|
||||
{isTextVisible && (
|
||||
<>
|
||||
<div className="text-white/40">{message1}</div>
|
||||
<div>{message2}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-10 cursor-pointer text-xs font-black">
|
||||
Tag to top
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setIsPhoneCallMode(false)}
|
||||
className={cn(
|
||||
'flex-center mb-25 h-20 w-20 cursor-pointer rounded-full bg-white/10',
|
||||
'bg-[linear-gradient(180deg,rgba(255,59,48,1)0%,rgba(222,46,36,1)100%)]'
|
||||
)}
|
||||
>
|
||||
<IconFont type="icon-tonghua" size={50} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 显示文本按钮 */}
|
||||
<div
|
||||
onClick={() => setIsTextVisible(!isTextVisible)}
|
||||
className={cn(
|
||||
'flex-center absolute top-8 right-10 h-10 w-10 cursor-pointer rounded-full',
|
||||
'bg-white/10 hover:bg-white/20'
|
||||
)}
|
||||
>
|
||||
<IconFont type="icon-tonghua" size={20} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
import { atom } from 'jotai';
|
||||
|
||||
// 是否打开两侧的设置
|
||||
export const settingOpenAtom = atom(true);
|
||||
|
||||
// 是否是立绘模式
|
||||
export const isPortraitModeAtom = atom(false);
|
||||
|
||||
// 是否是通话模式
|
||||
export const isPhoneCallModeAtom = atom(false);
|
||||
|
||||
// 左侧 tab active key
|
||||
export const leftTabActiveKeyAtom = atom<'info' | 'history'>('info');
|
||||
|
||||
// 左侧 角色历史列表
|
||||
export const historyListOpenAtom = atom<boolean>(false);
|
||||
|
|
@ -1,26 +1,22 @@
|
|||
'use client';
|
||||
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { isPhoneCallModeAtom } from './atoms';
|
||||
import { useChatStore } from './store';
|
||||
import ChatMode from './ChatMode';
|
||||
import './index.css';
|
||||
import PhoneCallMode from './PhoneCallMode';
|
||||
import ChatContextProvider from './Context';
|
||||
|
||||
export default function CharacterChat() {
|
||||
const isPhoneCallMode = useAtomValue(isPhoneCallModeAtom);
|
||||
const isPhoneCallMode = useChatStore((state) => state.isPhoneCallMode);
|
||||
|
||||
return (
|
||||
<ChatContextProvider>
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(23, 0, 18, 0.6) 0%, rgba(0, 0, 0, 0.1) 100%)',
|
||||
}}
|
||||
className="relative flex h-full w-full justify-center overflow-hidden"
|
||||
>
|
||||
{isPhoneCallMode ? <PhoneCallMode /> : <ChatMode />}
|
||||
</div>
|
||||
</ChatContextProvider>
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(23, 0, 18, 0.6) 0%, rgba(0, 0, 0, 0.1) 100%)',
|
||||
}}
|
||||
className="relative flex h-full w-full justify-center overflow-hidden"
|
||||
>
|
||||
{isPhoneCallMode ? <PhoneCallMode /> : <ChatMode />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import { create } from 'zustand';
|
||||
|
||||
export const useChatStore = create<{
|
||||
// UI state
|
||||
isPhoneCallMode: boolean;
|
||||
setIsPhoneCallMode: (isPhoneCallMode: boolean) => void;
|
||||
settingOpen: boolean;
|
||||
setSettingOpen: (settingOpen: boolean) => void;
|
||||
isPortraitMode: boolean;
|
||||
setIsPortraitMode: (isPortraitMode: boolean) => void;
|
||||
leftTabActiveKey: 'info' | 'history';
|
||||
setLeftTabActiveKey: (leftTabActiveKey: 'info' | 'history') => void;
|
||||
historyListOpen: boolean;
|
||||
setHistoryListOpen: (historyListOpen: boolean) => void;
|
||||
// data state
|
||||
}>((set) => ({
|
||||
isPhoneCallMode: false,
|
||||
setIsPhoneCallMode: (isPhoneCallMode) => set({ isPhoneCallMode }),
|
||||
settingOpen: false,
|
||||
setSettingOpen: (settingOpen) => set({ settingOpen }),
|
||||
isPortraitMode: false,
|
||||
setIsPortraitMode: (isPortraitMode) => set({ isPortraitMode }),
|
||||
leftTabActiveKey: 'info',
|
||||
setLeftTabActiveKey: (leftTabActiveKey) => set({ leftTabActiveKey }),
|
||||
historyListOpen: false,
|
||||
setHistoryListOpen: (historyListOpen) => set({ historyListOpen }),
|
||||
}));
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { Geist, Geist_Mono } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import GlobalContainer from '@/layouts/GlobalContainer';
|
||||
import Providers from '@/layouts/Providers';
|
||||
import Script from 'next/script';
|
||||
|
||||
const geistSans = Geist({
|
||||
|
|
@ -30,11 +30,11 @@ export default function RootLayout({
|
|||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<Script
|
||||
src="//at.alicdn.com/t/c/font_5054282_ij9uesv751.js"
|
||||
src="//at.alicdn.com/t/c/font_5054282_z80k01jnmui.js"
|
||||
strategy="afterInteractive"
|
||||
async
|
||||
/>
|
||||
<GlobalContainer>{children}</GlobalContainer>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
'use client';
|
||||
import { IntlProvider } from './IntlProvider';
|
||||
import { QueryProvider } from './QueryProvider';
|
||||
|
||||
export default function GlobalContainer({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<IntlProvider>
|
||||
<QueryProvider>{children}</QueryProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useLocale } from '@/layouts/GlobalContainer/IntlProvider';
|
||||
import { useLocale } from '@/layouts/Providers/IntlProvider';
|
||||
import { Select } from 'radix-ui';
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
|
|
|
|||
|
|
@ -7,19 +7,11 @@ import {
|
|||
useState,
|
||||
useEffect,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import zhMessages from '@/locales/zh';
|
||||
import enMessages from '@/locales/en';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
|
||||
type Locale = 'zh' | 'en';
|
||||
|
||||
const messages: Record<Locale, any> = {
|
||||
zh: zhMessages,
|
||||
en: enMessages,
|
||||
};
|
||||
|
||||
interface LocaleContextType {
|
||||
locale: Locale;
|
||||
setLocale: (locale: Locale) => void;
|
||||
|
|
@ -55,24 +47,35 @@ function getLocaleFromCookie(): Locale {
|
|||
|
||||
export function IntlProvider({ children }: IntlProviderProps) {
|
||||
const [locale, setLocaleState] = useState<Locale>('en');
|
||||
const [messages, setMessages] = useState<Record<string, any>>();
|
||||
|
||||
const loadLocale = useMemoizedFn(async (locale: Locale) => {
|
||||
// 动态加载, 提升首屏加载速度
|
||||
const messages = await import(`@/locales/${locale}.ts`);
|
||||
setMessages(messages.default);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const cookieLocale = getLocaleFromCookie();
|
||||
if (cookieLocale) {
|
||||
setLocaleState(cookieLocale);
|
||||
loadLocale(cookieLocale);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setLocale = useCallback((newLocale: Locale) => {
|
||||
const setLocale = useMemoizedFn((newLocale: Locale) => {
|
||||
setLocaleState(newLocale);
|
||||
setLocaleToCookie(newLocale);
|
||||
}, []);
|
||||
loadLocale(newLocale);
|
||||
});
|
||||
|
||||
if (!messages) return null;
|
||||
|
||||
return (
|
||||
<LocaleContext.Provider value={{ locale, setLocale }}>
|
||||
<NextIntlClientProvider
|
||||
locale={locale as any}
|
||||
messages={messages[locale]}
|
||||
messages={messages}
|
||||
timeZone="Asia/Shanghai"
|
||||
>
|
||||
{children}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
'use client';
|
||||
import { IntlProvider } from './IntlProvider';
|
||||
import { QueryProvider } from './QueryProvider';
|
||||
|
||||
export default function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<QueryProvider>
|
||||
<IntlProvider>{children}</IntlProvider>
|
||||
</QueryProvider>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue