refactor: sync running-container source back to repo for reproducible docker compose build; remove dead code (orphan pipeline.py + 104 stale files from iterative dev) and drop unused app/ mirror dir

This commit is contained in:
qyyfw committed 2026-07-11 06:29:32 +08:00
1 parent 1346fbbd4b
commit 5d7f32db0a
138 files changed
+12846 -84248

No files matched your search

File diff suppressed because it is too large. Load diff
+95 -206
View File
@@ -1,232 +1,121 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import './AdminLayout.css';
interface NavItem {
path: string;
icon: string;
label: string;
exact?: boolean;
}
interface NavGroup {
title: string;
icon: string;
items: NavItem[];
}
const adminNavGroups: NavGroup[] = [
{
title: '驾驶舱',
icon: 'dashboard',
items: [
{ path: '/admin', icon: 'dashboard', label: '平台驾驶舱', exact: true },
{ path: '/admin/analytics', icon: 'analytics', label: '数据分析' },
],
},
{
title: 'AI 能力',
icon: 'agents',
items: [
{ path: '/admin/agents', icon: 'agents', label: 'Agent 管理' },
{ path: '/admin/skills', icon: 'skills', label: '技能管理' },
{ path: '/admin/models', icon: 'models', label: '大模型库' },
{ path: '/admin/knowledge', icon: 'knowledge', label: '知识库管理' },
{ path: '/admin/workflow', icon: 'workflow', label: '工作流编排' },
{ path: '/admin/mcp', icon: 'mcp', label: 'MCP 服务' },
],
},
{
title: '应用与市场',
icon: 'market',
items: [
{ path: '/admin/apps', icon: 'apps', label: 'AI 应用管理' },
{ path: '/admin/market', icon: 'market', label: '应用市场审核' },
{ path: '/admin/code-exec', icon: 'code', label: '代码执行沙箱' },
],
},
{
title: '用户运营',
icon: 'users',
items: [
{ path: '/admin/users', icon: 'users', label: '用户管理' },
{ path: '/admin/membership', icon: 'membership', label: '会员订阅' },
{ path: '/admin/promotion', icon: 'promotion', label: '推广系统' },
{ path: '/admin/achievement', icon: 'achievement', label: '成就系统' },
],
},
{
title: '商业变现',
icon: 'billing',
items: [
{ path: '/admin/billing', icon: 'billing', label: '计费与支付' },
{ path: '/admin/payment', icon: 'payment', label: '支付渠道配置' },
{ path: '/admin/services', icon: 'services', label: '增值服务' },
{ path: '/admin/ads', icon: 'ads', label: '广告位管理' },
{ path: '/admin/assets', icon: 'assets', label: '资产管理' },
{ path: '/admin/tenant', icon: 'tenant', label: '多租户管理' },
],
},
{
title: '系统管理',
icon: 'settings',
items: [
{ path: '/admin/settings', icon: 'settings', label: '网站设置' },
{ path: '/admin/logs', icon: 'logs', label: '日志与风控' },
{ path: '/admin/im-bots', icon: 'imbot', label: 'IM 机器人' },
{ path: '/admin/privacy', icon: 'privacy', label: '隐私与本地代理' },
{ path: '/admin/distributions', icon: 'distro', label: '发行版构建' },
],
},
{
title: '开发与集成',
icon: 'developer',
items: [
{ path: '/admin/storage', icon: 'storage', label: '存储配置' },
{ path: '/admin/automation', icon: 'automation', label: '自动化任务' },
{ path: '/admin/developer', icon: 'developer', label: '开发者 API' },
{ path: '/admin/developer-tools', icon: 'devtools', label: '开发者工具' },
],
},
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="18" height="18" 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="18" height="18" 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="18" height="18" 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="18" height="18" 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="18" height="18" 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="18" height="18" 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="18" height="18" 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="18" height="18" 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="18" height="18" 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="18" height="18" 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>,
agents: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="5" r="2"/><path d="M12 7v4"/><line x1="8" y1="16" x2="8" y2="16"/><line x1="16" y1="16" x2="16" y2="16"/></svg>,
skills: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>,
code: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>,
distro: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>,
privacy: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/><circle cx="12" cy="16" r="1"/></svg>,
promotion: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="8" r="7"/><path d="M8.5 12.5L6 21l6-2 6 2-2.5-8.5"/><circle cx="12" cy="8" r="3"/></svg>,
achievement: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.27 7.82 22 7 14.14l-5-4.87 6.91-1.01L12 2z"/></svg>,
tenant: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M3 21V8l9-5 9 5v13"/><path d="M9 21v-6h6v6"/><path d="M3 13h18"/></svg>,
billing: <svg width="18" height="18" 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"/><line x1="6" y1="15" x2="10" y2="15"/></svg>,
analytics: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/><line x1="3" y1="20" x2="21" y2="20"/></svg>,
imbot: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>,
storage: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>,
automation: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>,
developer: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>,
devtools: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>,
knowledge: <svg width="18" height="18" 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>,
workflow: <svg width="18" height="18" 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"/><line x1="10" y1="6" x2="14" y2="6"/><line x1="10" y1="17" x2="14" y2="17"/></svg>,
mcp: <svg width="18" height="18" 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>,
apps: <svg width="18" height="18" 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>,
assets: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>,
chevron: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"/></svg>,
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);
// Default: all groups collapsed. Auto-expand the group containing the active route.
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(() => {
const initial = new Set<string>();
const currentHash = window.location.hash;
for (const group of adminNavGroups) {
const isActive = group.items.some(item =>
item.exact ? currentHash === `#${item.path}` : currentHash.startsWith(`#${item.path}`)
);
if (isActive) {
initial.add(group.title);
break;
}
}
return initial;
});
const toggleGroup = (title: string) => {
setExpandedGroups(prev => {
const next = new Set(prev);
if (next.has(title)) next.delete(title);
else next.add(title);
return next;
});
};
const handleLogout = () => {
logout();
navigate('/');
};
const handleVisitFrontend = () => {
const url = window.location.origin + window.location.pathname + '#/chat';
window.open(url, '_blank');
};
if (!user || user.role !== 'admin') {
navigate('/login');
return null;
}
return (
<div className="admin-layout">
<aside className="admin-sidebar">
{/* Admin Sidebar */}
<aside className={`admin-sidebar ${collapsed ? 'collapsed' : ''}`}>
<div className="admin-sidebar-header">
<h1 className="admin-logo">七虎AI</h1>
<span className="admin-subtitle">管理控制台</span>
<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>
<button onClick={handleVisitFrontend} className="admin-frontend-link">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
<span>访问前台</span>
</button>
<nav className="sidebar-nav">
{adminNavGroups.map((group) => {
const isExpanded = expandedGroups.has(group.title);
const hasActiveChild = group.items.some(item =>
item.exact ? window.location.hash === `#${item.path}` : window.location.hash.startsWith(`#${item.path}`)
);
return (
<div key={group.title} className={`nav-group ${hasActiveChild ? 'nav-group-active' : ''}`}>
<div
className={`nav-group-header ${isExpanded ? 'expanded' : ''}`}
onClick={() => toggleGroup(group.title)}
>
<span className="nav-group-chevron-wrap">
<span className={`nav-group-chevron ${isExpanded ? 'chevron-expanded' : ''}`}>
{adminIconPaths.chevron}
</span>
</span>
<span className="nav-group-title">{group.title}</span>
</div>
{isExpanded && (
<div className="nav-group-items">
{group.items.map((item) => (
<NavLink
key={item.path}
to={item.path}
end={item.exact}
className={({ isActive }) =>
`nav-item ${isActive ? 'nav-item-active' : ''}`
}
>
<span className="nav-item-icon">{adminIconPaths[item.icon]}</span>
<span className="nav-item-label">{item.label}</span>
</NavLink>
))}
</div>
)}
</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">
<div className="admin-user-info">
<div className="admin-user-avatar">{user?.username?.[0]?.toUpperCase() || 'A'}</div>
<div className="admin-user-detail">
<div className="admin-user-name">{user?.username || 'admin'}</div>
<button className="admin-logout-btn" onClick={handleLogout}>退出登录</button>
<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>
</aside>
<main className="admin-content">
<Outlet />
</main>
</div>
);
+19 -71
View File
@@ -1,18 +1,16 @@
/* ===== Front Layout V3 - Light Theme ===== */
/* ===== Front Layout ===== */
.front-layout {
display: flex;
height: 100vh;
overflow: hidden;
background: #F5F7FC;
color: #1A1F36;
}
/* Sidebar */
.front-sidebar {
width: var(--sidebar-width);
min-width: var(--sidebar-width);
background: linear-gradient(180deg, #FFFFFF 0%, #FAFBFE 100%);
border-right: 1px solid rgba(0, 0, 0, 0.08);
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);
@@ -31,7 +29,7 @@
justify-content: space-between;
padding: var(--space-lg) var(--space-lg);
height: var(--header-height);
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
border-bottom: 1px solid var(--color-border-light);
}
.sidebar-logo {
@@ -43,7 +41,7 @@
.sidebar-brand {
font-size: var(--text-lg);
font-weight: 700;
color: #1A1F36;
color: var(--color-text-primary);
letter-spacing: -0.02em;
white-space: nowrap;
}
@@ -55,14 +53,13 @@
width: 32px;
height: 32px;
border-radius: var(--radius-md);
color: #8892B0;
color: var(--color-text-tertiary);
transition: all var(--transition-fast);
cursor: pointer;
}
.collapse-btn:hover {
background: rgba(0, 0, 0, 0.08);
color: #8892B0;
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
/* Navigation */
@@ -71,7 +68,7 @@
display: flex;
flex-direction: column;
justify-content: space-between;
padding: var(--space-sm);
padding: var(--space-sm) var(--space-sm);
overflow-y: auto;
}
@@ -80,23 +77,14 @@
flex-direction: column;
gap: 2px;
}
.nav-group-title {
padding: 14px var(--space-md) 6px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #9CA3AF;
}
.nav-item {
position: relative;
display: flex;
align-items: center;
gap: var(--space-md);
padding: 10px var(--space-md);
padding: var(--space-md) var(--space-md);
border-radius: var(--radius-md);
color: #8892B0;
color: var(--color-text-secondary);
text-decoration: none;
font-size: var(--text-base);
font-weight: 500;
@@ -105,13 +93,13 @@
}
.nav-item:hover {
background: rgba(108, 99, 255, 0.06);
color: #1A1F36;
background: var(--color-sidebar-hover);
color: var(--color-text-primary);
}
.nav-item.active {
background: rgba(108, 99, 255, 0.12);
color: #8B85FF;
background: var(--color-sidebar-active);
color: var(--color-accent);
}
.nav-icon {
@@ -127,30 +115,10 @@
flex: 1;
}
/* Active dot indicator */
.nav-active-dot {
display: none;
}
.nav-item.active .nav-active-dot {
display: block;
position: absolute;
right: 8px;
width: 6px;
height: 6px;
border-radius: 50%;
background: #6C63FF;
box-shadow: 0 0 8px rgba(108, 99, 255, 0.5);
}
/* Sidebar Footer */
.sidebar-footer {
padding: var(--space-md);
border-top: 1px solid rgba(0, 0, 0, 0.08);
}
.sidebar-footer .nav-item {
margin-top: 2px;
border-top: 1px solid var(--color-border-light);
}
.user-info-mini {
@@ -158,14 +126,13 @@
align-items: center;
gap: var(--space-md);
padding: var(--space-sm);
margin-bottom: 4px;
}
.user-avatar-sm {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
background: linear-gradient(135deg, #6C63FF, #A78BFA);
background: linear-gradient(135deg, var(--color-accent), #8B8DFF);
color: white;
display: flex;
align-items: center;
@@ -184,21 +151,16 @@
.user-name {
font-size: var(--text-sm);
font-weight: 600;
color: #1A1F36;
color: var(--color-text-primary);
line-height: 1.3;
}
.user-plan {
font-size: var(--text-xs);
color: #8B85FF;
color: var(--color-accent);
line-height: 1.3;
}
.logout-btn:hover {
color: #EF4444 !important;
background: rgba(239, 68, 68, 0.1) !important;
}
/* Main Content */
.front-main {
flex: 1;
@@ -206,12 +168,6 @@
display: flex;
flex-direction: column;
min-width: 0;
background: #F5F7FC;
}
/* Forward-slash route for apps - hidden default route */
.front-main > * {
height: 100%;
}
/* Responsive */
@@ -231,11 +187,3 @@
transform: translateX(0);
}
}
/* Home & Admin links */
.nav-home-link {
margin-bottom: 4px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
padding-bottom: 8px;
}
+146 -183
View File
@@ -1,183 +1,146 @@
import React, { useState } from 'react';
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import './FrontLayout.css';
const navGroups = [
{
title: '智能体',
items: [
{ path: '/chat', icon: 'chat', label: 'AI聊天' },
{ path: '/knowledge', icon: 'knowledge', label: '知识库' },
{ path: '/market', icon: 'market', label: '技能市场' },
{ path: '/evolution', icon: 'evolution', label: 'AI自进化' },
],
},
{
title: 'IM Bot',
items: [
{ path: '/channels', icon: 'channel', label: '频道管理' },
{ path: '/wechat-bot', icon: 'wechat', label: '微信机器人' },
],
},
{
title: '资源中心',
items: [
{ path: '/assets', icon: 'assets', label: '资产文件' },
{ path: '/token', icon: 'token', label: 'Token驾驶舱' },
{ path: '/pricing', icon: 'pricing', label: '付费中心' },
],
},
];
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>,
apps: <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="2" width="9" height="9" rx="2"/><rect x="13" y="2" width="9" height="9" rx="2"/><rect x="2" y="13" width="9" height="9" rx="2"/><rect x="13" y="13" width="5" height="5" rx="1.5"/><path d="M18 18v3"/><path d="M19.5 16.5v6"/></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>,
wechat: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/><circle cx="9" cy="11" r="1"/><circle cx="15" cy="11" r="1"/></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' : ''}`}>
{/* Header */}
<div className="sidebar-header">
<div className="sidebar-logo">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id="flGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#6C63FF"/>
<stop offset="100%" stopColor="#A78BFA"/>
</linearGradient>
</defs>
<rect width="32" height="32" rx="8" fill="url(#flGrad)"/>
<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">七虎AI</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">
{collapsed ? <><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></> : <><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></>}
</svg>
</button>
</div>
{/* Navigation */}
<nav className="sidebar-nav">
<NavLink to="/" className={({ isActive }) => `nav-item nav-home-link`} end>
<span className="nav-item-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
</span>
{!collapsed && <span className="nav-item-label">返回首页</span>}
</NavLink>
{navGroups.map(group => (
<div className="nav-section" key={group.title}>
{!collapsed && <div className="nav-group-title">{group.title}</div>}
{group.items.map(item => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
title={collapsed ? item.label : undefined}
>
<span className="nav-icon">{iconPaths[item.icon]}</span>
{!collapsed && <span className="nav-label">{item.label}</span>}
{!collapsed && <span className="nav-active-dot" />}
</NavLink>
))}
</div>
))}
<div className="nav-section nav-bottom">
{bottomItems.map(item => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
title={collapsed ? item.label : undefined}
>
<span className="nav-icon">{iconPaths[item.icon]}</span>
{!collapsed && <span className="nav-label">{item.label}</span>}
{!collapsed && <span className="nav-active-dot" />}
</NavLink>
))}
</div>
</nav>
{/* Footer */}
<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">
<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>
</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;
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;