feat(pages): 添加聊天页面和设置页面

- chat/[id]/page.tsx: 动态路由聊天页面,支持消息展示和发送
- settings/page.tsx: 设置页面,包含模型、主题、语言等配置项
This commit is contained in:
gaoziman 2025-12-17 22:55:03 +08:00
parent d055ec7473
commit 01777b3786
2 changed files with 468 additions and 0 deletions

151
src/app/chat/[id]/page.tsx Normal file
View File

@ -0,0 +1,151 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { useParams } from 'next/navigation';
import { Share2, MoreHorizontal } from 'lucide-react';
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
import { ChatInput } from '@/components/features/ChatInput';
import { MessageBubble } from '@/components/features/MessageBubble';
import { cn } from '@/lib/utils';
import {
models,
tools as initialTools,
chatHistories,
sampleMessages,
currentUser
} from '@/data/mock';
import type { Model, Tool, Message } from '@/types';
export default function ChatPage() {
const params = useParams();
const chatId = params.id as string;
const [sidebarOpen, setSidebarOpen] = useState(true);
const [selectedModel, setSelectedModel] = useState<Model>(models[1]);
const [tools, setTools] = useState<Tool[]>(initialTools);
const [messages, setMessages] = useState<Message[]>(sampleMessages);
const messagesEndRef = useRef<HTMLDivElement>(null);
// 获取当前聊天信息
const currentChat = chatHistories.find(c => c.id === chatId);
const chatTitle = currentChat?.title || '新对话';
// 滚动到底部
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleToolToggle = (toolId: string) => {
setTools((prev) =>
prev.map((tool) =>
tool.id === toolId ? { ...tool, enabled: !tool.enabled } : tool
)
);
};
const handleEnableAllTools = (enabled: boolean) => {
setTools((prev) =>
prev.map((tool) => ({ ...tool, enabled }))
);
};
const handleSend = (message: string) => {
// 添加用户消息
const userMessage: Message = {
id: `msg-${Date.now()}`,
role: 'user',
content: message,
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage]);
// 模拟 AI 回复
setTimeout(() => {
const aiMessage: Message = {
id: `msg-${Date.now() + 1}`,
role: 'assistant',
content: '感谢您的消息!这是一个模拟的 AI 回复。在实际应用中,这里会调用 AI API 生成真实的回复内容。\n\n您可以继续与我对话我会尽力帮助您解决问题。',
timestamp: new Date(),
};
setMessages((prev) => [...prev, aiMessage]);
}, 1000);
};
return (
<div className="flex min-h-screen">
{/* 侧边栏 */}
<Sidebar
user={currentUser}
chatHistories={chatHistories}
isOpen={sidebarOpen}
onToggle={() => setSidebarOpen(!sidebarOpen)}
/>
{/* 主内容区 */}
<main
className={cn(
'flex-1 flex flex-col min-h-screen transition-all duration-300',
sidebarOpen ? 'ml-[var(--sidebar-width)]' : 'ml-0'
)}
>
{/* 固定顶部 Header */}
<header className="h-[var(--header-height)] px-4 flex items-center justify-between border-b border-[var(--color-border)] bg-white sticky top-0 z-10">
<div className="flex items-center gap-3">
<SidebarToggle onClick={() => setSidebarOpen(!sidebarOpen)} />
<h1 className="text-base font-medium text-[var(--color-text-primary)] truncate max-w-[300px]">
{chatTitle}
</h1>
</div>
<div className="flex items-center gap-2">
<button
className="flex items-center gap-2 px-3 py-1.5 text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)] rounded-lg transition-colors"
title="分享对话"
>
<Share2 size={16} />
<span></span>
</button>
<button
className="w-8 h-8 flex items-center justify-center text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] rounded-lg transition-colors"
title="更多选项"
>
<MoreHorizontal size={18} />
</button>
</div>
</header>
{/* 消息列表区域 */}
<div className="flex-1 overflow-y-auto">
<div className="max-w-[900px] mx-auto px-4 py-6">
{messages.map((message) => (
<MessageBubble
key={message.id}
message={message}
user={message.role === 'user' ? currentUser : undefined}
/>
))}
<div ref={messagesEndRef} />
</div>
</div>
{/* 固定底部输入框 */}
<div className="sticky bottom-0 bg-gradient-to-t from-white via-white to-transparent pt-4">
<div className="max-w-[900px] mx-auto px-4 pb-4">
<ChatInput
models={models}
selectedModel={selectedModel}
onModelSelect={setSelectedModel}
tools={tools}
onToolToggle={handleToolToggle}
onEnableAllTools={handleEnableAllTools}
onSend={handleSend}
/>
</div>
</div>
</main>
</div>
);
}

