diff --git a/.gitignore b/.gitignore index e71b34a..524c472 100644 --- a/.gitignore +++ b/.gitignore @@ -322,3 +322,5 @@ Temporary Items .opencode openspec/changes/archive temp +.agents +skills-lock.json \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0b74696..a0c3c37 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 ( - - - + + + + + ); } diff --git a/frontend/src/__tests__/components/AppLayout.test.tsx b/frontend/src/__tests__/components/AppLayout.test.tsx index 566f084..3838a44 100644 --- a/frontend/src/__tests__/components/AppLayout.test.tsx +++ b/frontend/src/__tests__/components/AppLayout.test.tsx @@ -70,7 +70,7 @@ describe('AppLayout', () => { const { container } = renderWithRouter(); 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 () => { diff --git a/frontend/src/__tests__/components/ProviderTable.test.tsx b/frontend/src/__tests__/components/ProviderTable.test.tsx index bb3bb71..165c742 100644 --- a/frontend/src/__tests__/components/ProviderTable.test.tsx +++ b/frontend/src/__tests__/components/ProviderTable.test.tsx @@ -101,7 +101,7 @@ describe('ProviderTable', () => { const onEdit = vi.fn(); render(); - const editButtons = screen.getAllByRole('button', { name: '编辑' }); + const editButtons = screen.getAllByRole('button', { name: /编 ?辑/ }); await user.click(editButtons[0]); expect(onEdit).toHaveBeenCalledTimes(1); diff --git a/frontend/src/__tests__/hooks/useModels.test.tsx b/frontend/src/__tests__/hooks/useModels.test.tsx index adf9ed0..4d8a1a8 100644 --- a/frontend/src/__tests__/hooks/useModels.test.tsx +++ b/frontend/src/__tests__/hooks/useModels.test.tsx @@ -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(); + 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(); }); }); diff --git a/frontend/src/__tests__/hooks/useProviders.test.tsx b/frontend/src/__tests__/hooks/useProviders.test.tsx index 78da55c..64089c6 100644 --- a/frontend/src/__tests__/hooks/useProviders.test.tsx +++ b/frontend/src/__tests__/hooks/useProviders.test.tsx @@ -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(); + 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(); }); }); diff --git a/frontend/src/components/AppLayout/index.tsx b/frontend/src/components/AppLayout/index.tsx index 0222f6b..c6569f6 100644 --- a/frontend/src/components/AppLayout/index.tsx +++ b/frontend/src/components/AppLayout/index.tsx @@ -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() {

{getPageTitle()}

diff --git a/frontend/src/hooks/useModels.ts b/frontend/src/hooks/useModels.ts index a3c986f..1b78f9f 100644 --- a/frontend/src/hooks/useModels.ts +++ b/frontend/src/hooks/useModels.ts @@ -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), diff --git a/frontend/src/hooks/useProviders.ts b/frontend/src/hooks/useProviders.ts index 5664270..070835c 100644 --- a/frontend/src/hooks/useProviders.ts +++ b/frontend/src/hooks/useProviders.ts @@ -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), diff --git a/frontend/src/pages/NotFound.tsx b/frontend/src/pages/NotFound.tsx index c79bcf9..7e83d29 100644 --- a/frontend/src/pages/NotFound.tsx +++ b/frontend/src/pages/NotFound.tsx @@ -10,7 +10,7 @@ export function NotFound() { title="404" subTitle="抱歉,您访问的页面不存在。" extra={ - } diff --git a/frontend/src/pages/Providers/ModelForm.tsx b/frontend/src/pages/Providers/ModelForm.tsx index 0b1c268..cca754c 100644 --- a/frontend/src/pages/Providers/ModelForm.tsx +++ b/frontend/src/pages/Providers/ModelForm.tsx @@ -68,13 +68,9 @@ export function ModelForm({ name="providerId" rules={[{ required: true, message: '请选择供应商' }]} > - +