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 平台文档。
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
// ============================================================
|
|
// 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>
|
|
)
|
|
}
|