初始提交: Qihu-AIFlow v2.0.1 完整项目

- 前端 React+TypeScript (源码+构建产物)
- 后端 Python FastAPI
- 数据库迁移 001-015
- Docker部署配置
- 微信ClawBot API频道绑定
- 项目文档 README.md
This commit is contained in:
qyyfw committed 2026-07-02 14:40:46 +08:00
commit 78ff261b5a
199 files changed
+69126

No files matched your search

@@ -0,0 +1,394 @@
/* ===== Admin Layout ===== */
.admin-layout {
display: flex;
height: 100vh;
overflow: hidden;
background: #F1F3F7;
}
/* Admin Sidebar */
.admin-sidebar {
width: var(--admin-sidebar-width);
min-width: var(--admin-sidebar-width);
background: #1A1A2E;
display: flex;
flex-direction: column;
transition: width var(--transition-slow), min-width var(--transition-slow);
overflow: hidden;
z-index: var(--z-sticky);
}
.admin-sidebar.collapsed {
width: var(--sidebar-collapsed);
min-width: var(--sidebar-collapsed);
}
.admin-sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-lg);
height: var(--header-height);
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.admin-brand {
color: #E0E2F0 !important;
font-size: var(--text-base) !important;
}
.admin-sidebar .collapse-btn {
color: rgba(255,255,255,0.4);
}
.admin-sidebar .collapse-btn:hover {
background: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.8);
}
/* Admin Nav */
.admin-nav {
flex: 1;
padding: var(--space-sm);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2px;
}
.admin-nav-item {
display: flex;
align-items: center;
gap: var(--space-md);
padding: var(--space-md) var(--space-md);
border-radius: var(--radius-md);
color: rgba(255,255,255,0.55);
text-decoration: none;
font-size: var(--text-sm);
font-weight: 500;
transition: all var(--transition-fast);
white-space: nowrap;
width: 100%;
text-align: left;
}
.admin-nav-item:hover {
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.85);
}
.admin-nav-item.active {
background: rgba(107, 99, 255, 0.2);
color: #8B8DFF;
}
.admin-sidebar-footer {
padding: var(--space-sm);
border-top: 1px solid rgba(255,255,255,0.06);
display: flex;
flex-direction: column;
gap: 2px;
}
.logout-btn {
color: rgba(255,255,255,0.35) !important;
}
.logout-btn:hover {
color: #FF6B6B !important;
background: rgba(255,107,107,0.1) !important;
}
/* Admin Main */
.admin-main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
.admin-topbar {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--header-height);
padding: 0 var(--space-2xl);
background: #FFFFFF;
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.admin-topbar-left {
display: flex;
align-items: center;
}
.admin-welcome {
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.admin-topbar-right {
display: flex;
align-items: center;
gap: var(--space-lg);
}
.admin-user-tag {
display: flex;
align-items: center;
gap: var(--space-sm);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-text-primary);
}
.admin-topbar-action {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
background: var(--color-bg-primary);
color: var(--color-accent);
font-size: var(--text-sm);
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all var(--transition-fast);
}
.admin-topbar-action:hover {
background: var(--color-accent);
color: #FFFFFF;
border-color: var(--color-accent);
}
.admin-avatar {
background: #1A1A2E !important;
}
/* Admin Content Area */
.admin-content {
flex: 1;
overflow-y: auto;
padding: var(--space-2xl);
}
/* Admin Common Components */
.admin-page-title {
font-size: var(--text-xl);
font-weight: 700;
color: var(--color-text-primary);
margin-bottom: var(--space-2xl);
}
.admin-card {
background: #FFFFFF;
border-radius: var(--radius-xl);
border: 1px solid var(--color-border);
padding: var(--space-2xl);
margin-bottom: var(--space-xl);
}
.admin-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-xl);
}
.admin-card-title {
font-size: var(--text-lg);
font-weight: 600;
color: var(--color-text-primary);
}
.admin-table {
width: 100%;
border-collapse: collapse;
}
.admin-table th {
text-align: left;
padding: var(--space-md) var(--space-lg);
font-size: var(--text-xs);
font-weight: 600;
color: var(--color-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg-tertiary);
}
.admin-table td {
padding: var(--space-md) var(--space-lg);
font-size: var(--text-sm);
color: var(--color-text-primary);
border-bottom: 1px solid var(--color-border-light);
}
.admin-table tr:hover td {
background: var(--color-bg-hover);
}
.admin-badge {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: 600;
white-space: nowrap;
}
.badge-success { background: var(--color-success-light); color: var(--color-success); }
.badge-warning { background: var(--color-warning-light); color: var(--color-warning); }
.badge-error { background: var(--color-error-light); color: var(--color-error); }
.badge-info { background: var(--color-info-light); color: var(--color-info); }
.badge-neutral { background: var(--color-bg-tertiary); color: var(--color-text-secondary); }
.admin-btn {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-lg);
border-radius: var(--radius-md);
font-size: var(--text-sm);
font-weight: 600;
transition: all var(--transition-fast);
white-space: nowrap;
}
.admin-btn-primary {
background: var(--color-accent);
color: #FFFFFF;
}
.admin-btn-primary:hover {
background: var(--color-accent-hover);
}
.admin-btn-outline {
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-text-primary);
}
.admin-btn-outline:hover {
background: var(--color-bg-hover);
border-color: var(--color-border-hover);
}
.admin-btn-danger {
background: transparent;
border: 1px solid var(--color-error);
color: var(--color-error);
}
.admin-btn-danger:hover {
background: var(--color-error-light);
}
.admin-btn-sm {
padding: 3px 10px;
font-size: var(--text-xs);
}
/* Admin Stats */
.admin-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--space-lg);
margin-bottom: var(--space-2xl);
}
.admin-stat-card {
background: #FFFFFF;
border-radius: var(--radius-xl);
border: 1px solid var(--color-border);
padding: var(--space-xl);
}
.admin-stat-label {
font-size: var(--text-xs);
color: var(--color-text-tertiary);
font-weight: 500;
margin-bottom: var(--space-sm);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.admin-stat-value {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-text-primary);
}
/* Admin Form */
.admin-form-group {
margin-bottom: var(--space-xl);
}
.admin-form-label {
display: block;
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: var(--space-sm);
}
.admin-form-input {
width: 100%;
padding: var(--space-md) var(--space-lg);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: var(--text-sm);
color: var(--color-text-primary);
background: #FFFFFF;
transition: border-color var(--transition-fast);
outline: none;
}
.admin-form-input:focus {
border-color: var(--color-accent);
box-shadow: 0 0 0 3px var(--color-accent-light);
}
.admin-form-input::placeholder {
color: var(--color-text-disabled);
}
.admin-form-textarea {
min-height: 100px;
resize: vertical;
}
.admin-form-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-lg);
}
.admin-form-actions {
display: flex;
gap: var(--space-md);
padding-top: var(--space-lg);
border-top: 1px solid var(--color-border-light);
}
/* Version info in sidebar */
.admin-version-info {
padding: var(--space-sm) var(--space-md);
font-size: var(--text-xs);
color: rgba(255,255,255,0.25);
text-align: center;
white-space: nowrap;
border-top: 1px solid rgba(255,255,255,0.04);
}
@media (max-width: 768px) {
.admin-content {
padding: var(--space-lg);
}
.admin-form-row {
grid-template-columns: 1fr;
}
}
@@ -0,0 +1,124 @@
import React, { useState } from 'react';
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import './AdminLayout.css';
const adminNavItems = [
{ path: '/admin', icon: 'dashboard', label: '平台驾驶舱', exact: true },
{ path: '/admin/users', icon: 'users', label: '用户管理' },
{ path: '/admin/models', icon: 'models', label: '大模型库管理' },
{ path: '/admin/settings', icon: 'settings', label: '网站基础设置' },
{ path: '/admin/payment', icon: 'payment', label: '支付渠道配置' },
{ path: '/admin/services', icon: 'services', label: '增值服务管理' },
{ path: '/admin/membership', icon: 'membership', label: '会员订阅体系' },
{ path: '/admin/ads', icon: 'ads', label: '广告位管理' },
{ path: '/admin/market', icon: 'market', label: '应用市场审核' },
{ path: '/admin/logs', icon: 'logs', label: '日志与风控' },
];
const adminIconPaths: Record<string, JSX.Element> = {
dashboard: <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"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>,
users: <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>,
models: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>,
settings: <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="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>,
payment: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></svg>,
services: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>,
membership: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>,
ads: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></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>,
logs: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>,
};
const AdminLayout: React.FC = () => {
const { user, logout } = useAuth();
const navigate = useNavigate();
const [collapsed, setCollapsed] = useState(false);
if (!user || user.role !== 'admin') {
navigate('/login');
return null;
}
return (
<div className="admin-layout">
{/* Admin Sidebar */}
<aside className={`admin-sidebar ${collapsed ? 'collapsed' : ''}`}>
<div className="admin-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="#1A1A2E"/>
<path d="M8 20V12l4 4 4-4v8" stroke="#6C63FF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
<path d="M6 22h20" stroke="#6C63FF" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
</svg>
{!collapsed && <span className="sidebar-brand admin-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="admin-nav">
{adminNavItems.map(item => (
<NavLink
key={item.path}
to={item.path}
end={item.exact}
className={({ isActive }) => `admin-nav-item ${isActive ? 'active' : ''}`}
>
<span className="nav-icon">{adminIconPaths[item.icon]}</span>
{!collapsed && <span className="nav-label">{item.label}</span>}
</NavLink>
))}
</nav>
<div className="admin-sidebar-footer">
<NavLink to="/chat" className="admin-nav-item">
<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="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</span>
{!collapsed && <span className="nav-label">返回前台</span>}
</NavLink>
<button className="admin-nav-item logout-btn" onClick={() => { logout(); navigate('/login'); }}>
<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>
{!collapsed && (
<div className="admin-version-info">
Qihu-AIFlow v2.0.0
</div>
)}
</aside>
{/* Admin Main Content */}
<main className="admin-main">
<header className="admin-topbar">
<div className="admin-topbar-left">
<span className="admin-welcome">欢迎,{user.username}</span>
</div>
<div className="admin-topbar-right">
<a href="#/chat" className="admin-topbar-action">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" 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>
<span>返回前台</span>
</a>
<div className="admin-user-tag">
<div className="user-avatar-sm admin-avatar">
{user.username.charAt(0).toUpperCase()}
</div>
<span>{user.username}</span>
</div>
</div>
</header>
<div className="admin-content">
<Outlet />
</div>
</main>
</div>
);
};
export default AdminLayout;
@@ -0,0 +1,189 @@
/* ===== Front Layout ===== */
.front-layout {
display: flex;
height: 100vh;
overflow: hidden;
}
/* Sidebar */
.front-sidebar {
width: var(--sidebar-width);
min-width: var(--sidebar-width);
background: var(--color-sidebar-bg);
border-right: 1px solid var(--color-border-light);
display: flex;
flex-direction: column;
transition: width var(--transition-slow), min-width var(--transition-slow);
overflow: hidden;
z-index: var(--z-sticky);
}
.front-sidebar.collapsed {
width: var(--sidebar-collapsed);
min-width: var(--sidebar-collapsed);
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-lg) var(--space-lg);
height: var(--header-height);
border-bottom: 1px solid var(--color-border-light);
}
.sidebar-logo {
display: flex;
align-items: center;
gap: var(--space-md);
}
.sidebar-brand {
font-size: var(--text-lg);
font-weight: 700;
color: var(--color-text-primary);
letter-spacing: -0.02em;
white-space: nowrap;
}
.collapse-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--radius-md);
color: var(--color-text-tertiary);
transition: all var(--transition-fast);
}
.collapse-btn:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
/* Navigation */
.sidebar-nav {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: var(--space-sm) var(--space-sm);
overflow-y: auto;
}
.nav-section {
display: flex;
flex-direction: column;
gap: 2px;
}
.nav-item {
display: flex;
align-items: center;
gap: var(--space-md);
padding: var(--space-md) var(--space-md);
border-radius: var(--radius-md);
color: var(--color-text-secondary);
text-decoration: none;
font-size: var(--text-base);
font-weight: 500;
transition: all var(--transition-fast);
white-space: nowrap;
}
.nav-item:hover {
background: var(--color-sidebar-hover);
color: var(--color-text-primary);
}
.nav-item.active {
background: var(--color-sidebar-active);
color: var(--color-accent);
}
.nav-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
flex-shrink: 0;
}
.nav-label {
flex: 1;
}
/* Sidebar Footer */
.sidebar-footer {
padding: var(--space-md);
border-top: 1px solid var(--color-border-light);
}
.user-info-mini {
display: flex;
align-items: center;
gap: var(--space-md);
padding: var(--space-sm);
}
.user-avatar-sm {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
background: linear-gradient(135deg, var(--color-accent), #8B8DFF);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-sm);
font-weight: 600;
flex-shrink: 0;
}
.user-details {
display: flex;
flex-direction: column;
overflow: hidden;
}
.user-name {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text-primary);
line-height: 1.3;
}
.user-plan {
font-size: var(--text-xs);
color: var(--color-accent);
line-height: 1.3;
}
/* Main Content */
.front-main {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
min-width: 0;
}
/* Responsive */
@media (max-width: 768px) {
.front-sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: var(--z-overlay);
transform: translateX(-100%);
}
.front-sidebar.collapsed {
transform: translateX(-100%);
}
.front-sidebar.mobile-open {
transform: translateX(0);
}
}
@@ -0,0 +1,146 @@
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;