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
refactor(saas): 重构认证中间件与限流策略
- 登录限流调整为5次/分钟/IP
- 注册限流调整为3次/小时/IP
- GET请求不计入限流
fix(saas): 修复调度器时间戳处理
- 使用NOW()替代文本时间戳
- 兼容TEXT和TIMESTAMPTZ列类型
feat(saas): 实现环境变量插值
- 支持${ENV_VAR}语法解析
- 数据库密码支持环境变量注入
chore: 新增前端管理界面
- 基于React+Ant Design Pro
- 包含路由守卫/错误边界
- 对接58个API端点
docs: 更新安全加固文档
- 新增密钥管理规范
- 记录P0安全项审计结果
- 补充TLS终止说明
test: 完善配置解析单元测试
- 新增环境变量插值测试用例
122 lines
4.3 KiB
TypeScript
122 lines
4.3 KiB
TypeScript
// ============================================================
|
|
// 仪表盘页面
|
|
// ============================================================
|
|
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { Card, Col, Row, Statistic, Table, Tag, Typography, Spin, Alert } from 'antd'
|
|
import {
|
|
TeamOutlined,
|
|
CloudServerOutlined,
|
|
ApiOutlined,
|
|
ThunderboltOutlined,
|
|
ColumnWidthOutlined,
|
|
} from '@ant-design/icons'
|
|
import { statsService } from '@/services/stats'
|
|
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',
|
|
}
|
|
|
|
export default function Dashboard() {
|
|
const { data: stats, isLoading: statsLoading, error: statsError } = useQuery({
|
|
queryKey: ['dashboard-stats'],
|
|
queryFn: ({ signal }) => statsService.dashboard(signal),
|
|
})
|
|
|
|
const { data: logsData, isLoading: logsLoading } = useQuery({
|
|
queryKey: ['recent-logs'],
|
|
queryFn: ({ signal }) => logService.list({ page: 1, page_size: 10 }, signal),
|
|
})
|
|
|
|
if (statsError) {
|
|
return <Alert type="error" message="加载仪表盘数据失败" description={(statsError as Error).message} showIcon />
|
|
}
|
|
|
|
const statCards = [
|
|
{ title: '总账号', value: stats?.total_accounts ?? 0, icon: <TeamOutlined />, color: '#1677ff' },
|
|
{ title: '活跃服务商', value: stats?.active_providers ?? 0, icon: <CloudServerOutlined />, color: '#52c41a' },
|
|
{ title: '活跃模型', value: stats?.active_models ?? 0, icon: <ApiOutlined />, color: '#722ed1' },
|
|
{ title: '今日请求', value: stats?.tasks_today ?? 0, icon: <ThunderboltOutlined />, color: '#fa8c16' },
|
|
{ title: '今日 Token', value: ((stats?.tokens_today_input ?? 0) + (stats?.tokens_today_output ?? 0)), icon: <ColumnWidthOutlined />, color: '#eb2f96' },
|
|
]
|
|
|
|
const logColumns = [
|
|
{
|
|
title: '操作类型',
|
|
dataIndex: 'action',
|
|
key: 'action',
|
|
width: 140,
|
|
render: (action: string) => (
|
|
<Tag color={actionColors[action] || 'default'}>
|
|
{actionLabels[action] || action}
|
|
</Tag>
|
|
),
|
|
},
|
|
{ title: '目标类型', dataIndex: 'target_type', key: 'target_type', width: 100, render: (v: string | null) => v || '-' },
|
|
{
|
|
title: '时间',
|
|
dataIndex: 'created_at',
|
|
key: 'created_at',
|
|
width: 180,
|
|
render: (v: string) => new Date(v).toLocaleString('zh-CN'),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<div>
|
|
<Title level={4} style={{ marginBottom: 24 }}>仪表盘</Title>
|
|
|
|
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
|
{statsLoading ? (
|
|
<Col span={24}><Spin /></Col>
|
|
) : (
|
|
statCards.map((card) => (
|
|
<Col xs={24} sm={12} md={8} lg={4} key={card.title}>
|
|
<Card>
|
|
<Statistic
|
|
title={card.title}
|
|
value={card.value}
|
|
prefix={<span style={{ color: card.color }}>{card.icon}</span>}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
))
|
|
)}
|
|
</Row>
|
|
|
|
<Card title="最近操作日志" size="small">
|
|
<Table<OperationLog>
|
|
columns={logColumns}
|
|
dataSource={logsData?.items ?? []}
|
|
loading={logsLoading}
|
|
rowKey="id"
|
|
pagination={false}
|
|
size="small"
|
|
/>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|