refactor: 迁移前端 antd 组件至 v6 规范 API,消除废弃用法
- 迁移静态 message 到 App.useApp() 模式,使 message 感知 ConfigProvider 上下文 - Button type/danger 迁移为 variant/color 新 API - Space direction 迁移为 vertical 布尔属性 - Select.Option 子组件迁移为 options 属性 - AppLayout 硬编码颜色替换为 antd design token - 优化 useThemeConfig:default/dark 改为静态导出,减少不必要的 hook 调用 - 同步更新 openspec 主规范文档
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -322,3 +322,5 @@ Temporary Items
|
|||||||
.opencode
|
.opencode
|
||||||
openspec/changes/archive
|
openspec/changes/archive
|
||||||
temp
|
temp
|
||||||
|
.agents
|
||||||
|
skills-lock.json
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { BrowserRouter } from 'react-router';
|
import { BrowserRouter } from 'react-router';
|
||||||
import { ConfigProvider } from 'antd';
|
import { App as AntApp, ConfigProvider } from 'antd';
|
||||||
import { AppRoutes } from '@/routes';
|
import { AppRoutes } from '@/routes';
|
||||||
import { ThemeProvider, useTheme } from '@/contexts/ThemeContext';
|
import { ThemeProvider, useTheme } from '@/contexts/ThemeContext';
|
||||||
import { useThemeConfig } from '@/themes';
|
import { useThemeConfig } from '@/themes';
|
||||||
@@ -21,9 +21,11 @@ function ThemedApp() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider {...configProps}>
|
<ConfigProvider {...configProps}>
|
||||||
|
<AntApp>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</AntApp>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ describe('AppLayout', () => {
|
|||||||
|
|
||||||
const { container } = renderWithRouter(<AppLayout />);
|
const { container } = renderWithRouter(<AppLayout />);
|
||||||
const header = container.querySelector('.ant-layout-header') as HTMLElement;
|
const header = container.querySelector('.ant-layout-header') as HTMLElement;
|
||||||
expect(header.style.background).toBe('#141414');
|
expect(header.style.borderBottom).toContain('1px solid');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses light menu theme in default mode', async () => {
|
it('uses light menu theme in default mode', async () => {
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ describe('ProviderTable', () => {
|
|||||||
const onEdit = vi.fn();
|
const onEdit = vi.fn();
|
||||||
render(<ProviderTable {...defaultProps} onEdit={onEdit} />);
|
render(<ProviderTable {...defaultProps} onEdit={onEdit} />);
|
||||||
|
|
||||||
const editButtons = screen.getAllByRole('button', { name: '编辑' });
|
const editButtons = screen.getAllByRole('button', { name: /编 ?辑/ });
|
||||||
await user.click(editButtons[0]);
|
await user.click(editButtons[0]);
|
||||||
|
|
||||||
expect(onEdit).toHaveBeenCalledTimes(1);
|
expect(onEdit).toHaveBeenCalledTimes(1);
|
||||||
|
|||||||
@@ -6,15 +6,21 @@ import { setupServer } from 'msw/node';
|
|||||||
import { useModels, useCreateModel, useUpdateModel, useDeleteModel } from '@/hooks/useModels';
|
import { useModels, useCreateModel, useUpdateModel, useDeleteModel } from '@/hooks/useModels';
|
||||||
import type { Model, CreateModelInput, UpdateModelInput } from '@/types';
|
import type { Model, CreateModelInput, UpdateModelInput } from '@/types';
|
||||||
|
|
||||||
// Mock antd message since it uses DOM APIs not available in jsdom
|
const mockMessage = {
|
||||||
vi.mock('antd', () => ({
|
|
||||||
message: {
|
|
||||||
success: vi.fn(),
|
success: vi.fn(),
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
},
|
};
|
||||||
}));
|
|
||||||
|
|
||||||
import { message } from 'antd';
|
vi.mock('antd', async (importOriginal) => {
|
||||||
|
const original = await importOriginal<typeof import('antd')>();
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
App: {
|
||||||
|
...original.App,
|
||||||
|
useApp: () => ({ message: mockMessage }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Test data
|
// Test data
|
||||||
const mockModels: Model[] = [
|
const mockModels: Model[] = [
|
||||||
@@ -167,7 +173,7 @@ describe('useCreateModel', () => {
|
|||||||
modelName: 'gpt-4.1',
|
modelName: 'gpt-4.1',
|
||||||
});
|
});
|
||||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] });
|
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] });
|
||||||
expect(message.success).toHaveBeenCalledWith('模型创建成功');
|
expect(mockMessage.success).toHaveBeenCalledWith('模型创建成功');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls message.error on failure', async () => {
|
it('calls message.error on failure', async () => {
|
||||||
@@ -191,7 +197,7 @@ describe('useCreateModel', () => {
|
|||||||
result.current.mutate(input);
|
result.current.mutate(input);
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||||
expect(message.error).toHaveBeenCalled();
|
expect(mockMessage.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -222,7 +228,7 @@ describe('useUpdateModel', () => {
|
|||||||
modelName: 'gpt-4o-updated',
|
modelName: 'gpt-4o-updated',
|
||||||
});
|
});
|
||||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] });
|
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] });
|
||||||
expect(message.success).toHaveBeenCalledWith('模型更新成功');
|
expect(mockMessage.success).toHaveBeenCalledWith('模型更新成功');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls message.error on failure', async () => {
|
it('calls message.error on failure', async () => {
|
||||||
@@ -239,7 +245,7 @@ describe('useUpdateModel', () => {
|
|||||||
result.current.mutate({ id: 'model-1', input: { modelName: 'Updated' } });
|
result.current.mutate({ id: 'model-1', input: { modelName: 'Updated' } });
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||||
expect(message.error).toHaveBeenCalled();
|
expect(mockMessage.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -265,7 +271,7 @@ describe('useDeleteModel', () => {
|
|||||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||||
|
|
||||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] });
|
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] });
|
||||||
expect(message.success).toHaveBeenCalledWith('模型删除成功');
|
expect(mockMessage.success).toHaveBeenCalledWith('模型删除成功');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls message.error on failure', async () => {
|
it('calls message.error on failure', async () => {
|
||||||
@@ -282,6 +288,6 @@ describe('useDeleteModel', () => {
|
|||||||
result.current.mutate('model-1');
|
result.current.mutate('model-1');
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||||
expect(message.error).toHaveBeenCalled();
|
expect(mockMessage.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,16 +6,21 @@ import { setupServer } from 'msw/node';
|
|||||||
import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders';
|
import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders';
|
||||||
import type { Provider, CreateProviderInput, UpdateProviderInput } from '@/types';
|
import type { Provider, CreateProviderInput, UpdateProviderInput } from '@/types';
|
||||||
|
|
||||||
// Mock antd message since it uses DOM APIs not available in jsdom
|
const mockMessage = {
|
||||||
vi.mock('antd', () => ({
|
|
||||||
message: {
|
|
||||||
success: vi.fn(),
|
success: vi.fn(),
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
},
|
};
|
||||||
}));
|
|
||||||
|
|
||||||
// Import the mocked message for assertions
|
vi.mock('antd', async (importOriginal) => {
|
||||||
import { message } from 'antd';
|
const original = await importOriginal<typeof import('antd')>();
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
App: {
|
||||||
|
...original.App,
|
||||||
|
useApp: () => ({ message: mockMessage }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Test data
|
// Test data
|
||||||
const mockProviders: Provider[] = [
|
const mockProviders: Provider[] = [
|
||||||
@@ -149,7 +154,7 @@ describe('useCreateProvider', () => {
|
|||||||
name: 'NewProvider',
|
name: 'NewProvider',
|
||||||
});
|
});
|
||||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['providers'] });
|
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['providers'] });
|
||||||
expect(message.success).toHaveBeenCalledWith('供应商创建成功');
|
expect(mockMessage.success).toHaveBeenCalledWith('供应商创建成功');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls message.error on failure', async () => {
|
it('calls message.error on failure', async () => {
|
||||||
@@ -174,7 +179,7 @@ describe('useCreateProvider', () => {
|
|||||||
result.current.mutate(input);
|
result.current.mutate(input);
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||||
expect(message.error).toHaveBeenCalled();
|
expect(mockMessage.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,7 +210,7 @@ describe('useUpdateProvider', () => {
|
|||||||
name: 'UpdatedProvider',
|
name: 'UpdatedProvider',
|
||||||
});
|
});
|
||||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['providers'] });
|
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['providers'] });
|
||||||
expect(message.success).toHaveBeenCalledWith('供应商更新成功');
|
expect(mockMessage.success).toHaveBeenCalledWith('供应商更新成功');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls message.error on failure', async () => {
|
it('calls message.error on failure', async () => {
|
||||||
@@ -222,7 +227,7 @@ describe('useUpdateProvider', () => {
|
|||||||
result.current.mutate({ id: 'provider-1', input: { name: 'Updated' } });
|
result.current.mutate({ id: 'provider-1', input: { name: 'Updated' } });
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||||
expect(message.error).toHaveBeenCalled();
|
expect(mockMessage.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -248,7 +253,7 @@ describe('useDeleteProvider', () => {
|
|||||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||||
|
|
||||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['providers'] });
|
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['providers'] });
|
||||||
expect(message.success).toHaveBeenCalledWith('供应商删除成功');
|
expect(mockMessage.success).toHaveBeenCalledWith('供应商删除成功');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls message.error on failure', async () => {
|
it('calls message.error on failure', async () => {
|
||||||
@@ -265,6 +270,6 @@ describe('useDeleteProvider', () => {
|
|||||||
result.current.mutate('provider-1');
|
result.current.mutate('provider-1');
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||||
expect(message.error).toHaveBeenCalled();
|
expect(mockMessage.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Layout, Menu } from 'antd';
|
import { Layout, Menu, theme } from 'antd';
|
||||||
import { CloudServerOutlined, BarChartOutlined, SettingOutlined } from '@ant-design/icons';
|
import { CloudServerOutlined, BarChartOutlined, SettingOutlined } from '@ant-design/icons';
|
||||||
import { Outlet, useLocation, useNavigate } from 'react-router';
|
import { Outlet, useLocation, useNavigate } from 'react-router';
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
import { useTheme } from '@/contexts/ThemeContext';
|
||||||
@@ -17,6 +17,7 @@ export function AppLayout() {
|
|||||||
const { effectiveThemeId } = useTheme();
|
const { effectiveThemeId } = useTheme();
|
||||||
const isDark = effectiveThemeId === 'dark';
|
const isDark = effectiveThemeId === 'dark';
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
const getPageTitle = () => {
|
const getPageTitle = () => {
|
||||||
if (location.pathname === '/providers') return '供应商管理';
|
if (location.pathname === '/providers') return '供应商管理';
|
||||||
@@ -49,7 +50,7 @@ export function AppLayout() {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
color: isDark ? '#fff' : 'rgba(0, 0, 0, 0.88)',
|
color: token.colorText,
|
||||||
fontSize: collapsed ? '1rem' : '1.25rem',
|
fontSize: collapsed ? '1rem' : '1.25rem',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
transition: 'all 0.2s',
|
transition: 'all 0.2s',
|
||||||
@@ -72,10 +73,10 @@ export function AppLayout() {
|
|||||||
<Layout.Header
|
<Layout.Header
|
||||||
style={{
|
style={{
|
||||||
padding: '0 2rem',
|
padding: '0 2rem',
|
||||||
background: effectiveThemeId === 'dark' ? '#141414' : '#fff',
|
background: token.colorBgContainer,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderBottom: effectiveThemeId === 'dark' ? '1px solid #303030' : '1px solid #f0f0f0',
|
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h1 style={{ margin: 0, fontSize: '1.25rem' }}>{getPageTitle()}</h1>
|
<h1 style={{ margin: 0, fontSize: '1.25rem' }}>{getPageTitle()}</h1>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { message } from 'antd';
|
import { App } from 'antd';
|
||||||
import type { CreateModelInput, UpdateModelInput } from '@/types';
|
import type { CreateModelInput, UpdateModelInput } from '@/types';
|
||||||
import * as api from '@/api/models';
|
import * as api from '@/api/models';
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ export function useModels(providerId?: string) {
|
|||||||
|
|
||||||
export function useCreateModel() {
|
export function useCreateModel() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (input: CreateModelInput) => api.createModel(input),
|
mutationFn: (input: CreateModelInput) => api.createModel(input),
|
||||||
@@ -32,6 +33,7 @@ export function useCreateModel() {
|
|||||||
|
|
||||||
export function useUpdateModel() {
|
export function useUpdateModel() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, input }: { id: string; input: UpdateModelInput }) =>
|
mutationFn: ({ id, input }: { id: string; input: UpdateModelInput }) =>
|
||||||
@@ -48,6 +50,7 @@ export function useUpdateModel() {
|
|||||||
|
|
||||||
export function useDeleteModel() {
|
export function useDeleteModel() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (id: string) => api.deleteModel(id),
|
mutationFn: (id: string) => api.deleteModel(id),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { message } from 'antd';
|
import { App } from 'antd';
|
||||||
import type { CreateProviderInput, UpdateProviderInput } from '@/types';
|
import type { CreateProviderInput, UpdateProviderInput } from '@/types';
|
||||||
import * as api from '@/api/providers';
|
import * as api from '@/api/providers';
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ export function useProviders() {
|
|||||||
|
|
||||||
export function useCreateProvider() {
|
export function useCreateProvider() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (input: CreateProviderInput) => api.createProvider(input),
|
mutationFn: (input: CreateProviderInput) => api.createProvider(input),
|
||||||
@@ -31,6 +32,7 @@ export function useCreateProvider() {
|
|||||||
|
|
||||||
export function useUpdateProvider() {
|
export function useUpdateProvider() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, input }: { id: string; input: UpdateProviderInput }) =>
|
mutationFn: ({ id, input }: { id: string; input: UpdateProviderInput }) =>
|
||||||
@@ -47,6 +49,7 @@ export function useUpdateProvider() {
|
|||||||
|
|
||||||
export function useDeleteProvider() {
|
export function useDeleteProvider() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (id: string) => api.deleteProvider(id),
|
mutationFn: (id: string) => api.deleteProvider(id),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function NotFound() {
|
|||||||
title="404"
|
title="404"
|
||||||
subTitle="抱歉,您访问的页面不存在。"
|
subTitle="抱歉,您访问的页面不存在。"
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" onClick={() => navigate('/providers')}>
|
<Button color="primary" variant="solid" onClick={() => navigate('/providers')}>
|
||||||
返回首页
|
返回首页
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,13 +68,9 @@ export function ModelForm({
|
|||||||
name="providerId"
|
name="providerId"
|
||||||
rules={[{ required: true, message: '请选择供应商' }]}
|
rules={[{ required: true, message: '请选择供应商' }]}
|
||||||
>
|
>
|
||||||
<Select>
|
<Select
|
||||||
{providers.map((p) => (
|
options={providers.map((p) => ({ label: p.name, value: p.id }))}
|
||||||
<Select.Option key={p.id} value={p.id}>
|
/>
|
||||||
{p.name}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) {
|
|||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space>
|
<Space>
|
||||||
{onEdit && (
|
{onEdit && (
|
||||||
<Button type="link" size="small" onClick={() => onEdit(record)}>
|
<Button variant="link" size="small" onClick={() => onEdit(record)}>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -45,7 +45,7 @@ export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) {
|
|||||||
okText="确定"
|
okText="确定"
|
||||||
cancelText="取消"
|
cancelText="取消"
|
||||||
>
|
>
|
||||||
<Button type="link" danger size="small">
|
<Button variant="link" color="danger" size="small">
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
@@ -59,7 +59,7 @@ export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) {
|
|||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||||
<span style={{ fontWeight: 500 }}>关联模型 ({models.length})</span>
|
<span style={{ fontWeight: 500 }}>关联模型 ({models.length})</span>
|
||||||
{onAdd && (
|
{onAdd && (
|
||||||
<Button type="link" size="small" onClick={onAdd}>
|
<Button variant="link" size="small" onClick={onAdd}>
|
||||||
添加模型
|
添加模型
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function ProviderTable({
|
|||||||
width: 160,
|
width: 160,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="link" size="small" onClick={() => onEdit(record)}>
|
<Button variant="link" size="small" onClick={() => onEdit(record)}>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
@@ -77,7 +77,7 @@ export function ProviderTable({
|
|||||||
okText="确定"
|
okText="确定"
|
||||||
cancelText="取消"
|
cancelText="取消"
|
||||||
>
|
>
|
||||||
<Button type="link" danger size="small">
|
<Button variant="link" color="danger" size="small">
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
@@ -90,7 +90,7 @@ export function ProviderTable({
|
|||||||
<Card
|
<Card
|
||||||
title="供应商列表"
|
title="供应商列表"
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" onClick={onAdd}>
|
<Button color="primary" variant="solid" onClick={onAdd}>
|
||||||
添加供应商
|
添加供应商
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ export function SettingsPage() {
|
|||||||
const { themeId, followSystem, setThemeId, setFollowSystem } = useTheme();
|
const { themeId, followSystem, setThemeId, setFollowSystem } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
<Space vertical size="large" style={{ width: '100%' }}>
|
||||||
<Card title="主题">
|
<Card title="主题">
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<Space vertical size="middle" style={{ width: '100%' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<div>
|
<div>
|
||||||
<Text strong>主题</Text>
|
<Text strong>主题</Text>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { theme } from 'antd';
|
import { theme } from 'antd';
|
||||||
import type { ConfigProviderProps } from 'antd';
|
import type { ConfigProviderProps } from 'antd';
|
||||||
|
|
||||||
const useDarkTheme = (): ConfigProviderProps => ({
|
const darkConfig: ConfigProviderProps = {
|
||||||
theme: {
|
theme: {
|
||||||
algorithm: theme.darkAlgorithm,
|
algorithm: theme.darkAlgorithm,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
export default useDarkTheme;
|
export default darkConfig;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { theme } from 'antd';
|
import { theme } from 'antd';
|
||||||
import type { ConfigProviderProps } from 'antd';
|
import type { ConfigProviderProps } from 'antd';
|
||||||
|
|
||||||
const useDefaultTheme = (): ConfigProviderProps => ({
|
const defaultConfig: ConfigProviderProps = {
|
||||||
theme: {
|
theme: {
|
||||||
algorithm: theme.defaultAlgorithm,
|
algorithm: theme.defaultAlgorithm,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
export default useDefaultTheme;
|
export default defaultConfig;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import type { ConfigProviderProps } from 'antd';
|
import type { ConfigProviderProps } from 'antd';
|
||||||
import useDefaultTheme from './default';
|
import defaultConfig from './default';
|
||||||
import useDarkTheme from './dark';
|
import darkConfig from './dark';
|
||||||
import useMuiTheme from './mui';
|
import useMuiTheme from './mui';
|
||||||
import useShadcnTheme from './shadcn';
|
import useShadcnTheme from './shadcn';
|
||||||
import useBootstrapTheme from './bootstrap';
|
import useBootstrapTheme from './bootstrap';
|
||||||
@@ -25,13 +26,12 @@ export const themeOptions: ThemeOption[] = [
|
|||||||
const themeIdSet = new Set<ThemeId>(themeOptions.map((opt) => opt.id));
|
const themeIdSet = new Set<ThemeId>(themeOptions.map((opt) => opt.id));
|
||||||
|
|
||||||
export function useThemeConfig(themeId: ThemeId): ConfigProviderProps {
|
export function useThemeConfig(themeId: ThemeId): ConfigProviderProps {
|
||||||
const defaultConfig = useDefaultTheme();
|
|
||||||
const darkConfig = useDarkTheme();
|
|
||||||
const muiConfig = useMuiTheme();
|
const muiConfig = useMuiTheme();
|
||||||
const shadcnConfig = useShadcnTheme();
|
const shadcnConfig = useShadcnTheme();
|
||||||
const bootstrapConfig = useBootstrapTheme();
|
const bootstrapConfig = useBootstrapTheme();
|
||||||
const glassConfig = useGlassTheme();
|
const glassConfig = useGlassTheme();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
const configs: Record<ThemeId, ConfigProviderProps> = {
|
const configs: Record<ThemeId, ConfigProviderProps> = {
|
||||||
default: defaultConfig,
|
default: defaultConfig,
|
||||||
dark: darkConfig,
|
dark: darkConfig,
|
||||||
@@ -40,8 +40,8 @@ export function useThemeConfig(themeId: ThemeId): ConfigProviderProps {
|
|||||||
bootstrap: bootstrapConfig,
|
bootstrap: bootstrapConfig,
|
||||||
glass: glassConfig,
|
glass: glassConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
return configs[themeId] ?? configs.default;
|
return configs[themeId] ?? configs.default;
|
||||||
|
}, [themeId, muiConfig, shadcnConfig, bootstrapConfig, glassConfig]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidThemeId(value: string): value is ThemeId {
|
export function isValidThemeId(value: string): value is ThemeId {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
|||||||
|
|
||||||
### Requirement: 提供供应商管理页面
|
### Requirement: 提供供应商管理页面
|
||||||
|
|
||||||
前端 SHALL 使用 Ant Design 组件提供供应商管理页面。
|
前端 SHALL 使用 Ant Design 组件提供供应商管理页面,所有组件 SHALL 遵循 antd v6 API 规范。
|
||||||
|
|
||||||
#### Scenario: 显示供应商列表
|
#### Scenario: 显示供应商列表
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
|||||||
- **WHEN** 用户提交包含有效数据的表单
|
- **WHEN** 用户提交包含有效数据的表单
|
||||||
- **THEN** 前端 SHALL 通过 useMutation 调用创建 API
|
- **THEN** 前端 SHALL 通过 useMutation 调用创建 API
|
||||||
- **THEN** 成功后 SHALL 关闭 Modal 并刷新供应商列表
|
- **THEN** 成功后 SHALL 关闭 Modal 并刷新供应商列表
|
||||||
- **THEN** 失败 SHALL 使用 message.error() 提示
|
- **THEN** 失败 SHALL 使用 `App.useApp()` 提供的 `message` 实例显示错误提示
|
||||||
|
|
||||||
#### Scenario: 编辑现有供应商
|
#### Scenario: 编辑现有供应商
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
|||||||
|
|
||||||
### Requirement: 提供模型管理界面
|
### Requirement: 提供模型管理界面
|
||||||
|
|
||||||
前端 SHALL 在供应商页面展开行中提供模型管理。
|
前端 SHALL 在供应商页面展开行中提供模型管理,所有组件 SHALL 遵循 antd v6 API 规范。
|
||||||
|
|
||||||
#### Scenario: 显示供应商的模型
|
#### Scenario: 显示供应商的模型
|
||||||
|
|
||||||
@@ -80,6 +80,7 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
|||||||
- **WHEN** 用户在展开行中点击"添加模型"
|
- **WHEN** 用户在展开行中点击"添加模型"
|
||||||
- **THEN** 前端 SHALL 显示 Ant Design Modal + Form
|
- **THEN** 前端 SHALL 显示 Ant Design Modal + Form
|
||||||
- **THEN** provider_id SHALL 自动关联当前供应商
|
- **THEN** provider_id SHALL 自动关联当前供应商
|
||||||
|
- **THEN** 供应商选择 SHALL 使用 `options` 属性而非 `Select.Option` 子组件
|
||||||
- **WHEN** 用户提交表单
|
- **WHEN** 用户提交表单
|
||||||
- **THEN** 前端 SHALL 通过 useMutation 调用创建 API
|
- **THEN** 前端 SHALL 通过 useMutation 调用创建 API
|
||||||
- **THEN** 成功后 SHALL 刷新模型列表
|
- **THEN** 成功后 SHALL 刷新模型列表
|
||||||
@@ -141,12 +142,12 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
|||||||
|
|
||||||
### Requirement: 优雅处理 API 错误
|
### Requirement: 优雅处理 API 错误
|
||||||
|
|
||||||
前端 SHALL 处理 API 错误并显示用户友好的消息。
|
前端 SHALL 处理 API 错误并显示用户友好的消息,message 组件 SHALL 通过 `App.useApp()` 获取。
|
||||||
|
|
||||||
#### Scenario: API 请求失败
|
#### Scenario: API 请求失败
|
||||||
|
|
||||||
- **WHEN** API 请求失败(网络错误、4xx、5xx)
|
- **WHEN** API 请求失败(网络错误、4xx、5xx)
|
||||||
- **THEN** 前端 SHALL 使用 Ant Design 的 message.error() 显示全局错误提示
|
- **THEN** 前端 SHALL 使用 `App.useApp()` 提供的 `message` 实例显示全局错误提示
|
||||||
- **THEN** 错误消息 SHALL 具有描述性
|
- **THEN** 错误消息 SHALL 具有描述性
|
||||||
|
|
||||||
#### Scenario: 验证错误
|
#### Scenario: 验证错误
|
||||||
@@ -187,21 +188,24 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
|||||||
|
|
||||||
### Requirement: 使用无组件库的最小 UI
|
### Requirement: 使用无组件库的最小 UI
|
||||||
|
|
||||||
前端 SHALL 使用 Ant Design 6 作为 UI 组件库。
|
前端 SHALL 使用 Ant Design 6 作为 UI 组件库,所有组件 SHALL 使用 antd v6 推荐的 API。
|
||||||
|
|
||||||
#### Scenario: Ant Design 组件使用
|
#### Scenario: Ant Design 组件使用
|
||||||
|
|
||||||
- **WHEN** 实现前端
|
- **WHEN** 实现前端
|
||||||
- **THEN** 它 SHALL 使用 Ant Design 6 组件(Table、Form、Modal、Menu、Tag、Popconfirm、DatePicker、Button、Select 等)
|
- **THEN** Button 组件 SHALL 使用 `variant` 和 `color` 属性替代已废弃的 `type`(link/text/dashed)和 `danger` 属性
|
||||||
- **THEN** 它 SHALL 使用 @ant-design/icons 提供图标
|
- **THEN** Button `type="link"` SHALL 替换为 `variant="link"`
|
||||||
|
- **THEN** Button `danger` 属性 SHALL 替换为 `color="danger"`
|
||||||
|
- **THEN** Space 组件 SHALL 使用 `vertical` 布尔属性替代已废弃的 `direction` 属性
|
||||||
|
- **THEN** Select 组件 SHALL 使用 `options` 属性替代 `Select.Option` 子组件
|
||||||
- **THEN** 图标 SHALL 优先使用图标库中已有的图标
|
- **THEN** 图标 SHALL 优先使用图标库中已有的图标
|
||||||
|
|
||||||
#### Scenario: Ant Design 主题支持
|
#### Scenario: App 组件包裹
|
||||||
|
|
||||||
- **WHEN** 配置 Ant Design 主题
|
- **WHEN** 应用初始化
|
||||||
- **THEN** 前端 SHALL 支持亮色和暗色模式切换
|
- **THEN** `App.tsx` SHALL 在 `ConfigProvider` 内部使用 Ant Design `<App>` 组件包裹应用
|
||||||
- **THEN** 前端 SHALL 使用 Ant Design v6 的 theme.darkAlgorithm 和 theme.defaultAlgorithm
|
- **THEN** hooks 中 SHALL 使用 `App.useApp()` 获取 `message` 实例
|
||||||
- **THEN** 前端 SHALL 通过 ConfigProvider 动态切换主题
|
- **THEN** `message` 实例 SHALL NOT 通过静态 `import { message } from 'antd'` 获取
|
||||||
|
|
||||||
### Requirement: 样式体系
|
### Requirement: 样式体系
|
||||||
|
|
||||||
@@ -351,10 +355,10 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
|||||||
- **WHEN** 用户执行创建、更新或删除操作
|
- **WHEN** 用户执行创建、更新或删除操作
|
||||||
- **THEN** 前端 SHALL 使用 TanStack Query 的 useMutation hook
|
- **THEN** 前端 SHALL 使用 TanStack Query 的 useMutation hook
|
||||||
- **THEN** 操作成功后 SHALL 自动失效相关查询缓存
|
- **THEN** 操作成功后 SHALL 自动失效相关查询缓存
|
||||||
- **THEN** 操作失败 SHALL 使用 Ant Design message.error() 显示错误提示
|
- **THEN** 操作失败 SHALL 使用 `App.useApp()` 提供的 `message` 实例显示错误提示
|
||||||
|
|
||||||
#### Scenario: 错误提示
|
#### Scenario: 错误提示
|
||||||
|
|
||||||
- **WHEN** API 请求失败(网络错误、4xx、5xx)
|
- **WHEN** API 请求失败(网络错误、4xx、5xx)
|
||||||
- **THEN** 前端 SHALL 使用 Ant Design 的 message.error() 显示全局错误提示
|
- **THEN** 前端 SHALL 使用 `App.useApp()` 提供的 `message` 实例显示全局错误提示
|
||||||
- **THEN** 错误消息 SHALL 具有描述性
|
- **THEN** 错误消息 SHALL 具有描述性
|
||||||
|
|||||||
@@ -104,13 +104,14 @@
|
|||||||
|
|
||||||
### Requirement: 提供设置页面
|
### Requirement: 提供设置页面
|
||||||
|
|
||||||
前端 SHALL 提供设置页面,使用卡片布局组织设置内容。
|
前端 SHALL 提供设置页面,使用卡片布局组织设置内容,所有布局组件 SHALL 遵循 antd v6 API 规范。
|
||||||
|
|
||||||
#### Scenario: 主题卡片
|
#### Scenario: 主题卡片
|
||||||
|
|
||||||
- **WHEN** 用户访问设置页面
|
- **WHEN** 用户访问设置页面
|
||||||
- **THEN** 前端 SHALL 显示主题设置卡片
|
- **THEN** 前端 SHALL 显示主题设置卡片
|
||||||
- **THEN** 卡片内 SHALL 包含主题下拉栏和跟随系统开关
|
- **THEN** 卡片内 SHALL 包含主题下拉栏和跟随系统开关
|
||||||
|
- **THEN** 卡片内布局 SHALL 使用 `Space` 的 `vertical` 布尔属性替代已废弃的 `direction` 属性
|
||||||
|
|
||||||
#### Scenario: 主题下拉栏
|
#### Scenario: 主题下拉栏
|
||||||
|
|
||||||
@@ -136,14 +137,15 @@
|
|||||||
|
|
||||||
### Requirement: 侧边栏跟随应用主题
|
### Requirement: 侧边栏跟随应用主题
|
||||||
|
|
||||||
侧边栏 SHALL 根据 `effectiveThemeId` 动态切换亮色或暗色外观。
|
侧边栏 SHALL 根据 `effectiveThemeId` 动态切换亮色或暗色外观,使用 antd token 获取颜色值。
|
||||||
|
|
||||||
#### Scenario: 亮色主题下的侧边栏
|
#### Scenario: 亮色主题下的侧边栏
|
||||||
|
|
||||||
- **WHEN** `effectiveThemeId` 不为 `'dark'`
|
- **WHEN** `effectiveThemeId` 不为 `'dark'`
|
||||||
- **THEN** 侧边栏 Sider 背景 SHALL 为浅色(`#fff`)
|
- **THEN** 侧边栏 Sider 背景 SHALL 使用 antd token 的 `colorBgContainer` 而非硬编码颜色值
|
||||||
- **THEN** Logo 文字颜色 SHALL 为深色
|
- **THEN** Logo 文字颜色 SHALL 使用 antd token 的 `colorText` 而非硬编码颜色值
|
||||||
- **THEN** Menu 组件 SHALL 使用 `theme="light"`
|
- **THEN** Menu 组件 SHALL 使用 `theme="light"`
|
||||||
|
- **THEN** Header 背景和边框 SHALL 使用 antd token 获取颜色值
|
||||||
|
|
||||||
#### Scenario: 暗色主题下的侧边栏
|
#### Scenario: 暗色主题下的侧边栏
|
||||||
|
|
||||||
@@ -167,6 +169,7 @@
|
|||||||
- **WHEN** 应用初始化
|
- **WHEN** 应用初始化
|
||||||
- **THEN** 主题注册表 SHALL 包含 6 套主题的完整配置
|
- **THEN** 主题注册表 SHALL 包含 6 套主题的完整配置
|
||||||
- **THEN** 每套主题 SHALL 通过 `useConfig()` hook 返回 `ConfigProviderProps`
|
- **THEN** 每套主题 SHALL 通过 `useConfig()` hook 返回 `ConfigProviderProps`
|
||||||
|
- **THEN** `useThemeConfig` SHALL 优化加载策略,避免不必要的主题初始化开销
|
||||||
|
|
||||||
#### Scenario: 主题文件组织
|
#### Scenario: 主题文件组织
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user