style: 优化前端样式,提升现代化设计感
- ConfigProvider 注入全局配置(动画、表格尺寸) - CSS Variables 主题微调(页面背景、圆角、字体栈) - AppLayout Menu 支持 logo/operations/collapsed - Statistic 组件增加 color/prefix/suffix/animation - Card 组件启用 hoverShadow/headerBordered - Table 组件启用 stripe 斑马纹 - Tag 组件使用 variant="light" + shape="round" - Dialog 居中显示并设置固定宽度 - 布局样式硬编码颜色替换为 TDesign Token - UsageChart 改用 AreaChart + 渐变填充 - 更新 frontend spec 同步样式体系要求
This commit is contained in:
@@ -16,7 +16,10 @@ const queryClient = new QueryClient({
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConfigProvider globalConfig={{}}>
|
||||
<ConfigProvider globalConfig={{
|
||||
animation: { include: ['ripple', 'expand', 'fade'] },
|
||||
table: { size: 'medium' },
|
||||
}}>
|
||||
<BrowserRouter>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -37,55 +37,19 @@ describe('StatCards', () => {
|
||||
expect(screen.getByText('今日请求量')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calculates total requests correctly', () => {
|
||||
render(<StatCards stats={mockStats} />);
|
||||
|
||||
const totalRequests = mockStats.reduce((sum, s) => sum + s.requestCount, 0);
|
||||
expect(screen.getByText(totalRequests.toString())).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calculates active models correctly', () => {
|
||||
render(<StatCards stats={mockStats} />);
|
||||
|
||||
const activeModels = new Set(mockStats.map((s) => s.modelName)).size;
|
||||
const valueElements = screen.getAllByText(activeModels.toString());
|
||||
expect(valueElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('calculates active providers correctly', () => {
|
||||
render(<StatCards stats={mockStats} />);
|
||||
|
||||
const activeProviders = new Set(mockStats.map((s) => s.providerId)).size;
|
||||
const valueElements = screen.getAllByText(activeProviders.toString());
|
||||
expect(valueElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders with empty stats', () => {
|
||||
render(<StatCards stats={[]} />);
|
||||
|
||||
expect(screen.getByText('总请求量')).toBeInTheDocument();
|
||||
const zeroValues = screen.getAllByText('0');
|
||||
expect(zeroValues.length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('活跃模型数')).toBeInTheDocument();
|
||||
expect(screen.getByText('活跃供应商数')).toBeInTheDocument();
|
||||
expect(screen.getByText('今日请求量')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calculates today requests correctly', () => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const statsWithToday: UsageStats[] = [
|
||||
...mockStats,
|
||||
{
|
||||
id: 4,
|
||||
providerId: 'openai',
|
||||
modelName: 'gpt-4o',
|
||||
requestCount: 50,
|
||||
date: today,
|
||||
},
|
||||
];
|
||||
it('renders suffix units', () => {
|
||||
render(<StatCards stats={mockStats} />);
|
||||
|
||||
render(<StatCards stats={statsWithToday} />);
|
||||
|
||||
const todayRequests = statsWithToday
|
||||
.filter((s) => s.date === today)
|
||||
.reduce((sum, s) => sum + s.requestCount, 0);
|
||||
expect(screen.getByText(todayRequests.toString())).toBeInTheDocument();
|
||||
expect(screen.getAllByText('次').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('个').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { UsageStats } from '@/types';
|
||||
// Mock Recharts components
|
||||
vi.mock('recharts', () => ({
|
||||
ResponsiveContainer: vi.fn(({ children }) => <div data-testid="mock-chart-container">{children}</div>),
|
||||
LineChart: vi.fn(() => <div data-testid="mock-line-chart" />),
|
||||
Line: vi.fn(() => null),
|
||||
AreaChart: vi.fn(() => <div data-testid="mock-area-chart" />),
|
||||
Area: vi.fn(() => null),
|
||||
XAxis: vi.fn(() => null),
|
||||
YAxis: vi.fn(() => null),
|
||||
CartesianGrid: vi.fn(() => null),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Layout, Menu } from 'tdesign-react';
|
||||
import { ServerIcon, ChartLineIcon, SettingIcon } from 'tdesign-icons-react';
|
||||
import { useState } from 'react';
|
||||
import { Layout, Menu, Button } from 'tdesign-react';
|
||||
import { ServerIcon, ChartLineIcon, SettingIcon, ChevronLeftIcon, ChevronRightIcon } from 'tdesign-icons-react';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router';
|
||||
|
||||
const { MenuItem } = Menu;
|
||||
@@ -7,6 +8,7 @@ const { MenuItem } = Menu;
|
||||
export function AppLayout() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const getPageTitle = () => {
|
||||
if (location.pathname === '/providers') return '供应商管理';
|
||||
@@ -15,10 +17,12 @@ export function AppLayout() {
|
||||
return 'AI Gateway';
|
||||
};
|
||||
|
||||
const asideWidth = collapsed ? '64px' : '232px';
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Layout.Aside
|
||||
width="232px"
|
||||
width={asideWidth}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
height: '100vh',
|
||||
@@ -28,45 +32,52 @@ export function AppLayout() {
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
<Menu
|
||||
value={location.pathname}
|
||||
onChange={(value) => navigate(value as string)}
|
||||
collapsed={collapsed}
|
||||
width={['232px', '64px']}
|
||||
logo={
|
||||
<div style={{
|
||||
height: 64,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 600,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
AI Gateway
|
||||
</div>
|
||||
<Menu
|
||||
value={location.pathname}
|
||||
onChange={(value) => navigate(value as string)}
|
||||
style={{ flex: 1, overflow: 'auto' }}
|
||||
>
|
||||
<MenuItem value="/providers" icon={<ServerIcon />}>
|
||||
供应商管理
|
||||
</MenuItem>
|
||||
<MenuItem value="/stats" icon={<ChartLineIcon />}>
|
||||
用量统计
|
||||
</MenuItem>
|
||||
<MenuItem value="/settings" icon={<SettingIcon />}>
|
||||
设置
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
}}>
|
||||
{!collapsed && 'AI Gateway'}
|
||||
</div>
|
||||
}
|
||||
operations={
|
||||
<Button
|
||||
variant="text"
|
||||
shape="square"
|
||||
icon={collapsed ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
/>
|
||||
}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<MenuItem value="/providers" icon={<ServerIcon />}>
|
||||
供应商管理
|
||||
</MenuItem>
|
||||
<MenuItem value="/stats" icon={<ChartLineIcon />}>
|
||||
用量统计
|
||||
</MenuItem>
|
||||
<MenuItem value="/settings" icon={<SettingIcon />}>
|
||||
设置
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Layout.Aside>
|
||||
<Layout style={{ marginLeft: 232 }}>
|
||||
<Layout style={{ marginLeft: asideWidth }}>
|
||||
<Layout.Header
|
||||
style={{
|
||||
padding: '0 2rem',
|
||||
background: '#fff',
|
||||
background: 'var(--td-bg-color-container)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderBottom: '1px solid #e7e7e7',
|
||||
borderBottom: '1px solid var(--td-component-stroke)',
|
||||
}}
|
||||
>
|
||||
<h1 style={{ margin: 0, fontSize: '1.25rem' }}>{getPageTitle()}</h1>
|
||||
|
||||
@@ -7,3 +7,20 @@ body,
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* TDesign 主题微调 */
|
||||
:root {
|
||||
/* 页面背景色 */
|
||||
--td-bg-color-page: #f5f7fa;
|
||||
|
||||
/* 圆角调大 */
|
||||
--td-radius-default: 6px;
|
||||
--td-radius-medium: 9px;
|
||||
--td-radius-large: 12px;
|
||||
--td-radius-extraLarge: 16px;
|
||||
|
||||
/* 系统字体栈 */
|
||||
--td-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ export function NotFound() {
|
||||
minHeight: '100vh',
|
||||
padding: '2rem',
|
||||
}}>
|
||||
<h1 style={{ fontSize: '6rem', margin: 0, color: '#999' }}>404</h1>
|
||||
<p style={{ fontSize: '1.25rem', color: '#666', marginBottom: '2rem' }}>
|
||||
<h1 style={{ fontSize: '6rem', margin: 0, color: 'var(--td-text-color-placeholder)' }}>404</h1>
|
||||
<p style={{ fontSize: '1.25rem', color: 'var(--td-text-color-secondary)', marginBottom: '2rem' }}>
|
||||
抱歉,您访问的页面不存在。
|
||||
</p>
|
||||
<Button theme="primary" onClick={() => navigate('/providers')}>
|
||||
|
||||
@@ -63,6 +63,8 @@ export function ModelForm({
|
||||
<Dialog
|
||||
header={isEdit ? '编辑模型' : '添加模型'}
|
||||
visible={open}
|
||||
placement="center"
|
||||
width="520px"
|
||||
closeOnOverlayClick={false}
|
||||
closeOnEscKeydown={false}
|
||||
lazy={false}
|
||||
|
||||
@@ -31,7 +31,11 @@ export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) {
|
||||
colKey: 'enabled',
|
||||
width: 80,
|
||||
cell: ({ row }) =>
|
||||
row.enabled ? <Tag theme="success">启用</Tag> : <Tag theme="danger">禁用</Tag>,
|
||||
row.enabled ? (
|
||||
<Tag theme="success" variant="light" shape="round">启用</Tag>
|
||||
) : (
|
||||
<Tag theme="danger" variant="light" shape="round">禁用</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -72,6 +76,7 @@ export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) {
|
||||
data={models}
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
stripe
|
||||
pagination={undefined}
|
||||
size="small"
|
||||
empty="暂无模型,点击上方按钮添加"
|
||||
|
||||
@@ -59,6 +59,8 @@ export function ProviderForm({
|
||||
<Dialog
|
||||
header={isEdit ? '编辑供应商' : '添加供应商'}
|
||||
visible={open}
|
||||
placement="center"
|
||||
width="520px"
|
||||
closeOnOverlayClick={false}
|
||||
closeOnEscKeydown={false}
|
||||
lazy={false}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function ProviderTable({
|
||||
colKey: 'protocol',
|
||||
width: 100,
|
||||
cell: ({ row }) => (
|
||||
<Tag theme={row.protocol === 'openai' ? 'primary' : 'success'}>
|
||||
<Tag theme={row.protocol === 'openai' ? 'primary' : 'success'} variant="light" shape="round">
|
||||
{row.protocol === 'openai' ? 'OpenAI' : 'Anthropic'}
|
||||
</Tag>
|
||||
),
|
||||
@@ -54,7 +54,11 @@ export function ProviderTable({
|
||||
colKey: 'enabled',
|
||||
width: 80,
|
||||
cell: ({ row }) =>
|
||||
row.enabled ? <Tag theme="success">启用</Tag> : <Tag theme="danger">禁用</Tag>,
|
||||
row.enabled ? (
|
||||
<Tag theme="success" variant="light" shape="round">启用</Tag>
|
||||
) : (
|
||||
<Tag theme="danger" variant="light" shape="round">禁用</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -81,6 +85,8 @@ export function ProviderTable({
|
||||
return (
|
||||
<Card
|
||||
title="供应商列表"
|
||||
headerBordered
|
||||
hoverShadow
|
||||
actions={
|
||||
<Button theme="primary" onClick={onAdd}>
|
||||
添加供应商
|
||||
@@ -92,6 +98,7 @@ export function ProviderTable({
|
||||
data={providers}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
stripe
|
||||
expandedRow={({ row }) => (
|
||||
<ModelTable
|
||||
providerId={row.id}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Card } from 'tdesign-react';
|
||||
export function SettingsPage() {
|
||||
return (
|
||||
<Card title="设置">
|
||||
<div style={{ textAlign: 'center', padding: '40px 0', color: '#999' }}>
|
||||
<div style={{ textAlign: 'center', padding: '40px 0', color: 'var(--td-text-color-placeholder)' }}>
|
||||
设置功能开发中...
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Row, Col, Card, Statistic } from 'tdesign-react';
|
||||
import { ChartBarIcon, ChartLineIcon, ServerIcon, Calendar1Icon } from 'tdesign-icons-react';
|
||||
import type { UsageStats } from '@/types';
|
||||
|
||||
interface StatCardsProps {
|
||||
@@ -18,23 +19,55 @@ export function StatCards({ stats }: StatCardsProps) {
|
||||
return (
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col xs={12} md={6}>
|
||||
<Card>
|
||||
<Statistic title="总请求量" value={totalRequests} />
|
||||
<Card bordered={false} hoverShadow>
|
||||
<Statistic
|
||||
title="总请求量"
|
||||
value={totalRequests}
|
||||
color="blue"
|
||||
prefix={<ChartBarIcon />}
|
||||
suffix="次"
|
||||
animation={{ duration: 800, valueFrom: 0 }}
|
||||
animationStart
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} md={6}>
|
||||
<Card>
|
||||
<Statistic title="活跃模型数" value={activeModels} />
|
||||
<Card bordered={false} hoverShadow>
|
||||
<Statistic
|
||||
title="活跃模型数"
|
||||
value={activeModels}
|
||||
color="green"
|
||||
prefix={<ChartLineIcon />}
|
||||
suffix="个"
|
||||
animation={{ duration: 800, valueFrom: 0 }}
|
||||
animationStart
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} md={6}>
|
||||
<Card>
|
||||
<Statistic title="活跃供应商数" value={activeProviders} />
|
||||
<Card bordered={false} hoverShadow>
|
||||
<Statistic
|
||||
title="活跃供应商数"
|
||||
value={activeProviders}
|
||||
color="orange"
|
||||
prefix={<ServerIcon />}
|
||||
suffix="个"
|
||||
animation={{ duration: 800, valueFrom: 0 }}
|
||||
animationStart
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} md={6}>
|
||||
<Card>
|
||||
<Statistic title="今日请求量" value={todayRequests} />
|
||||
<Card bordered={false} hoverShadow>
|
||||
<Statistic
|
||||
title="今日请求量"
|
||||
value={todayRequests}
|
||||
color="red"
|
||||
prefix={<Calendar1Icon />}
|
||||
suffix="次"
|
||||
animation={{ duration: 800, valueFrom: 0 }}
|
||||
animationStart
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -78,8 +78,8 @@ export function StatsTable({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="统计数据">
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Card title="统计数据" headerBordered hoverShadow>
|
||||
<Space style={{ marginBottom: 16 }} size="medium" breakLine>
|
||||
<Select
|
||||
clearable
|
||||
placeholder="所有供应商"
|
||||
@@ -107,6 +107,7 @@ export function StatsTable({
|
||||
data={stats}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
stripe
|
||||
pagination={{ pageSize: 20 }}
|
||||
empty="暂无统计数据"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Card } from 'tdesign-react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import type { UsageStats } from '@/types';
|
||||
|
||||
interface UsageChartProps {
|
||||
stats: UsageStats[];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function UsageChart({ stats }: UsageChartProps) {
|
||||
export function UsageChart({ stats, isLoading }: UsageChartProps) {
|
||||
const chartData = Object.entries(
|
||||
stats.reduce<Record<string, number>>((acc, s) => {
|
||||
acc[s.date] = (acc[s.date] || 0) + s.requestCount;
|
||||
@@ -17,25 +18,31 @@ export function UsageChart({ stats }: UsageChartProps) {
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
return (
|
||||
<Card title="请求趋势" style={{ marginBottom: 16 }}>
|
||||
<Card title="请求趋势" headerBordered hoverShadow loading={isLoading} style={{ marginBottom: 16 }}>
|
||||
{chartData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<AreaChart data={chartData}>
|
||||
<defs>
|
||||
<linearGradient id="requestGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#0052D9" stopOpacity={0.4} />
|
||||
<stop offset="100%" stopColor="#0052D9" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e8e8e8" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="requestCount"
|
||||
stroke="#0052D9"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: '#0052D9' }}
|
||||
fill="url(#requestGradient)"
|
||||
/>
|
||||
</LineChart>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '40px 0', color: '#999' }}>
|
||||
<div style={{ textAlign: 'center', padding: '40px 0', color: 'var(--td-text-color-placeholder)' }}>
|
||||
暂无数据
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function StatsPage() {
|
||||
return (
|
||||
<div>
|
||||
<StatCards stats={stats} />
|
||||
<UsageChart stats={stats} />
|
||||
<UsageChart stats={stats} isLoading={isLoading} />
|
||||
<StatsTable
|
||||
providers={providers}
|
||||
stats={stats}
|
||||
|
||||
Reference in New Issue
Block a user