1
0

feat: 前端适配后端新接口

适配后端统一模型 ID、协议字段、UUID 自动生成和结构化错误响应:

- 类型定义:Provider 新增 protocol 字段,Model 新增 unifiedId,CreateModelInput 移除 id
- API 客户端:提取结构化错误响应中的错误码
- 供应商管理:添加协议选择下拉框和表格列
- 模型管理:移除 ID 输入,显示统一模型 ID(只读)
- Hooks:错误码映射为友好中文消息
- 测试:所有组件测试通过,mock 数据适配新字段
- 文档:更新 README 说明协议字段和统一模型 ID
This commit is contained in:
2026-04-21 20:49:37 +08:00
parent 24f03595a7
commit feff97acbd
28 changed files with 547 additions and 78 deletions

View File

@@ -0,0 +1,123 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ModelTable } from '@/pages/Providers/ModelTable';
import type { Model } from '@/types';
const mockModels: Model[] = [
{
id: 'model-1',
providerId: 'openai',
modelName: 'gpt-4o',
enabled: true,
createdAt: '2024-01-01T00:00:00Z',
unifiedId: 'openai/gpt-4o',
},
{
id: 'model-2',
providerId: 'openai',
modelName: 'gpt-3.5-turbo',
enabled: false,
createdAt: '2024-01-02T00:00:00Z',
unifiedId: 'openai/gpt-3.5-turbo',
},
];
const mockMutate = vi.fn();
vi.mock('@/hooks/useModels', () => ({
useModels: vi.fn((providerId: string) => {
if (providerId === 'openai') {
return { data: mockModels, isLoading: false };
}
return { data: [], isLoading: false };
}),
useDeleteModel: vi.fn(() => ({ mutate: mockMutate })),
}));
const defaultProps = {
providerId: 'openai',
onAdd: vi.fn(),
onEdit: vi.fn(),
};
describe('ModelTable', () => {
beforeEach(() => {
mockMutate.mockClear();
});
it('renders model list with unified ID and model name', () => {
render(<ModelTable {...defaultProps} />);
expect(screen.getByText(/关联模型/)).toBeInTheDocument();
expect(screen.getByText('openai/gpt-4o')).toBeInTheDocument();
expect(screen.getByText('openai/gpt-3.5-turbo')).toBeInTheDocument();
expect(screen.getByText('gpt-4o')).toBeInTheDocument();
expect(screen.getByText('gpt-3.5-turbo')).toBeInTheDocument();
});
it('renders status tags correctly', () => {
render(<ModelTable {...defaultProps} />);
const enabledTags = screen.getAllByText('启用');
const disabledTags = screen.getAllByText('禁用');
expect(enabledTags.length).toBeGreaterThanOrEqual(1);
expect(disabledTags.length).toBeGreaterThanOrEqual(1);
});
it('calls onAdd when clicking "添加模型" button', async () => {
const user = userEvent.setup();
const onAdd = vi.fn();
render(<ModelTable {...defaultProps} onAdd={onAdd} />);
await user.click(screen.getByRole('button', { name: '添加模型' }));
expect(onAdd).toHaveBeenCalledTimes(1);
});
it('calls onEdit with correct model when clicking "编辑"', async () => {
const user = userEvent.setup();
const onEdit = vi.fn();
render(<ModelTable {...defaultProps} onEdit={onEdit} />);
const editButtons = screen.getAllByRole('button', { name: /编 ?辑/ });
await user.click(editButtons[0]);
expect(onEdit).toHaveBeenCalledTimes(1);
expect(onEdit).toHaveBeenCalledWith(mockModels[0]);
});
it('calls deleteModel.mutate with correct model ID when delete is confirmed', async () => {
const user = userEvent.setup();
render(<ModelTable {...defaultProps} />);
// Find and click the delete button for the first row
const deleteButtons = screen.getAllByRole('button', { name: '删除' });
await user.click(deleteButtons[0]);
// TDesign Popconfirm renders confirmation popup with "确定" button
const confirmButton = await screen.findByRole('button', { name: '确定' });
await user.click(confirmButton);
// Assert that deleteModel.mutate was called with the correct model ID
expect(mockMutate).toHaveBeenCalledTimes(1);
expect(mockMutate).toHaveBeenCalledWith('model-1');
}, 10000);
it('shows custom empty text when models list is empty', () => {
render(<ModelTable providerId="anthropic" />);
expect(screen.getByText('暂无模型,点击上方按钮添加')).toBeInTheDocument();
});
it('does not render add button when onAdd is not provided', () => {
render(<ModelTable providerId="openai" />);
expect(screen.queryByRole('button', { name: '添加模型' })).not.toBeInTheDocument();
});
it('does not render edit button when onEdit is not provided', () => {
render(<ModelTable providerId="openai" onAdd={vi.fn()} />);
expect(screen.queryByRole('button', { name: /编 ?辑/ })).not.toBeInTheDocument();
});
});