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
1. App.tsx: add restoreSession() call on startup to prevent redirect to login page after refresh (isRestoring guard + BootstrapScreen) 2. CloneManager: call syncAgents() after loadClones() to restore currentAgent and conversation history on app load 3. zclaw-memory: add get_or_create_session() so frontend session UUID is persisted directly — kernel no longer creates mismatched IDs 4. openai.rs: assistant message content must be non-empty for Kimi/Qwen APIs — replace empty content with meaningful placeholders Also includes admin-v2 ModelServices unified page (merge providers + models + API keys into expandable row layout)
149 lines
4.4 KiB
TypeScript
149 lines
4.4 KiB
TypeScript
// ============================================================
|
|
// 仪表盘页面
|
|
// ============================================================
|
|
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { Card, Col, Row, Statistic, Table, Tag, Spin } from 'antd'
|
|
import {
|
|
TeamOutlined,
|
|
CloudServerOutlined,
|
|
ApiOutlined,
|
|
ThunderboltOutlined,
|
|
ColumnWidthOutlined,
|
|
} from '@ant-design/icons'
|
|
import { statsService } from '@/services/stats'
|
|
import { logService } from '@/services/logs'
|
|
import { PageHeader } from '@/components/PageHeader'
|
|
import { ErrorState } from '@/components/ErrorState'
|
|
import { actionLabels, actionColors } from '@/constants/status'
|
|
import type { OperationLog } from '@/types'
|
|
|
|
export default function Dashboard() {
|
|
const {
|
|
data: stats,
|
|
isLoading: statsLoading,
|
|
error: statsError,
|
|
refetch: refetchStats,
|
|
} = 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 (
|
|
<>
|
|
<PageHeader title="仪表盘" description="系统概览与最近活动" />
|
|
<ErrorState
|
|
message={(statsError as Error).message}
|
|
onRetry={() => refetchStats()}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const statCards = [
|
|
{ title: '总账号', value: stats?.total_accounts ?? 0, icon: <TeamOutlined />, color: '#863bff' },
|
|
{ title: '活跃服务商', value: stats?.active_providers ?? 0, icon: <CloudServerOutlined />, color: '#47bfff' },
|
|
{ title: '活跃模型', value: stats?.active_models ?? 0, icon: <ApiOutlined />, color: '#22c55e' },
|
|
{ title: '今日请求', value: stats?.tasks_today ?? 0, icon: <ThunderboltOutlined />, color: '#f59e0b' },
|
|
{
|
|
title: '今日 Token',
|
|
value: (stats?.tokens_today_input ?? 0) + (stats?.tokens_today_output ?? 0),
|
|
icon: <ColumnWidthOutlined />,
|
|
color: '#ef4444',
|
|
},
|
|
]
|
|
|
|
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>
|
|
<PageHeader title="仪表盘" description="系统概览与最近活动" />
|
|
|
|
{/* Stat Cards */}
|
|
<Row gutter={[16, 16]} className="mb-6">
|
|
{statsLoading ? (
|
|
<Col span={24}>
|
|
<div className="flex justify-center py-8">
|
|
<Spin size="large" />
|
|
</div>
|
|
</Col>
|
|
) : (
|
|
statCards.map((card) => (
|
|
<Col xs={24} sm={12} md={8} lg={4} key={card.title}>
|
|
<Card
|
|
className="hover:shadow-md transition-shadow duration-200"
|
|
styles={{ body: { padding: '20px 24px' } }}
|
|
>
|
|
<Statistic
|
|
title={
|
|
<span className="text-neutral-500 dark:text-neutral-400 text-xs font-medium uppercase tracking-wide">
|
|
{card.title}
|
|
</span>
|
|
}
|
|
value={card.value}
|
|
valueStyle={{ fontSize: 28, fontWeight: 600, color: card.color }}
|
|
prefix={
|
|
<span style={{ color: card.color, marginRight: 4 }}>{card.icon}</span>
|
|
}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
))
|
|
)}
|
|
</Row>
|
|
|
|
{/* Recent Logs */}
|
|
<Card
|
|
title={
|
|
<span className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
|
|
最近操作日志
|
|
</span>
|
|
}
|
|
size="small"
|
|
styles={{ body: { padding: 0 } }}
|
|
>
|
|
<Table<OperationLog>
|
|
columns={logColumns}
|
|
dataSource={logsData?.items ?? []}
|
|
loading={logsLoading}
|
|
rowKey="id"
|
|
pagination={false}
|
|
size="small"
|
|
/>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|