317
src/app/settings/page.tsx Normal file
View File

@ -0,0 +1,317 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { ArrowLeft, Zap, Download } from 'lucide-react';
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
import { Toggle } from '@/components/ui/Toggle';
import { cn } from '@/lib/utils';
import { currentUser, chatHistories, models } from '@/data/mock';
import type { Settings } from '@/types';
export default function SettingsPage() {
const [sidebarOpen, setSidebarOpen] = useState(true);
const [settings, setSettings] = useState<Settings>({
defaultModel: 'sonnet',
theme: 'light',
language: 'zh-CN',
enableWebSearch: true,
enableCodeExecution: false,
saveChatHistory: true,
});
const handleToggle = (key: keyof Settings) => {
setSettings((prev) => ({
...prev,
[key]: !prev[key],
}));
};
const handleSelectChange = (key: keyof Settings, value: string) => {
setSettings((prev) => ({
...prev,
[key]: value,
}));
};
return (
<div className="flex min-h-screen">
{/* 侧边栏 */}
<Sidebar
user={currentUser}
chatHistories={chatHistories}
isOpen={sidebarOpen}
onToggle={() => setSidebarOpen(!sidebarOpen)}
/>
{/* 主内容区 */}
<main
className={cn(
'flex-1 min-h-screen transition-all duration-300',
sidebarOpen ? 'ml-[var(--sidebar-width)]' : 'ml-0'
)}
>
{/* Header */}
<header className="h-[var(--header-height)] px-4 flex items-center">
<SidebarToggle onClick={() => setSidebarOpen(!sidebarOpen)} />
</header>
{/* Body */}
<div className="px-8 pb-8">
<div className="max-w-[640px] mx-auto">
{/* 返回链接 */}
<Link
href="/"
className="inline-flex items-center gap-2 text-[var(--color-text-secondary)] text-sm mb-4 hover:text-[var(--color-text-primary)] transition-colors"
>
<ArrowLeft size={16} />
<span></span>
</Link>
{/* 页面标题 */}
<div className="mb-8">
<h1 className="text-2xl font-semibold text-[var(--color-text-primary)] mb-2">
</h1>
<p className="text-sm text-[var(--color-text-secondary)]">
</p>
</div>
{/* 账户设置 */}
<SettingsSection
title="账户"
description="管理您的账户信息"
>
<SettingsItem
label="邮箱"
description={currentUser.email}
>
<button className="btn-ghost"></button>
</SettingsItem>
<SettingsItem
label="当前计划"
description={
<span className="inline-flex items-center gap-2 px-4 py-2 bg-[var(--color-primary-light)] rounded-full text-sm text-[var(--color-primary)] font-medium">
<Zap size={14} />
{currentUser.plan === 'free' ? '免费计划' : '专业计划'}
</span>
}
>
<button className="btn-primary"></button>
</SettingsItem>
<SettingsItem
label="使用量"
description="本月消息数"
>
<div className="w-full">
<div className="mt-3">
<div className="h-2 bg-[var(--color-bg-tertiary)] rounded-full overflow-hidden">
<div
className="h-full bg-[var(--color-primary)] rounded-full transition-all duration-500"
style={{ width: '35%' }}
/>
</div>
<div className="flex justify-between mt-2 text-xs text-[var(--color-text-tertiary)]">
<span>35 / 100 </span>
<span>35%</span>
</div>
</div>
</div>
</SettingsItem>
</SettingsSection>
{/* 偏好设置 */}
<SettingsSection
title="偏好设置"
description="自定义您的体验"
>
<SettingsItem
label="默认模型"
description="为新对话选择默认 AI 模型"
>
<select
className="settings-select"
value={settings.defaultModel}
onChange={(e) => handleSelectChange('defaultModel', e.target.value)}
>
{models.map((model) => (
<option key={model.id} value={model.id}>
{model.name}
</option>
))}
</select>
</SettingsItem>
<SettingsItem
label="主题"
description="选择您喜欢的主题"
>
<select
className="settings-select"
value={settings.theme}
onChange={(e) => handleSelectChange('theme', e.target.value)}
>
<option value="system"></option>
<option value="light"></option>
<option value="dark"></option>
</select>
</SettingsItem>
<SettingsItem
label="语言"
description="选择您的首选语言"
>
<select
className="settings-select"
value={settings.language}
onChange={(e) => handleSelectChange('language', e.target.value)}
>
<option value="en">English</option>
<option value="zh-CN"></option>
<option value="zh-TW"></option>
<option value="ja"></option>
</select>
</SettingsItem>
<SettingsItem
label="启用网络搜索"
description="允许 AI 搜索网络获取信息"
>
<Toggle
checked={settings.enableWebSearch}
onChange={() => handleToggle('enableWebSearch')}
/>
</SettingsItem>
<SettingsItem
label="启用代码执行"
description="允许 AI 运行代码片段"
>
<Toggle
checked={settings.enableCodeExecution}
onChange={() => handleToggle('enableCodeExecution')}
/>
</SettingsItem>
</SettingsSection>
{/* 数据与隐私 */}
<SettingsSection
title="数据与隐私"
description="管理您的数据和隐私设置"
>
<SettingsItem
label="聊天历史"
description="保存您的对话记录"
>
<Toggle
checked={settings.saveChatHistory}
onChange={() => handleToggle('saveChatHistory')}
/>
</SettingsItem>
<SettingsItem
label="导出数据"
description="下载所有聊天历史"
>
<button className="btn-ghost inline-flex items-center gap-2">
<Download size={16} />
</button>
</SettingsItem>
<SettingsItem
label="清除所有聊天"
description="删除所有对话历史"
>
<button className="btn-ghost text-red-600 hover:text-red-700">
</button>
</SettingsItem>
</SettingsSection>
{/* 危险区域 */}
<SettingsSection
title="危险区域"
description="不可逆操作"
variant="danger"
>
<SettingsItem
label="删除账户"
description="永久删除您的账户和所有数据"
>
<button className="btn-danger"></button>
</SettingsItem>
</SettingsSection>
</div>
</div>
</main>
</div>
);
}
// 设置区域组件
interface SettingsSectionProps {
title: string;
description: string;
children: React.ReactNode;
variant?: 'default' | 'danger';
}
function SettingsSection({ title, description, children, variant = 'default' }: SettingsSectionProps) {
return (
<section
className={cn(
'bg-white border rounded-xl mb-6 overflow-hidden',
variant === 'danger' ? 'border-red-200' : 'border-[var(--color-border)]'
)}
>
<div
className={cn(
'px-5 py-4 border-b',
variant === 'danger'
? 'bg-red-50 border-red-200'
: 'border-[var(--color-border-light)]'
)}
>
<h2
className={cn(
'text-base font-semibold',
variant === 'danger' ? 'text-red-600' : 'text-[var(--color-text-primary)]'
)}
>
{title}
</h2>
<p className="text-sm text-[var(--color-text-secondary)] mt-1">
{description}
</p>
</div>
<div>{children}</div>
</section>
);
}
// 设置项组件
interface SettingsItemProps {
label: string;
description: React.ReactNode;
children: React.ReactNode;
}
function SettingsItem({ label, description, children }: SettingsItemProps) {
return (
<div className="flex justify-between items-center px-5 py-4 border-b border-[var(--color-border-light)] last:border-b-0 hover:bg-[var(--color-bg-tertiary)] transition-colors">
<div className="flex-1">
<div className="text-sm font-medium text-[var(--color-text-primary)] mb-1">
{label}
</div>
<div className="text-xs text-[var(--color-text-tertiary)]">
{description}
</div>
</div>
<div className="ml-4">{children}</div>
</div>
);
}