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

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:
iven
2026-03-30 09:35:59 +08:00
parent 13c0b18bbc
commit a7d33d0207
52 changed files with 8102 additions and 118 deletions

112
admin-v2/src/pages/Logs.tsx Normal file
View File

@@ -0,0 +1,112 @@
// ============================================================
// 操作日志
// ============================================================
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Tag, Select, Typography } from 'antd'
import type { ProColumns } from '@ant-design/pro-components'
import { ProTable } from '@ant-design/pro-components'
import { logService } from '@/services/logs'
import type { OperationLog } from '@/types'
const { Title } = Typography
const actionLabels: Record<string, string> = {
login: '登录', logout: '登出',
create_account: '创建账号', update_account: '更新账号', delete_account: '删除账号',
create_provider: '创建服务商', update_provider: '更新服务商', delete_provider: '删除服务商',
create_model: '创建模型', update_model: '更新模型', delete_model: '删除模型',
create_token: '创建密钥', revoke_token: '撤销密钥',
update_config: '更新配置',
create_prompt: '创建提示词', update_prompt: '更新提示词', archive_prompt: '归档提示词',
desktop_audit: '桌面端审计',
}
const actionColors: Record<string, string> = {
login: 'green', logout: 'default',
create_account: 'blue', update_account: 'orange', delete_account: 'red',
create_provider: 'blue', update_provider: 'orange', delete_provider: 'red',
create_model: 'blue', update_model: 'orange', delete_model: 'red',
create_token: 'blue', revoke_token: 'red',
update_config: 'orange',
create_prompt: 'blue', update_prompt: 'orange', archive_prompt: 'red',
desktop_audit: 'default',
}
const actionOptions = Object.entries(actionLabels).map(([value, label]) => ({ value, label }))
export default function Logs() {
const [page, setPage] = useState(1)
const [actionFilter, setActionFilter] = useState<string | undefined>(undefined)
const { data, isLoading } = useQuery({
queryKey: ['logs', page, actionFilter],
queryFn: () => logService.list({ page, page_size: 20, action: actionFilter }),
})
const columns: ProColumns<OperationLog>[] = [
{
title: '操作类型',
dataIndex: 'action',
width: 140,
render: (_, r) => (
<Tag color={actionColors[r.action] || 'default'}>
{actionLabels[r.action] || r.action}
</Tag>
),
},
{ title: '目标类型', dataIndex: 'target_type', width: 100, render: (_, r) => r.target_type || '-' },
{ title: '目标 ID', dataIndex: 'target_id', width: 120, render: (_, r) => r.target_id ? <code>{r.target_id.substring(0, 8)}...</code> : '-' },
{
title: '详情',
dataIndex: 'details',
width: 250,
ellipsis: true,
render: (_, r) => {
if (!r.details) return '-'
if (typeof r.details === 'string') return r.details
return JSON.stringify(r.details)
},
},
{ title: 'IP 地址', dataIndex: 'ip_address', width: 130, render: (_, r) => <code>{r.ip_address || '-'}</code> },
{
title: '时间',
dataIndex: 'created_at',
width: 180,
render: (_, r) => new Date(r.created_at).toLocaleString('zh-CN'),
},
]
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<Title level={4} style={{ margin: 0 }}></Title>
<Select
value={actionFilter}
onChange={(v) => { setActionFilter(v === 'all' ? undefined : v); setPage(1) }}
placeholder="操作类型筛选"
style={{ width: 160 }}
allowClear
options={[{ value: 'all', label: '全部操作' }, ...actionOptions]}
/>
</div>
<ProTable<OperationLog>
columns={columns}
dataSource={data?.items ?? []}
loading={isLoading}
rowKey="id"
search={false}
toolBarRender={false}
pagination={{
total: data?.total ?? 0,
pageSize: 20,
current: page,
onChange: setPage,
showSizeChanger: false,
}}
/>
</div>
)
}