fix(admin): 后台侧栏按DESIGN.md翻浅色主题+品牌色#5B5FED;修正CSS类名错配(.sidebar-nav/.nav-item)使全部功能板块可见;默认展开所有分组
This commit is contained in:
1 parent
9b87012248
commit
5617d4304b
2 files changed
+419
-57
No files matched your search
@@ -1,20 +1,21 @@
|
||||
/* ===== Admin Layout ===== */
|
||||
/* ===== Admin Layout (DESIGN.md: 浅色主题 + 品牌色 #5B5FED) ===== */
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #F1F3F7;
|
||||
background: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
/* Admin Sidebar */
|
||||
/* Admin Sidebar —— 浅色侧栏 */
|
||||
.admin-sidebar {
|
||||
width: var(--admin-sidebar-width);
|
||||
min-width: var(--admin-sidebar-width);
|
||||
background: #1A1A2E;
|
||||
background: var(--color-sidebar-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width var(--transition-slow), min-width var(--transition-slow);
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--color-border);
|
||||
z-index: var(--z-sticky);
|
||||
}
|
||||
|
||||
@@ -25,76 +26,204 @@
|
||||
|
||||
.admin-sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-lg);
|
||||
align-items: baseline;
|
||||
gap: var(--space-sm);
|
||||
padding: 0 var(--space-lg);
|
||||
height: var(--header-height);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-brand {
|
||||
color: #E0E2F0 !important;
|
||||
font-size: var(--text-base) !important;
|
||||
.admin-logo {
|
||||
margin: 0;
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 800;
|
||||
color: var(--color-text-primary);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.admin-subtitle {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.admin-sidebar .collapse-btn {
|
||||
color: rgba(255,255,255,0.4);
|
||||
.admin-frontend-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
margin: var(--space-sm) var(--space-md);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
.admin-sidebar .collapse-btn:hover {
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: rgba(255,255,255,0.8);
|
||||
.admin-frontend-link:hover {
|
||||
background: var(--color-accent-light);
|
||||
border-color: var(--color-accent-light);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Admin Nav */
|
||||
.admin-nav {
|
||||
/* Admin Nav (侧栏分组 + 菜单项) */
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: var(--space-sm);
|
||||
padding: var(--space-sm) var(--space-xs);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.admin-nav-item {
|
||||
.nav-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--color-text-secondary);
|
||||
transition: background var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
.nav-group-header:hover {
|
||||
background: var(--color-sidebar-hover);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.nav-group-active > .nav-group-header {
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-group-chevron-wrap {
|
||||
display: inline-flex;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.nav-group-chevron {
|
||||
display: inline-flex;
|
||||
color: var(--color-text-tertiary);
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
.nav-group-chevron.chevron-expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.nav-group-title {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.nav-group-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 2px 0 6px;
|
||||
margin-left: var(--space-lg);
|
||||
border-left: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-md) var(--space-md);
|
||||
padding: 9px var(--space-md);
|
||||
margin-left: var(--space-sm);
|
||||
border-radius: var(--radius-md);
|
||||
color: rgba(255,255,255,0.55);
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
transition: all var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.nav-item:hover {
|
||||
background: var(--color-sidebar-hover);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.nav-item.active,
|
||||
.nav-item-active {
|
||||
background: var(--color-accent-light);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.nav-item.active .nav-item-icon,
|
||||
.nav-item-active .nav-item-icon {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.admin-nav-item:hover {
|
||||
background: rgba(255,255,255,0.06);
|
||||
color: rgba(255,255,255,0.85);
|
||||
.nav-item-icon {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.admin-nav-item.active {
|
||||
background: rgba(107, 99, 255, 0.2);
|
||||
color: #8B8DFF;
|
||||
.nav-item-label {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Sidebar footer */
|
||||
.admin-sidebar-footer {
|
||||
padding: var(--space-sm);
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
color: rgba(255,255,255,0.35) !important;
|
||||
.admin-user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
}
|
||||
.logout-btn:hover {
|
||||
color: #FF6B6B !important;
|
||||
background: rgba(255,107,107,0.1) !important;
|
||||
.admin-user-avatar {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-accent);
|
||||
color: #FFFFFF;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: var(--text-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.admin-user-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
.admin-user-name {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.admin-logout-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin-top: 2px;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
}
|
||||
.admin-logout-btn:hover {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* Admin Main */
|
||||
@@ -164,10 +293,6 @@
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.admin-avatar {
|
||||
background: #1A1A2E !important;
|
||||
}
|
||||
|
||||
/* Admin Content Area */
|
||||
.admin-content {
|
||||
flex: 1;
|
||||
@@ -378,10 +503,10 @@
|
||||
.admin-version-info {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
font-size: var(--text-xs);
|
||||
color: rgba(255,255,255,0.25);
|
||||
color: var(--color-text-disabled);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
border-top: 1px solid rgba(255,255,255,0.04);
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@@ -1,262 +1,499 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { Outlet, NavLink, useNavigate, useLocation } 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/sms', icon: 'sms', 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 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>,
|
||||
|
||||
sms: <svg width="18" height="18" 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>,
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const AdminLayout: React.FC = () => {
|
||||
|
||||
const { user, logout } = useAuth();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const navCrumb = (() => {
|
||||
|
||||
for (const g of adminNavGroups) {
|
||||
|
||||
for (const it of g.items) {
|
||||
|
||||
const match = it.exact
|
||||
|
||||
? location.pathname === it.path
|
||||
|
||||
: location.pathname === it.path || location.pathname.startsWith(it.path + '/');
|
||||
|
||||
if (match) return { group: g.title, label: it.label };
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return { group: '', label: '' };
|
||||
|
||||
})();
|
||||
|
||||
const navGroup = navCrumb.group;
|
||||
|
||||
const navLabel = navCrumb.label;
|
||||
|
||||
// Default: all groups collapsed. Auto-expand the group containing the active route.
|
||||
|
||||
|
||||
// 默认展开全部功能板块,确保后台所有模块在 UI 中可见(DESIGN.md: 侧栏分类清晰)
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(() => {
|
||||
const initial = new Set<string>();
|
||||
const currentPath = location.pathname;
|
||||
for (const group of adminNavGroups) {
|
||||
const isActive = group.items.some(item =>
|
||||
item.exact ? currentPath === item.path : currentPath.startsWith(item.path)
|
||||
);
|
||||
if (isActive) {
|
||||
initial.add(group.title);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return initial;
|
||||
return new Set(adminNavGroups.map((g) => g.title));
|
||||
});
|
||||
|
||||
|
||||
|
||||
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.open(url, '_blank');
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className="admin-layout">
|
||||
|
||||
<aside className="admin-sidebar">
|
||||
|
||||
<div className="admin-sidebar-header">
|
||||
|
||||
<h1 className="admin-logo">七虎AI</h1>
|
||||
|
||||
<span className="admin-subtitle">管理控制台</span>
|
||||
|
||||
</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 ? location.pathname === item.path : location.pathname.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>
|
||||
|
||||
<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>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<main className="admin-content">
|
||||
|
||||
<header className="admin-topbar">
|
||||
|
||||
<nav className="admin-breadcrumb" aria-label="breadcrumb">
|
||||
|
||||
<span className="crumb-root">管理控制台</span>
|
||||
|
||||
{navGroup && (<><span className="crumb-sep">/</span><span className="crumb-group">{navGroup}</span></>)}
|
||||
|
||||
{navLabel && (<><span className="crumb-sep">/</span><span className="crumb-current">{navLabel}</span></>)}
|
||||
|
||||
</nav>
|
||||
|
||||
<div className="admin-topbar-user">
|
||||
|
||||
<div className="admin-user-avatar sm">{user?.username?.[0]?.toUpperCase() || 'A'}</div>
|
||||
|
||||
<span className="admin-topbar-username">{user?.username || 'admin'}</span>
|
||||
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<Outlet />
|
||||
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default AdminLayout;
|
||||
|
||||
Reference in new issue
Block a user