feat: 增加打电话界面
This commit is contained in:
parent
859ef2d320
commit
debca09f6c
|
|
@ -17,7 +17,6 @@
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"jotai": "^2.15.0",
|
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
|
|
@ -27,7 +26,8 @@
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-virtuoso": "^4.14.1",
|
"react-virtuoso": "^4.14.1",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,6 @@ importers:
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 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:
|
js-cookie:
|
||||||
specifier: ^3.0.5
|
specifier: ^3.0.5
|
||||||
version: 3.0.5
|
version: 3.0.5
|
||||||
|
|
@ -59,6 +56,9 @@ importers:
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.3.1
|
specifier: ^3.3.1
|
||||||
version: 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:
|
devDependencies:
|
||||||
'@eslint/eslintrc':
|
'@eslint/eslintrc':
|
||||||
specifier: ^3
|
specifier: ^3
|
||||||
|
|
@ -2110,24 +2110,6 @@ packages:
|
||||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||||
hasBin: true
|
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:
|
js-cookie@3.0.5:
|
||||||
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
@ -2883,6 +2865,24 @@ packages:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
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:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
@ -5076,11 +5076,6 @@ snapshots:
|
||||||
|
|
||||||
jiti@2.6.1: {}
|
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-cookie@3.0.5: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
@ -5904,3 +5899,9 @@ snapshots:
|
||||||
yallist@5.0.0: {}
|
yallist@5.0.0: {}
|
||||||
|
|
||||||
yocto-queue@0.1.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';
|
'use client';
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai';
|
import { useChatStore } from '../store';
|
||||||
import { isPortraitModeAtom } from '../atoms';
|
|
||||||
import ChatMessageList from './components/ChatMessageList';
|
import ChatMessageList from './components/ChatMessageList';
|
||||||
import PortraitChat from './components/PortraitChat';
|
import PortraitChat from './components/PortraitChat';
|
||||||
|
|
||||||
export default function ChatList() {
|
export default function ChatList() {
|
||||||
const isPortraitMode = useAtomValue(isPortraitModeAtom);
|
const isPortraitMode = useChatStore((state) => state.isPortraitMode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-10 flex-1">
|
<div className="mb-10 flex-1">
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useChatStore } from '../../store';
|
||||||
import { historyListOpenAtom } from '../../atoms';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import IconFont from '@/components/ui/iconFont';
|
import IconFont from '@/components/ui/iconFont';
|
||||||
|
|
||||||
const ChatHistory = React.memo(() => {
|
const ChatHistory = React.memo(() => {
|
||||||
const setHistoryListOpen = useSetAtom(historyListOpenAtom);
|
const setHistoryListOpen = useChatStore((state) => state.setHistoryListOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col bg-[rgba(22,18,29,1)] pt-7.5 pr-5 pl-10">
|
<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';
|
'use client';
|
||||||
|
|
||||||
import { useAtom, useSetAtom } from 'jotai';
|
import { useChatStore } from '../../store';
|
||||||
import { historyListOpenAtom, leftTabActiveKeyAtom } from '../../atoms';
|
|
||||||
import { cn } from '@/lib';
|
import { cn } from '@/lib';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
@ -11,8 +10,9 @@ import ArchiveHistory from './ArchiveHistory';
|
||||||
import Info from './info';
|
import Info from './info';
|
||||||
|
|
||||||
export default function Side() {
|
export default function Side() {
|
||||||
const [activeKey, setActiveKey] = useAtom(leftTabActiveKeyAtom);
|
const activeKey = useChatStore((state) => state.leftTabActiveKey);
|
||||||
const setHistoryListOpen = useSetAtom(historyListOpenAtom);
|
const setActiveKey = useChatStore((state) => state.setLeftTabActiveKey);
|
||||||
|
const setHistoryListOpen = useChatStore((state) => state.setHistoryListOpen);
|
||||||
const Component = activeKey === 'info' ? Info : ArchiveHistory;
|
const Component = activeKey === 'info' ? Info : ArchiveHistory;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Side from './Side';
|
import Side from './Side';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useChatStore } from '../../store';
|
||||||
import { historyListOpenAtom } from '../../atoms';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Drawer } from '@/components';
|
import { Drawer } from '@/components';
|
||||||
import ChatHistory from './ChatHisory';
|
import ChatHistory from './ChatHisory';
|
||||||
|
|
||||||
const Left = memo(() => {
|
const Left = memo(() => {
|
||||||
const historyListOpen = useAtomValue(historyListOpenAtom);
|
const historyListOpen = useChatStore((state) => state.historyListOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-112">
|
<div className="relative flex h-full w-112">
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@ import {
|
||||||
PhoneCallIcon,
|
PhoneCallIcon,
|
||||||
PortraitModeIcon,
|
PortraitModeIcon,
|
||||||
} from '@/assets/chatacter';
|
} from '@/assets/chatacter';
|
||||||
import { useAtom, useSetAtom } from 'jotai';
|
import { useChatStore } from '../../store';
|
||||||
import { isPhoneCallModeAtom, isPortraitModeAtom } from '../../atoms';
|
|
||||||
import IconFont from '@/components/ui/iconFont';
|
import IconFont from '@/components/ui/iconFont';
|
||||||
import { cn } from '@/lib';
|
import { cn } from '@/lib';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
export default function Actions() {
|
export default function Actions() {
|
||||||
const [isPortraitMode, setIsPortraitMode] = useAtom(isPortraitModeAtom);
|
const isPortraitMode = useChatStore((state) => state.isPortraitMode);
|
||||||
const setIsPhoneCallMode = useSetAtom(isPhoneCallModeAtom);
|
const setIsPortraitMode = useChatStore((state) => state.setIsPortraitMode);
|
||||||
|
const setIsPhoneCallMode = useChatStore((state) => state.setIsPhoneCallMode);
|
||||||
const className = 'text-[#0066FF] cursor-pointer hover:text-[#4269D6]';
|
const className = 'text-[#0066FF] cursor-pointer hover:text-[#4269D6]';
|
||||||
|
|
||||||
const suggestMessages = [
|
const suggestMessages = [
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@
|
||||||
import Input from './input';
|
import Input from './input';
|
||||||
import Actions from './actions';
|
import Actions from './actions';
|
||||||
import ChatList from './ChatList';
|
import ChatList from './ChatList';
|
||||||
import { useAtom } from 'jotai';
|
import { useChatStore } from '../store';
|
||||||
import { settingOpenAtom } from '../atoms';
|
|
||||||
import SettingForm from './Right';
|
import SettingForm from './Right';
|
||||||
import { Drawer } from '@/components';
|
import { Drawer } from '@/components';
|
||||||
import Left from './Left';
|
import Left from './Left';
|
||||||
|
|
@ -12,7 +11,8 @@ import { ExitFullScreenIcon, FullScreenIcon } from '@/assets/common';
|
||||||
import { cn } from '@/lib';
|
import { cn } from '@/lib';
|
||||||
|
|
||||||
export default function Main() {
|
export default function Main() {
|
||||||
const [settingOpen, setSettingOpen] = useAtom(settingOpenAtom);
|
const settingOpen = useChatStore((state) => state.settingOpen);
|
||||||
|
const setSettingOpen = useChatStore((state) => state.setSettingOpen);
|
||||||
|
|
||||||
return (
|
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';
|
'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() {
|
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';
|
'use client';
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai';
|
import { useChatStore } from './store';
|
||||||
import { isPhoneCallModeAtom } from './atoms';
|
|
||||||
import ChatMode from './ChatMode';
|
import ChatMode from './ChatMode';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import PhoneCallMode from './PhoneCallMode';
|
import PhoneCallMode from './PhoneCallMode';
|
||||||
import ChatContextProvider from './Context';
|
|
||||||
|
|
||||||
export default function CharacterChat() {
|
export default function CharacterChat() {
|
||||||
const isPhoneCallMode = useAtomValue(isPhoneCallModeAtom);
|
const isPhoneCallMode = useChatStore((state) => state.isPhoneCallMode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatContextProvider>
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
background:
|
||||||
background:
|
'linear-gradient(0deg, rgba(23, 0, 18, 0.6) 0%, rgba(0, 0, 0, 0.1) 100%)',
|
||||||
'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"
|
||||||
className="relative flex h-full w-full justify-center overflow-hidden"
|
>
|
||||||
>
|
{isPhoneCallMode ? <PhoneCallMode /> : <ChatMode />}
|
||||||
{isPhoneCallMode ? <PhoneCallMode /> : <ChatMode />}
|
</div>
|
||||||
</div>
|
|
||||||
</ChatContextProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 type { Metadata } from 'next';
|
||||||
import { Geist, Geist_Mono } from 'next/font/google';
|
import { Geist, Geist_Mono } from 'next/font/google';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
import GlobalContainer from '@/layouts/GlobalContainer';
|
import Providers from '@/layouts/Providers';
|
||||||
import Script from 'next/script';
|
import Script from 'next/script';
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
|
|
@ -30,11 +30,11 @@ export default function RootLayout({
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<Script
|
<Script
|
||||||
src="//at.alicdn.com/t/c/font_5054282_ij9uesv751.js"
|
src="//at.alicdn.com/t/c/font_5054282_z80k01jnmui.js"
|
||||||
strategy="afterInteractive"
|
strategy="afterInteractive"
|
||||||
async
|
async
|
||||||
/>
|
/>
|
||||||
<GlobalContainer>{children}</GlobalContainer>
|
<Providers>{children}</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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';
|
'use client';
|
||||||
|
|
||||||
import { useLocale } from '@/layouts/GlobalContainer/IntlProvider';
|
import { useLocale } from '@/layouts/Providers/IntlProvider';
|
||||||
import { Select } from 'radix-ui';
|
import { Select } from 'radix-ui';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,11 @@ import {
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useCallback,
|
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import zhMessages from '@/locales/zh';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import enMessages from '@/locales/en';
|
|
||||||
|
|
||||||
type Locale = 'zh' | 'en';
|
type Locale = 'zh' | 'en';
|
||||||
|
|
||||||
const messages: Record<Locale, any> = {
|
|
||||||
zh: zhMessages,
|
|
||||||
en: enMessages,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface LocaleContextType {
|
interface LocaleContextType {
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
setLocale: (locale: Locale) => void;
|
setLocale: (locale: Locale) => void;
|
||||||
|
|
@ -55,24 +47,35 @@ function getLocaleFromCookie(): Locale {
|
||||||
|
|
||||||
export function IntlProvider({ children }: IntlProviderProps) {
|
export function IntlProvider({ children }: IntlProviderProps) {
|
||||||
const [locale, setLocaleState] = useState<Locale>('en');
|
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(() => {
|
useEffect(() => {
|
||||||
const cookieLocale = getLocaleFromCookie();
|
const cookieLocale = getLocaleFromCookie();
|
||||||
if (cookieLocale) {
|
if (cookieLocale) {
|
||||||
setLocaleState(cookieLocale);
|
setLocaleState(cookieLocale);
|
||||||
|
loadLocale(cookieLocale);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setLocale = useCallback((newLocale: Locale) => {
|
const setLocale = useMemoizedFn((newLocale: Locale) => {
|
||||||
setLocaleState(newLocale);
|
setLocaleState(newLocale);
|
||||||
setLocaleToCookie(newLocale);
|
setLocaleToCookie(newLocale);
|
||||||
}, []);
|
loadLocale(newLocale);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!messages) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocaleContext.Provider value={{ locale, setLocale }}>
|
<LocaleContext.Provider value={{ locale, setLocale }}>
|
||||||
<NextIntlClientProvider
|
<NextIntlClientProvider
|
||||||
locale={locale as any}
|
locale={locale as any}
|
||||||
messages={messages[locale]}
|
messages={messages}
|
||||||
timeZone="Asia/Shanghai"
|
timeZone="Asia/Shanghai"
|
||||||
>
|
>
|
||||||
{children}
|
{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