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:
@@ -1,6 +1,6 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { BrowserRouter } from 'react-router';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import { App as AntApp, ConfigProvider } from 'antd';
|
||||
import { AppRoutes } from '@/routes';
|
||||
import { ThemeProvider, useTheme } from '@/contexts/ThemeContext';
|
||||
import { useThemeConfig } from '@/themes';
|
||||
@@ -21,9 +21,11 @@ function ThemedApp() {
|
||||
|
||||
return (
|
||||
<ConfigProvider {...configProps}>
|
||||
<BrowserRouter>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
<AntApp>
|
||||
<BrowserRouter>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
</AntApp>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('AppLayout', () => {
|
||||
|
||||
const { container } = renderWithRouter(<AppLayout />);
|
||||
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 () => {
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('ProviderTable', () => {
|
||||
const onEdit = vi.fn();
|
||||
render(<ProviderTable {...defaultProps} onEdit={onEdit} />);
|
||||
|
||||
const editButtons = screen.getAllByRole('button', { name: '编辑' });
|
||||
const editButtons = screen.getAllByRole('button', { name: /编 ?辑/ });
|
||||
await user.click(editButtons[0]);
|
||||
|
||||
expect(onEdit).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -6,15 +6,21 @@ import { setupServer } from 'msw/node';
|
||||
import { useModels, useCreateModel, useUpdateModel, useDeleteModel } from '@/hooks/useModels';
|
||||
import type { Model, CreateModelInput, UpdateModelInput } from '@/types';
|
||||
|
||||
// Mock antd message since it uses DOM APIs not available in jsdom
|
||||
vi.mock('antd', () => ({
|
||||
message: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
const mockMessage = {
|
||||
success: 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
|
||||
const mockModels: Model[] = [
|
||||
@@ -167,7 +173,7 @@ describe('useCreateModel', () => {
|
||||
modelName: 'gpt-4.1',
|
||||
});
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] });
|
||||
expect(message.success).toHaveBeenCalledWith('模型创建成功');
|
||||
expect(mockMessage.success).toHaveBeenCalledWith('模型创建成功');
|
||||
});
|
||||
|
||||
it('calls message.error on failure', async () => {
|
||||
@@ -191,7 +197,7 @@ describe('useCreateModel', () => {
|
||||
result.current.mutate(input);
|
||||
|
||||
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',
|
||||
});
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] });
|
||||
expect(message.success).toHaveBeenCalledWith('模型更新成功');
|
||||
expect(mockMessage.success).toHaveBeenCalledWith('模型更新成功');
|
||||
});
|
||||
|
||||
it('calls message.error on failure', async () => {
|
||||
@@ -239,7 +245,7 @@ describe('useUpdateModel', () => {
|
||||
result.current.mutate({ id: 'model-1', input: { modelName: 'Updated' } });
|
||||
|
||||
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));
|
||||
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] });
|
||||
expect(message.success).toHaveBeenCalledWith('模型删除成功');
|
||||
expect(mockMessage.success).toHaveBeenCalledWith('模型删除成功');
|
||||
});
|
||||
|
||||
it('calls message.error on failure', async () => {
|
||||
@@ -282,6 +288,6 @@ describe('useDeleteModel', () => {
|
||||
result.current.mutate('model-1');
|
||||
|
||||
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 type { Provider, CreateProviderInput, UpdateProviderInput } from '@/types';
|
||||
|
||||
// Mock antd message since it uses DOM APIs not available in jsdom
|
||||
vi.mock('antd', () => ({
|
||||
message: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
const mockMessage = {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
// Import the mocked message for assertions
|
||||
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
|
||||
const mockProviders: Provider[] = [
|
||||
@@ -149,7 +154,7 @@ describe('useCreateProvider', () => {
|
||||
name: 'NewProvider',
|
||||
});
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['providers'] });
|
||||
expect(message.success).toHaveBeenCalledWith('供应商创建成功');
|
||||
expect(mockMessage.success).toHaveBeenCalledWith('供应商创建成功');
|
||||
});
|
||||
|
||||
it('calls message.error on failure', async () => {
|
||||
@@ -174,7 +179,7 @@ describe('useCreateProvider', () => {
|
||||
result.current.mutate(input);
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
expect(message.error).toHaveBeenCalled();
|
||||
expect(mockMessage.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -205,7 +210,7 @@ describe('useUpdateProvider', () => {
|
||||
name: 'UpdatedProvider',
|
||||
});
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['providers'] });
|
||||
expect(message.success).toHaveBeenCalledWith('供应商更新成功');
|
||||
expect(mockMessage.success).toHaveBeenCalledWith('供应商更新成功');
|
||||
});
|
||||
|
||||
it('calls message.error on failure', async () => {
|
||||
@@ -222,7 +227,7 @@ describe('useUpdateProvider', () => {
|
||||
result.current.mutate({ id: 'provider-1', input: { name: 'Updated' } });
|
||||
|
||||
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));
|
||||
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['providers'] });
|
||||
expect(message.success).toHaveBeenCalledWith('供应商删除成功');
|
||||
expect(mockMessage.success).toHaveBeenCalledWith('供应商删除成功');
|
||||
});
|
||||
|
||||
it('calls message.error on failure', async () => {
|
||||
@@ -265,6 +270,6 @@ describe('useDeleteProvider', () => {
|
||||
result.current.mutate('provider-1');
|
||||
|
||||
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 { Layout, Menu } from 'antd';
|
||||
import { Layout, Menu, theme } from 'antd';
|
||||
import { CloudServerOutlined, BarChartOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
@@ -17,6 +17,7 @@ export function AppLayout() {
|
||||
const { effectiveThemeId } = useTheme();
|
||||
const isDark = effectiveThemeId === 'dark';
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const getPageTitle = () => {
|
||||
if (location.pathname === '/providers') return '供应商管理';
|
||||
@@ -49,7 +50,7 @@ export function AppLayout() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: isDark ? '#fff' : 'rgba(0, 0, 0, 0.88)',
|
||||
color: token.colorText,
|
||||
fontSize: collapsed ? '1rem' : '1.25rem',
|
||||
fontWeight: 600,
|
||||
transition: 'all 0.2s',
|
||||
@@ -72,10 +73,10 @@ export function AppLayout() {
|
||||
<Layout.Header
|
||||
style={{
|
||||
padding: '0 2rem',
|
||||
background: effectiveThemeId === 'dark' ? '#141414' : '#fff',
|
||||
background: token.colorBgContainer,
|
||||
display: 'flex',
|
||||
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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { message } from 'antd';
|
||||
import { App } from 'antd';
|
||||
import type { CreateModelInput, UpdateModelInput } from '@/types';
|
||||
import * as api from '@/api/models';
|
||||
|
||||
@@ -17,6 +17,7 @@ export function useModels(providerId?: string) {
|
||||
|
||||
export function useCreateModel() {
|
||||
const queryClient = useQueryClient();
|
||||
const { message } = App.useApp();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (input: CreateModelInput) => api.createModel(input),
|
||||
@@ -32,6 +33,7 @@ export function useCreateModel() {
|
||||
|
||||
export function useUpdateModel() {
|
||||
const queryClient = useQueryClient();
|
||||
const { message } = App.useApp();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, input }: { id: string; input: UpdateModelInput }) =>
|
||||
@@ -48,6 +50,7 @@ export function useUpdateModel() {
|
||||
|
||||
export function useDeleteModel() {
|
||||
const queryClient = useQueryClient();
|
||||
const { message } = App.useApp();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.deleteModel(id),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { message } from 'antd';
|
||||
import { App } from 'antd';
|
||||
import type { CreateProviderInput, UpdateProviderInput } from '@/types';
|
||||
import * as api from '@/api/providers';
|
||||
|
||||
@@ -16,6 +16,7 @@ export function useProviders() {
|
||||
|
||||
export function useCreateProvider() {
|
||||
const queryClient = useQueryClient();
|
||||
const { message } = App.useApp();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (input: CreateProviderInput) => api.createProvider(input),
|
||||
@@ -31,6 +32,7 @@ export function useCreateProvider() {
|
||||
|
||||
export function useUpdateProvider() {
|
||||
const queryClient = useQueryClient();
|
||||
const { message } = App.useApp();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, input }: { id: string; input: UpdateProviderInput }) =>
|
||||
@@ -47,6 +49,7 @@ export function useUpdateProvider() {
|
||||
|
||||
export function useDeleteProvider() {
|
||||
const queryClient = useQueryClient();
|
||||
const { message } = App.useApp();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.deleteProvider(id),
|
||||
|
||||
@@ -10,7 +10,7 @@ export function NotFound() {
|
||||
title="404"
|
||||
subTitle="抱歉,您访问的页面不存在。"
|
||||
extra={
|
||||
<Button type="primary" onClick={() => navigate('/providers')}>
|
||||
<Button color="primary" variant="solid" onClick={() => navigate('/providers')}>
|
||||
返回首页
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -68,13 +68,9 @@ export function ModelForm({
|
||||
name="providerId"
|
||||
rules={[{ required: true, message: '请选择供应商' }]}
|
||||
>
|
||||
<Select>
|
||||
{providers.map((p) => (
|
||||
<Select.Option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
options={providers.map((p) => ({ label: p.name, value: p.id }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
|
||||
@@ -35,7 +35,7 @@ export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) {
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
{onEdit && (
|
||||
<Button type="link" size="small" onClick={() => onEdit(record)}>
|
||||
<Button variant="link" size="small" onClick={() => onEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
@@ -45,7 +45,7 @@ export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) {
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" danger size="small">
|
||||
<Button variant="link" color="danger" size="small">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
@@ -59,7 +59,7 @@ export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) {
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||
<span style={{ fontWeight: 500 }}>关联模型 ({models.length})</span>
|
||||
{onAdd && (
|
||||
<Button type="link" size="small" onClick={onAdd}>
|
||||
<Button variant="link" size="small" onClick={onAdd}>
|
||||
添加模型
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -68,7 +68,7 @@ export function ProviderTable({
|
||||
width: 160,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button type="link" size="small" onClick={() => onEdit(record)}>
|
||||
<Button variant="link" size="small" onClick={() => onEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
@@ -77,7 +77,7 @@ export function ProviderTable({
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" danger size="small">
|
||||
<Button variant="link" color="danger" size="small">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
@@ -90,7 +90,7 @@ export function ProviderTable({
|
||||
<Card
|
||||
title="供应商列表"
|
||||
extra={
|
||||
<Button type="primary" onClick={onAdd}>
|
||||
<Button color="primary" variant="solid" onClick={onAdd}>
|
||||
添加供应商
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ export function SettingsPage() {
|
||||
const { themeId, followSystem, setThemeId, setFollowSystem } = useTheme();
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<Space vertical size="large" style={{ width: '100%' }}>
|
||||
<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>
|
||||
<Text strong>主题</Text>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { theme } from 'antd';
|
||||
import type { ConfigProviderProps } from 'antd';
|
||||
|
||||
const useDarkTheme = (): ConfigProviderProps => ({
|
||||
const darkConfig: ConfigProviderProps = {
|
||||
theme: {
|
||||
algorithm: theme.darkAlgorithm,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default useDarkTheme;
|
||||
export default darkConfig;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { theme } from 'antd';
|
||||
import type { ConfigProviderProps } from 'antd';
|
||||
|
||||
const useDefaultTheme = (): ConfigProviderProps => ({
|
||||
const defaultConfig: ConfigProviderProps = {
|
||||
theme: {
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default useDefaultTheme;
|
||||
export default defaultConfig;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { ConfigProviderProps } from 'antd';
|
||||
import useDefaultTheme from './default';
|
||||
import useDarkTheme from './dark';
|
||||
import defaultConfig from './default';
|
||||
import darkConfig from './dark';
|
||||
import useMuiTheme from './mui';
|
||||
import useShadcnTheme from './shadcn';
|
||||
import useBootstrapTheme from './bootstrap';
|
||||
@@ -25,23 +26,22 @@ export const themeOptions: ThemeOption[] = [
|
||||
const themeIdSet = new Set<ThemeId>(themeOptions.map((opt) => opt.id));
|
||||
|
||||
export function useThemeConfig(themeId: ThemeId): ConfigProviderProps {
|
||||
const defaultConfig = useDefaultTheme();
|
||||
const darkConfig = useDarkTheme();
|
||||
const muiConfig = useMuiTheme();
|
||||
const shadcnConfig = useShadcnTheme();
|
||||
const bootstrapConfig = useBootstrapTheme();
|
||||
const glassConfig = useGlassTheme();
|
||||
|
||||
const configs: Record<ThemeId, ConfigProviderProps> = {
|
||||
default: defaultConfig,
|
||||
dark: darkConfig,
|
||||
mui: muiConfig,
|
||||
shadcn: shadcnConfig,
|
||||
bootstrap: bootstrapConfig,
|
||||
glass: glassConfig,
|
||||
};
|
||||
|
||||
return configs[themeId] ?? configs.default;
|
||||
return useMemo(() => {
|
||||
const configs: Record<ThemeId, ConfigProviderProps> = {
|
||||
default: defaultConfig,
|
||||
dark: darkConfig,
|
||||
mui: muiConfig,
|
||||
shadcn: shadcnConfig,
|
||||
bootstrap: bootstrapConfig,
|
||||
glass: glassConfig,
|
||||
};
|
||||
return configs[themeId] ?? configs.default;
|
||||
}, [themeId, muiConfig, shadcnConfig, bootstrapConfig, glassConfig]);
|
||||
}
|
||||
|
||||
export function isValidThemeId(value: string): value is ThemeId {
|
||||
|
||||
Reference in New Issue
Block a user