dev 分支暂存

This commit is contained in:
2026-05-16 08:26:56 +08:00
parent 58c8caa570
commit eb4129176c
71 changed files with 8474 additions and 214 deletions
@@ -0,0 +1,44 @@
import { useState } from 'react';
import { Sidebar } from './Sidebar';
import { Header } from './Header';
import { useAuth } from '@/hooks/useAuth';
interface AppLayoutProps {
children: React.ReactNode;
}
export function AppLayout({ children }: AppLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const { isLoggedIn } = useAuth();
return (
<div className="flex h-screen bg-[#FFFAF5] dark:bg-[#1a1a2e]">
{/* 侧边栏 */}
{isLoggedIn && (
<>
{/* 移动端遮罩 */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/30 z-20 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
<div
className={`
fixed lg:static inset-y-0 left-0 z-30 w-64 transform transition-transform duration-300 ease-in-out
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}
>
<Sidebar onClose={() => setSidebarOpen(false)} />
</div>
</>
)}
{/* 主内容区 */}
<div className="flex-1 flex flex-col min-w-0">
{isLoggedIn && <Header onMenuClick={() => setSidebarOpen(!sidebarOpen)} />}
<main className="flex-1 overflow-hidden">{children}</main>
</div>
</div>
);
}
@@ -0,0 +1,45 @@
import { CyreneAvatar } from '@/components/persona/CyreneAvatar';
import { MoodIndicator } from '@/components/persona/MoodIndicator';
import { useAuth } from '@/hooks/useAuth';
interface HeaderProps {
onMenuClick: () => void;
}
export function Header({ onMenuClick }: HeaderProps) {
const { logout } = useAuth();
return (
<header className="flex items-center justify-between px-4 py-2 border-b border-pink-100 dark:border-pink-900 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm">
<div className="flex items-center gap-3">
{/* 移动端菜单按钮 */}
<button
onClick={onMenuClick}
className="lg:hidden p-1 text-gray-400 hover:text-pink-500 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<CyreneAvatar size="sm" />
<div>
<h1 className="text-base font-semibold text-pink-600 dark:text-pink-400">
</h1>
<MoodIndicator />
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400 hidden sm:block">🌸 </span>
<button
onClick={logout}
className="text-xs text-gray-400 hover:text-pink-500 transition-colors px-2 py-1"
>
退
</button>
</div>
</header>
);
}
@@ -0,0 +1,99 @@
import { useSession } from '@/hooks/useSession';
import { useSessionStore } from '@/store/sessionStore';
import { useEffect } from 'react';
import { CyreneAvatar } from '@/components/persona/CyreneAvatar';
interface SidebarProps {
onClose?: () => void;
}
export function Sidebar({ onClose }: SidebarProps) {
const { sessions, currentSessionId, loadSessions, createSession, deleteSession, setCurrentSession } = useSession();
const storeSessions = useSessionStore((s) => s.sessions);
useEffect(() => {
loadSessions();
}, [loadSessions]);
const displaySessions = sessions.length > 0 ? sessions : storeSessions;
const handleNewChat = async () => {
const session = await createSession();
if (session && onClose) onClose();
};
return (
<aside className="h-full bg-white/90 dark:bg-gray-900/90 border-r border-pink-100 dark:border-pink-900 flex flex-col">
{/* 侧边栏头部 */}
<div className="p-4 border-b border-pink-100 dark:border-pink-900">
<button
onClick={handleNewChat}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-pink-400 hover:bg-pink-500 text-white rounded-xl text-sm font-medium transition-colors"
>
<span>+</span>
<span></span>
</button>
</div>
{/* 会话列表 */}
<div className="flex-1 overflow-y-auto py-2">
{displaySessions.length === 0 ? (
<p className="text-xs text-gray-400 text-center py-8">
</p>
) : (
displaySessions.map((session) => (
<div
key={session.id}
onClick={() => {
setCurrentSession(session.id);
if (onClose) onClose();
}}
className={`
group flex items-center justify-between px-4 py-2.5 mx-2 rounded-lg cursor-pointer transition-colors
${
currentSessionId === session.id || session.id === useSessionStore.getState().currentSessionId
? 'bg-pink-50 dark:bg-pink-900/30 text-pink-600'
: 'hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-300'
}
`}
>
<div className="flex items-center gap-2 min-w-0 flex-1">
<CyreneAvatar size="sm" />
<div className="min-w-0">
<p className="text-sm font-medium truncate">{session.title || '新的对话'}</p>
<p className="text-xs text-gray-400 truncate">
{session.message_count || 0}
</p>
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
deleteSession(session.id);
}}
className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-red-400 transition-all"
title="删除会话"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
))
)}
</div>
{/* 底部信息 */}
<div className="p-4 border-t border-pink-100 dark:border-pink-900">
<div className="flex items-center gap-2 text-xs text-gray-400">
<CyreneAvatar size="sm" />
<div>
<p className="font-medium text-pink-400"> AI</p>
<p>v0.1.0 MVP</p>
</div>
</div>
</div>
</aside>
);
}