feat(admin): Admin V2 — Ant Design Pro 纯 SPA 重写
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Next.js SSR/hydration 与 SWR fetch-on-mount 存在根本冲突: hydration 卸载组件时 abort 的请求仍占用后端 DB 连接, retry 循环耗尽 PostgreSQL 连接池导致后端完全卡死。 admin-v2 使用 Vite + React + antd 纯 SPA 彻底消除此问题: - 12 页面全部完成(Login, Dashboard, Accounts, Providers, Models, API Keys, Usage, Relay, Config, Prompts, Logs, Agent Templates) - ProTable + ProForm + ProLayout 统一 UI 模式 - TanStack Query + Axios + Zustand 数据层 - JWT 自动刷新 + 401 重试机制 - 全部 18 网络请求 200 OK,零 ERR_ABORTED 同时更新 troubleshooting 第 13 节和 SaaS 平台文档。
This commit is contained in:
103
admin-v2/src/layouts/AdminLayout.tsx
Normal file
103
admin-v2/src/layouts/AdminLayout.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
// ============================================================
|
||||
// AdminLayout — ProLayout 管理后台布局
|
||||
// ============================================================
|
||||
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
|
||||
import ProLayout from '@ant-design/pro-layout'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
TeamOutlined,
|
||||
CloudServerOutlined,
|
||||
ApiOutlined,
|
||||
KeyOutlined,
|
||||
BarChartOutlined,
|
||||
SwapOutlined,
|
||||
SettingOutlined,
|
||||
FileTextOutlined,
|
||||
MessageOutlined,
|
||||
RobotOutlined,
|
||||
LogoutOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { Avatar, Dropdown, message } from 'antd'
|
||||
import type { MenuDataItem } from '@ant-design/pro-layout'
|
||||
|
||||
const menuConfig: MenuDataItem[] = [
|
||||
{ path: '/', name: '仪表盘', icon: <DashboardOutlined /> },
|
||||
{ path: '/accounts', name: '账号管理', icon: <TeamOutlined />, permission: 'account:admin' },
|
||||
{ path: '/providers', name: '服务商', icon: <CloudServerOutlined />, permission: 'provider:manage' },
|
||||
{ path: '/models', name: '模型管理', icon: <ApiOutlined />, permission: 'model:read' },
|
||||
{ path: '/agent-templates', name: 'Agent 模板', icon: <RobotOutlined />, permission: 'model:read' },
|
||||
{ path: '/api-keys', name: 'API 密钥', icon: <KeyOutlined />, permission: 'admin:full' },
|
||||
{ path: '/usage', name: '用量统计', icon: <BarChartOutlined />, permission: 'admin:full' },
|
||||
{ path: '/relay', name: '中转任务', icon: <SwapOutlined />, permission: 'relay:use' },
|
||||
{ path: '/config', name: '系统配置', icon: <SettingOutlined />, permission: 'config:read' },
|
||||
{ path: '/prompts', name: '提示词管理', icon: <MessageOutlined />, permission: 'prompt:read' },
|
||||
{ path: '/logs', name: '操作日志', icon: <FileTextOutlined />, permission: 'admin:full' },
|
||||
]
|
||||
|
||||
function filterMenuByPermission(
|
||||
items: MenuDataItem[],
|
||||
hasPermission: (p: string) => boolean,
|
||||
): MenuDataItem[] {
|
||||
return items
|
||||
.filter((item) => !item.permission || hasPermission(item.permission as string))
|
||||
.map(({ permission, ...rest }) => ({
|
||||
...rest,
|
||||
children: rest.children ? filterMenuByPermission(rest.children, hasPermission) : undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
export default function AdminLayout() {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { account, hasPermission, logout } = useAuthStore()
|
||||
|
||||
const menuData = filterMenuByPermission(menuConfig, hasPermission)
|
||||
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
message.success('已退出登录')
|
||||
navigate('/login', { replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<ProLayout
|
||||
title="ZCLAW"
|
||||
logo={<div style={{ width: 28, height: 28, background: '#1677ff', borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: 700, fontSize: 14 }}>Z</div>}
|
||||
layout="mix"
|
||||
fixSiderbar
|
||||
fixedHeader
|
||||
location={{ pathname: location.pathname }}
|
||||
menuDataRender={() => menuData}
|
||||
menuItemRender={(item, dom) => (
|
||||
<div onClick={() => item.path && navigate(item.path)}>{dom}</div>
|
||||
)}
|
||||
avatarProps={{
|
||||
src: undefined,
|
||||
title: account?.display_name || account?.username || 'Admin',
|
||||
size: 'small',
|
||||
render: (_, dom) => (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined />,
|
||||
label: '退出登录',
|
||||
onClick: handleLogout,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{dom}
|
||||
</Dropdown>
|
||||
),
|
||||
}}
|
||||
suppressSiderWhenMenuEmpty
|
||||
contentStyle={{ padding: 24 }}
|
||||
>
|
||||
<Outlet />
|
||||
</ProLayout>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user