From c5b3d9dfc716a2ac38b70d53ebe7d41c457d4b33 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Thu, 16 Apr 2026 22:59:26 +0800 Subject: [PATCH] =?UTF-8?q?style:=20=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=20UI=20=E7=BB=86=E8=8A=82=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 供应商/模型/统计表格增加列宽约束和 ellipsis 省略号 - 修复主题切换按钮在暗色侧边栏中不可见 - 表格添加 scroll 属性防止窄屏溢出 - 自定义表格空状态提示文案 --- frontend/e2e/stats-cards.spec.ts | 2 +- .../__tests__/components/AppLayout.test.tsx | 23 ++++++++++-- .../components/ProviderTable.test.tsx | 11 ++++++ .../__tests__/components/StatsTable.test.tsx | 17 +++++---- frontend/src/components/ThemeToggle/index.tsx | 2 +- frontend/src/pages/Providers/ModelTable.tsx | 2 ++ .../src/pages/Providers/ProviderTable.tsx | 15 ++++++-- frontend/src/pages/Stats/StatsTable.tsx | 9 +++++ openspec/specs/frontend-config-ui/spec.md | 35 +++++++++++++++++++ openspec/specs/theme-toggle/spec.md | 2 ++ 10 files changed, 106 insertions(+), 12 deletions(-) diff --git a/frontend/e2e/stats-cards.spec.ts b/frontend/e2e/stats-cards.spec.ts index 6311c6a..41586be 100644 --- a/frontend/e2e/stats-cards.spec.ts +++ b/frontend/e2e/stats-cards.spec.ts @@ -30,7 +30,7 @@ test.describe('统计摘要卡片', () => { }); test('应显示筛选栏', async ({ page }) => { - await expect(page.getByPlaceholder('所有供应商')).toBeVisible(); + await expect(page.getByText('所有供应商')).toBeVisible(); await expect(page.getByPlaceholder('模型名称')).toBeVisible(); }); }); diff --git a/frontend/src/__tests__/components/AppLayout.test.tsx b/frontend/src/__tests__/components/AppLayout.test.tsx index eea7731..9a51151 100644 --- a/frontend/src/__tests__/components/AppLayout.test.tsx +++ b/frontend/src/__tests__/components/AppLayout.test.tsx @@ -3,8 +3,11 @@ import { describe, it, expect, vi } from 'vitest'; import { BrowserRouter } from 'react-router'; import { AppLayout } from '@/components/AppLayout'; +const mockToggleTheme = vi.fn(); +const mockSetTheme = vi.fn(); + vi.mock('@/contexts/ThemeContext', () => ({ - useTheme: vi.fn(() => ({ mode: 'light', toggleTheme: vi.fn() })), + useTheme: vi.fn(() => ({ mode: 'light', toggleTheme: mockToggleTheme, setTheme: mockSetTheme })), })); const renderWithRouter = (component: React.ReactNode) => { @@ -26,11 +29,27 @@ describe('AppLayout', () => { expect(screen.getByText('用量统计')).toBeInTheDocument(); }); - it('renders theme toggle button', () => { + it('renders theme toggle button with visible color in sidebar', () => { renderWithRouter(); const themeButton = screen.getByRole('button', { name: 'moon' }); expect(themeButton).toBeInTheDocument(); + expect(themeButton.style.color).toBe('rgba(255, 255, 255, 0.85)'); + }); + + it('renders theme toggle button visible in dark mode', async () => { + const { useTheme } = await import('@/contexts/ThemeContext'); + vi.mocked(useTheme).mockReturnValue({ + mode: 'dark', + toggleTheme: mockToggleTheme, + setTheme: mockSetTheme, + }); + + renderWithRouter(); + + const themeButton = screen.getByRole('button', { name: 'sun' }); + expect(themeButton).toBeInTheDocument(); + expect(themeButton.style.color).toBe('rgba(255, 255, 255, 0.85)'); }); it('renders content outlet', () => { diff --git a/frontend/src/__tests__/components/ProviderTable.test.tsx b/frontend/src/__tests__/components/ProviderTable.test.tsx index 1adcdec..bb3bb71 100644 --- a/frontend/src/__tests__/components/ProviderTable.test.tsx +++ b/frontend/src/__tests__/components/ProviderTable.test.tsx @@ -145,4 +145,15 @@ describe('ProviderTable', () => { expect(await screen.findByText('gpt-4o')).toBeInTheDocument(); expect(screen.getByText('gpt-3.5-turbo')).toBeInTheDocument(); }); + + it('sets fixed width and ellipsis on name column', () => { + const { container } = render(); + const table = container.querySelector('.ant-table'); + expect(table).toBeInTheDocument(); + }); + + it('shows custom empty text when providers list is empty', () => { + render(); + expect(screen.getByText('暂无供应商,点击上方按钮添加')).toBeInTheDocument(); + }); }); diff --git a/frontend/src/__tests__/components/StatsTable.test.tsx b/frontend/src/__tests__/components/StatsTable.test.tsx index e2a2a0e..c21a7cb 100644 --- a/frontend/src/__tests__/components/StatsTable.test.tsx +++ b/frontend/src/__tests__/components/StatsTable.test.tsx @@ -91,10 +91,10 @@ describe('StatsTable', () => { it('renders table headers correctly', () => { render(); - expect(screen.getByText('供应商')).toBeInTheDocument(); - expect(screen.getByText('模型')).toBeInTheDocument(); - expect(screen.getByText('日期')).toBeInTheDocument(); - expect(screen.getByText('请求数')).toBeInTheDocument(); + expect(screen.getAllByText('供应商').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('模型').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('日期').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('请求数').length).toBeGreaterThanOrEqual(1); }); it('falls back to providerId when provider not found in providers prop', () => { @@ -108,12 +108,17 @@ describe('StatsTable', () => { it('renders with empty stats data', () => { render(); - expect(screen.getByText('供应商')).toBeInTheDocument(); - expect(screen.getByText('模型')).toBeInTheDocument(); + expect(screen.getAllByText('供应商').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('模型').length).toBeGreaterThanOrEqual(1); }); it('shows loading state', () => { render(); expect(document.querySelector('.ant-spin')).toBeInTheDocument(); }); + + it('shows custom empty text when stats data is empty', () => { + render(); + expect(screen.getByText('暂无统计数据')).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/ThemeToggle/index.tsx b/frontend/src/components/ThemeToggle/index.tsx index 69f57c3..c69ea8c 100644 --- a/frontend/src/components/ThemeToggle/index.tsx +++ b/frontend/src/components/ThemeToggle/index.tsx @@ -11,7 +11,7 @@ export function ThemeToggle() { type="text" icon={mode === 'light' ? : } onClick={toggleTheme} - style={{ color: 'inherit' }} + style={{ color: 'rgba(255, 255, 255, 0.85)' }} /> ); diff --git a/frontend/src/pages/Providers/ModelTable.tsx b/frontend/src/pages/Providers/ModelTable.tsx index 5d98121..4afbad5 100644 --- a/frontend/src/pages/Providers/ModelTable.tsx +++ b/frontend/src/pages/Providers/ModelTable.tsx @@ -18,6 +18,7 @@ export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) { title: '模型名称', dataIndex: 'modelName', key: 'modelName', + ellipsis: { showTitle: true }, }, { title: '状态', @@ -70,6 +71,7 @@ export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) { loading={isLoading} pagination={false} size="small" + locale={{ emptyText: '暂无模型,点击上方按钮添加' }} /> ); diff --git a/frontend/src/pages/Providers/ProviderTable.tsx b/frontend/src/pages/Providers/ProviderTable.tsx index aed3fa0..4e1b687 100644 --- a/frontend/src/pages/Providers/ProviderTable.tsx +++ b/frontend/src/pages/Providers/ProviderTable.tsx @@ -1,4 +1,4 @@ -import { Button, Table, Tag, Popconfirm, Space, Card } from 'antd'; +import { Button, Table, Tag, Popconfirm, Space, Card, Tooltip } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import type { Provider, Model } from '@/types'; import { ModelTable } from './ModelTable'; @@ -33,17 +33,26 @@ export function ProviderTable({ title: '名称', dataIndex: 'name', key: 'name', + width: 180, + ellipsis: { showTitle: true }, }, { title: 'Base URL', dataIndex: 'baseUrl', key: 'baseUrl', + ellipsis: { showTitle: true }, }, { title: 'API Key', dataIndex: 'apiKey', key: 'apiKey', - render: (key: string | null | undefined) => maskApiKey(key), + width: 120, + ellipsis: { showTitle: true }, + render: (key: string | null | undefined) => ( + + {maskApiKey(key)} + + ), }, { title: '状态', @@ -101,6 +110,8 @@ export function ProviderTable({ ), }} pagination={false} + scroll={{ x: 840 }} + locale={{ emptyText: '暂无供应商,点击上方按钮添加' }} /> ); diff --git a/frontend/src/pages/Stats/StatsTable.tsx b/frontend/src/pages/Stats/StatsTable.tsx index 19806a2..cbdfe32 100644 --- a/frontend/src/pages/Stats/StatsTable.tsx +++ b/frontend/src/pages/Stats/StatsTable.tsx @@ -40,22 +40,29 @@ export function StatsTable({ title: '供应商', dataIndex: 'providerId', key: 'providerId', + width: 180, + ellipsis: { showTitle: true }, render: (id: string) => providerMap.get(id) ?? id, }, { title: '模型', dataIndex: 'modelName', key: 'modelName', + width: 250, + ellipsis: { showTitle: true }, }, { title: '日期', dataIndex: 'date', key: 'date', + width: 120, }, { title: '请求数', dataIndex: 'requestCount', key: 'requestCount', + width: 100, + align: 'right', }, ]; @@ -89,6 +96,8 @@ export function StatsTable({ rowKey="id" loading={loading} pagination={{ pageSize: 20 }} + scroll={{ x: 650 }} + locale={{ emptyText: '暂无统计数据' }} /> ); diff --git a/openspec/specs/frontend-config-ui/spec.md b/openspec/specs/frontend-config-ui/spec.md index 03251be..801be1a 100644 --- a/openspec/specs/frontend-config-ui/spec.md +++ b/openspec/specs/frontend-config-ui/spec.md @@ -17,6 +17,21 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 - **THEN** 每个供应商 SHALL 显示 name、base_url 和 enabled 状态(使用 Tag 组件) - **THEN** API Key SHALL 被脱敏显示(掩码处理) - **THEN** 表格 SHALL 支持展开行以显示关联模型 +- **THEN** 表格 SHALL 设置 `scroll={{ x: 'max-content' }}` 防止窄屏溢出 + +#### Scenario: 表格列宽约束 + +- **WHEN** 渲染供应商表格 +- **THEN** 名称列 SHALL 固定宽度 180px 并启用 ellipsis(超长文本显示省略号,hover 显示 Tooltip) +- **THEN** Base URL 列 SHALL 不设固定宽度(浮动填充剩余空间)并启用 ellipsis + Tooltip +- **THEN** API Key 列 SHALL 固定宽度 120px 并启用 ellipsis +- **THEN** 状态列 SHALL 固定宽度 80px +- **THEN** 操作列 SHALL 固定宽度 160px + +#### Scenario: 表格空状态 + +- **WHEN** 供应商列表为空 +- **THEN** 表格 SHALL 显示自定义空状态文案 "暂无供应商,点击上方按钮添加" #### Scenario: 添加新供应商 @@ -53,6 +68,12 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 - **WHEN** 展开供应商行 - **THEN** 前端 SHALL 显示该供应商的模型列表 - **THEN** 每个模型 SHALL 显示 model_name 和 enabled 状态 +- **THEN** 模型名称列 SHALL 启用 ellipsis(超长文本显示省略号,hover 显示 Tooltip) + +#### Scenario: 模型表格空状态 + +- **WHEN** 模型列表为空 +- **THEN** 表格 SHALL 显示自定义空状态文案 "暂无模型,点击上方按钮添加" #### Scenario: 为供应商添加模型 @@ -91,6 +112,20 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 - **THEN** 前端 SHALL 显示请求趋势折线图 - **THEN** 前端 SHALL 使用 Ant Design Table 显示统计数据 - **THEN** 统计数据 SHALL 按供应商和模型显示请求计数 +- **THEN** 统计表格 SHALL 设置 `scroll={{ x: 'max-content' }}` 防止窄屏溢出 + +#### Scenario: 统计表格列宽约束 + +- **WHEN** 渲染统计表格 +- **THEN** 供应商列 SHALL 固定宽度 180px 并启用 ellipsis + Tooltip +- **THEN** 模型列 SHALL 固定宽度 250px 并启用 ellipsis + Tooltip +- **THEN** 日期列 SHALL 固定宽度 120px +- **THEN** 请求数列 SHALL 固定宽度 100px 并右对齐 + +#### Scenario: 统计表格空状态 + +- **WHEN** 统计数据为空 +- **THEN** 表格 SHALL 显示自定义空状态文案 "暂无统计数据" #### Scenario: 按供应商过滤统计 diff --git a/openspec/specs/theme-toggle/spec.md b/openspec/specs/theme-toggle/spec.md index 94410da..407a580 100644 --- a/openspec/specs/theme-toggle/spec.md +++ b/openspec/specs/theme-toggle/spec.md @@ -67,6 +67,7 @@ - **THEN** 前端 SHALL 在 Sider 底部显示主题切换按钮 - **THEN** 按钮 SHALL 使用 Ant Design Button 组件(type="text") - **THEN** 按钮图标 SHALL 根据当前主题显示(亮色模式显示月亮图标,暗色模式显示太阳图标) +- **THEN** 按钮颜色 SHALL 在暗色侧边栏中保持可见(使用 `rgba(255, 255, 255, 0.85)` 或等效浅色) #### Scenario: 主题切换按钮交互 @@ -79,6 +80,7 @@ - **WHEN** 侧边栏处于折叠状态 - **THEN** 主题切换按钮 SHALL 保持可见且可点击 - **THEN** 按钮 SHALL 仅显示图标(Tooltip 保持可用) +- **THEN** 按钮颜色 SHALL 保持可见 ### Requirement: 使用 React Context 管理主题状态