147 lines
8.0 KiB
TypeScript
147 lines
8.0 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import './FrontLayout.css';
|
|
|
|
const navItems = [
|
|
{ path: '/chat', icon: 'chat', label: 'AI聊天' },
|
|
{ path: '/channels', icon: 'channel', label: '频道' },
|
|
{ path: '/knowledge', icon: 'knowledge', label: '知识库' },
|
|
{ path: '/market', icon: 'market', label: '应用市场' },
|
|
{ path: '/token', icon: 'token', label: 'Token驾驶舱' },
|
|
{ path: '/pricing', icon: 'pricing', label: '付费中心' },
|
|
{ path: '/assets', icon: 'assets', label: '资产' },
|
|
{ path: '/evolution', icon: 'evolution', label: 'AI进化' },
|
|
];
|
|
|
|
const bottomItems = [
|
|
{ path: '/profile', icon: 'profile', label: '个人中心' },
|
|
{ path: '/notifications', icon: 'notify', label: '消息通知' },
|
|
];
|
|
|
|
const iconPaths: Record<string, JSX.Element> = {
|
|
chat: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>,
|
|
channel: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>,
|
|
knowledge: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>,
|
|
market: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></svg>,
|
|
token: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>,
|
|
pricing: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>,
|
|
profile: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>,
|
|
notify: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>,
|
|
assets: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>,
|
|
evolution: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>,
|
|
};
|
|
|
|
const FrontLayout: React.FC = () => {
|
|
const { user, logout } = useAuth();
|
|
const navigate = useNavigate();
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
|
|
if (!user) {
|
|
navigate('/login');
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="front-layout">
|
|
{/* Sidebar */}
|
|
<aside className={`front-sidebar ${collapsed ? 'collapsed' : ''}`}>
|
|
<div className="sidebar-header">
|
|
<div className="sidebar-logo">
|
|
<svg width="28" height="28" viewBox="0 0 32 32" fill="none">
|
|
<rect width="32" height="32" rx="8" fill="#5B5FED"/>
|
|
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
|
|
<path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
|
|
</svg>
|
|
{!collapsed && <span className="sidebar-brand">Qihu-AIFlow</span>}
|
|
</div>
|
|
<button className="collapse-btn" onClick={() => setCollapsed(!collapsed)}>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<nav className="sidebar-nav">
|
|
<div className="nav-section">
|
|
{navItems.map(item => (
|
|
<NavLink
|
|
key={item.path}
|
|
to={item.path}
|
|
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
|
|
>
|
|
<span className="nav-icon">{iconPaths[item.icon]}</span>
|
|
{!collapsed && <span className="nav-label">{item.label}</span>}
|
|
</NavLink>
|
|
))}
|
|
</div>
|
|
<div className="nav-section nav-bottom">
|
|
{bottomItems.map(item => (
|
|
<NavLink
|
|
key={item.path}
|
|
to={item.path}
|
|
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
|
|
>
|
|
<span className="nav-icon">{iconPaths[item.icon]}</span>
|
|
{!collapsed && <span className="nav-label">{item.label}</span>}
|
|
</NavLink>
|
|
))}
|
|
</div>
|
|
</nav>
|
|
|
|
<div className="sidebar-footer">
|
|
<div className="user-info-mini">
|
|
<div className="user-avatar-sm">
|
|
{user.username.charAt(0).toUpperCase()}
|
|
</div>
|
|
{!collapsed && (
|
|
<div className="user-details">
|
|
<span className="user-name">{user.username}</span>
|
|
<span className="user-plan">{user.membership?.planName || '免费版'}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{user.role === 'admin' && (
|
|
<NavLink
|
|
to="/admin"
|
|
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
|
|
title="返回后台"
|
|
>
|
|
<span className="nav-icon">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
|
<rect x="3" y="3" width="7" height="7" rx="1.2"/>
|
|
<rect x="14" y="3" width="7" height="7" rx="1.2"/>
|
|
<rect x="3" y="14" width="7" height="7" rx="1.2"/>
|
|
<rect x="14" y="14" width="7" height="7" rx="1.2"/>
|
|
</svg>
|
|
</span>
|
|
{!collapsed && <span className="nav-label">返回后台</span>}
|
|
</NavLink>
|
|
)}
|
|
<button
|
|
className="nav-item logout-btn"
|
|
onClick={() => { logout(); navigate('/login'); }}
|
|
title="退出登录"
|
|
>
|
|
<span className="nav-icon">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
|
<polyline points="16 17 21 12 16 7"/>
|
|
<line x1="21" y1="12" x2="9" y2="12"/>
|
|
</svg>
|
|
</span>
|
|
{!collapsed && <span className="nav-label">退出登录</span>}
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<main className="front-main">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FrontLayout;
|