refactor: 简化模型管理,移除启用/禁用,优化测试和布局

- 移除供应商/模型启用禁用能力,清理DB schema/migration/API/前端
- 供应商测试改为Base URL连通性+/models探测
- 新增POST /api/models/test模型连接测试
- 新增GET /api/providers/options专用供应商选项接口
- 统一工具栏为ModelsToolbar,参考项目管理布局
- 模型弹窗优化:默认能力、响应式3列标签、并排数值
- 前后端正整数校验、供应商下拉loading/error/empty状态
- 表格列宽统一,操作列/名称列固定宽度
This commit is contained in:
2026-05-29 18:03:33 +08:00
parent 9241c782e6
commit 34e915ccf4
39 changed files with 895 additions and 961 deletions

View File

@@ -1,6 +1,15 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { CreateModelRequest, Model, ModelListResponse, ModelResponse, UpdateModelRequest } from "../../shared/api";
import type {
CreateModelRequest,
Model,
ModelListResponse,
ModelResponse,
ModelTestResponse,
ModelTestResultResponse,
TestModelRequest,
UpdateModelRequest,
} from "../../shared/api";
const MODELS_KEY = ["models"] as const;
@@ -21,16 +30,6 @@ export async function deleteModel(id: string): Promise<void> {
}
}
export async function disableModel(id: string): Promise<Model> {
const response = await fetch(`/api/models/${id}/disable`, { method: "POST" });
return handleResponse(response);
}
export async function enableModel(id: string): Promise<Model> {
const response = await fetch(`/api/models/${id}/enable`, { method: "POST" });
return handleResponse(response);
}
export async function fetchModel(id: string): Promise<Model> {
const response = await fetch(`/api/models/${id}`);
return handleResponse(response);
@@ -57,6 +56,20 @@ export async function fetchModelList(params: {
return response.json() as Promise<ModelListResponse>;
}
export async function testModelConnection(data: TestModelRequest): Promise<ModelTestResponse> {
const response = await fetch("/api/models/test", {
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
method: "POST",
});
if (!response.ok) {
const body = (await response.json().catch(() => null)) as null | { error?: string };
throw new Error(body?.error ?? `HTTP ${response.status}`);
}
const result = (await response.json()) as ModelTestResultResponse;
return result.modelTestResponse;
}
export async function updateModel(id: string, data: UpdateModelRequest): Promise<Model> {
const response = await fetch(`/api/models/${id}`, {
body: JSON.stringify(data),
@@ -86,26 +99,6 @@ export function useDeleteModel() {
});
}
export function useDisableModel() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: disableModel,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: MODELS_KEY });
},
});
}
export function useEnableModel() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: enableModel,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: MODELS_KEY });
},
});
}
export function useModel(id: string) {
return useQuery({
enabled: !!id,
@@ -121,6 +114,12 @@ export function useModelList(params: { keyword?: string; page?: number; pageSize
});
}
export function useTestModelConnection() {
return useMutation({
mutationFn: testModelConnection,
});
}
export function useUpdateModel() {
const queryClient = useQueryClient();
return useMutation({

View File

@@ -4,6 +4,7 @@ import type {
CreateProviderRequest,
Provider,
ProviderListResponse,
ProviderOptionsResponse,
ProviderResponse,
ProviderTestResponse,
ProviderTestResultResponse,
@@ -30,16 +31,6 @@ export async function deleteProvider(id: string): Promise<void> {
}
}
export async function disableProvider(id: string): Promise<Provider> {
const response = await fetch(`/api/providers/${id}/disable`, { method: "POST" });
return handleResponse(response);
}
export async function enableProvider(id: string): Promise<Provider> {
const response = await fetch(`/api/providers/${id}/enable`, { method: "POST" });
return handleResponse(response);
}
export async function fetchProvider(id: string): Promise<Provider> {
const response = await fetch(`/api/providers/${id}`);
return handleResponse(response);
@@ -64,6 +55,15 @@ export async function fetchProviderList(params: {
return response.json() as Promise<ProviderListResponse>;
}
export async function fetchProviderOptions(): Promise<ProviderOptionsResponse> {
const response = await fetch("/api/providers/options");
if (!response.ok) {
const body = (await response.json().catch(() => null)) as null | { error?: string };
throw new Error(body?.error ?? `HTTP ${response.status}`);
}
return response.json() as Promise<ProviderOptionsResponse>;
}
export async function testProviderConfig(data: CreateProviderRequest): Promise<ProviderTestResponse> {
const response = await fetch("/api/providers/test", {
body: JSON.stringify(data),
@@ -78,16 +78,6 @@ export async function testProviderConfig(data: CreateProviderRequest): Promise<P
return result.providerTestResponse;
}
export async function testProviderConnection(id: string): Promise<ProviderTestResponse> {
const response = await fetch(`/api/providers/${id}/test`, { method: "POST" });
if (!response.ok) {
const body = (await response.json().catch(() => null)) as null | { error?: string };
throw new Error(body?.error ?? `HTTP ${response.status}`);
}
const data = (await response.json()) as ProviderTestResultResponse;
return data.providerTestResponse;
}
export async function updateProvider(id: string, data: UpdateProviderRequest): Promise<Provider> {
const response = await fetch(`/api/providers/${id}`, {
body: JSON.stringify(data),
@@ -118,26 +108,6 @@ export function useDeleteProvider() {
});
}
export function useDisableProvider() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: disableProvider,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: PROVIDERS_KEY });
},
});
}
export function useEnableProvider() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: enableProvider,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: PROVIDERS_KEY });
},
});
}
export function useProvider(id: string) {
return useQuery({
enabled: !!id,
@@ -153,15 +123,16 @@ export function useProviderList(params: { keyword?: string; page?: number; pageS
});
}
export function useTestProviderConfig() {
return useMutation({
mutationFn: testProviderConfig,
export function useProviderOptions() {
return useQuery({
queryFn: fetchProviderOptions,
queryKey: [...PROVIDERS_KEY, "options"],
});
}
export function useTestProviderConnection() {
export function useTestProviderConfig() {
return useMutation({
mutationFn: testProviderConnection,
mutationFn: testProviderConfig,
});
}

View File

@@ -5,8 +5,9 @@ import type {
CreateModelRequest,
Model,
ModelCapability,
Provider,
ProviderTestResponse,
ModelTestResponse,
ProviderOption,
TestModelRequest,
UpdateModelRequest,
} from "../../../../shared/api";
@@ -26,11 +27,15 @@ interface ModelFormModalProps {
onOpenChange: (open: boolean) => void;
onUpdate: (args: { data: UpdateModelRequest; id: string }) => Promise<unknown>;
open: boolean;
providers: Provider[];
providers: ProviderOption[];
providersError: Error | null;
providersLoading: boolean;
submitting: boolean;
testConnection?: (providerId: string) => Promise<ProviderTestResponse>;
testModelConnection?: (data: TestModelRequest) => Promise<ModelTestResponse>;
}
const DEFAULT_CAPABILITIES: ModelCapability[] = ["text", "reasoning"];
const CAPABILITY_OPTIONS: Array<{ label: string; value: ModelCapability }> = [
{ label: "文本", value: "text" },
{ label: "推理", value: "reasoning" },
@@ -50,8 +55,10 @@ export function ModelFormModal({
onUpdate,
open,
providers,
providersError,
providersLoading,
submitting,
testConnection,
testModelConnection,
}: ModelFormModalProps) {
const { message } = AntApp.useApp();
const [form] = Form.useForm<FormValues>();
@@ -70,6 +77,7 @@ export function ModelFormModal({
});
} else {
form.resetFields();
form.setFieldsValue({ capabilities: DEFAULT_CAPABILITIES });
}
}, [editingModel, form, open]);
@@ -109,15 +117,20 @@ export function ModelFormModal({
};
const handleTest = async () => {
if (!testConnection) return;
if (!testModelConnection) return;
const providerId: unknown = form.getFieldValue("providerId");
const modelId: unknown = form.getFieldValue("modelId");
if (typeof providerId !== "string" || !providerId) {
message.warning("请先选择供应商");
return;
}
if (typeof modelId !== "string" || !modelId) {
message.warning("请先输入模型 ID");
return;
}
setTesting(true);
try {
const result = await testConnection(providerId);
const result = await testModelConnection({ modelId, providerId });
if (result.ok) {
message.success(result.message);
} else {
@@ -130,7 +143,7 @@ export function ModelFormModal({
}
};
const providerOptions = providers.filter((p) => p.enabled).map((p) => ({ label: p.name, value: p.id }));
const providerOptions = providers.map((p) => ({ label: p.name, value: p.id }));
return (
<Modal
@@ -152,7 +165,15 @@ export function ModelFormModal({
<Input placeholder="请输入模型名称" />
</Form.Item>
<Form.Item label="所属供应商" name="providerId" rules={[{ message: "请选择供应商", required: true }]}>
<Select options={providerOptions} placeholder="请选择供应商" />
<Select
loading={providersLoading}
notFoundContent={getProviderNotFoundContent(providersLoading, providersError)}
optionFilterProp="label"
options={providerOptions}
placeholder="请选择供应商"
showSearch
status={providersError ? "error" : undefined}
/>
</Form.Item>
<Form.Item
label="模型 ID"
@@ -163,22 +184,28 @@ export function ModelFormModal({
</Form.Item>
<Form.Item label="能力标签" name="capabilities" rules={[{ message: "请至少选择一个能力标签", required: true }]}>
<Checkbox.Group>
<Row>
<Row gutter={[8, 8]}>
{CAPABILITY_OPTIONS.map((opt) => (
<Col key={opt.value} span={12}>
<Col key={opt.value} md={8} sm={12} xs={24}>
<Checkbox value={opt.value}>{opt.label}</Checkbox>
</Col>
))}
</Row>
</Checkbox.Group>
</Form.Item>
<Form.Item label="上下文长度" name="contextLength">
<InputNumber placeholder="可选" />
</Form.Item>
<Form.Item label="最大输出 Token" name="maxOutputTokens">
<InputNumber placeholder="可选" />
</Form.Item>
{testConnection && (
<Row gutter={16}>
<Col sm={12} xs={24}>
<Form.Item label="上下文长度" name="contextLength" rules={[positiveIntegerRule("上下文长度")]}>
<InputNumber min={1} placeholder="可选" precision={0} styles={{ root: { width: "100%" } }} />
</Form.Item>
</Col>
<Col sm={12} xs={24}>
<Form.Item label="最大输出 Token" name="maxOutputTokens" rules={[positiveIntegerRule("最大输出 Token")]}>
<InputNumber min={1} placeholder="可选" precision={0} styles={{ root: { width: "100%" } }} />
</Form.Item>
</Col>
</Row>
{testModelConnection && (
<Form.Item>
<Space>
<Button loading={testing} onClick={() => void handleTest()}>
@@ -191,3 +218,19 @@ export function ModelFormModal({
</Modal>
);
}
function getProviderNotFoundContent(loading: boolean, error: Error | null): string {
if (loading) return "正在加载供应商";
if (error) return `供应商加载失败:${error.message}`;
return "暂无供应商,请先新建供应商";
}
function positiveIntegerRule(label: string) {
return {
validator(_: unknown, value: null | number | undefined) {
if (value === undefined || value === null) return Promise.resolve();
if (Number.isInteger(value) && value > 0) return Promise.resolve();
return Promise.reject(new Error(`${label}必须为正整数`));
},
};
}

View File

@@ -1,21 +1,19 @@
import type { ColumnsType } from "antd/es/table";
import { CheckCircleOutlined, DeleteOutlined, EditOutlined, StopOutlined } from "@ant-design/icons";
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
import { App as AntApp, Button, Popconfirm, Space, Table, Tag } from "antd";
import type { Model, ModelListResponse, Provider } from "../../../../shared/api";
import type { Model, ModelListResponse, ProviderOption } from "../../../../shared/api";
interface ModelTableProps {
data: ModelListResponse | undefined;
loading: boolean;
onDelete: (id: string) => Promise<unknown>;
onDisable: (id: string) => Promise<unknown>;
onEdit: (model: Model) => void;
onEnable: (id: string) => Promise<unknown>;
onPageChange: (page: number, pageSize: number) => void;
page: number;
pageSize: number;
providers: Provider[];
providers: ProviderOption[];
}
const CAPABILITY_LABELS: Record<string, string> = {
@@ -29,13 +27,12 @@ const CAPABILITY_LABELS: Record<string, string> = {
"video-recognition": "视频识别",
};
function getProviderName(providerId: string, providers: Provider[]): string {
function getProviderName(providerId: string, providers: ProviderOption[]): string {
return providers.find((p) => p.id === providerId)?.name ?? providerId;
}
const COLUMNS: ColumnsType<Model> = [
{ dataIndex: "name", ellipsis: true, title: "模型名称", width: 160 },
{ dataIndex: "modelId", ellipsis: true, title: "模型 ID", width: 180 },
{ dataIndex: "name", ellipsis: true, title: "名称", width: 180 },
{
dataIndex: "providerId",
ellipsis: true,
@@ -46,28 +43,13 @@ const COLUMNS: ColumnsType<Model> = [
dataIndex: "capabilities",
render: (value: string[]) =>
value.length > 0 ? (
<Space size={[0, 4]} wrap>
<Space size={[4, 4]} wrap>
{value.map((c) => (
<Tag key={c}>{CAPABILITY_LABELS[c] ?? c}</Tag>
))}
</Space>
) : null,
title: "能力",
width: 200,
},
{
align: "center",
dataIndex: "enabled",
render: (value: boolean) => (value ? <Tag color="blue"></Tag> : <Tag></Tag>),
title: "状态",
width: 100,
},
{
align: "center",
dataIndex: "createdAt",
render: (_value: unknown, record: Model) => formatDatetime(record.createdAt),
title: "创建时间",
width: 185,
},
];
@@ -75,9 +57,7 @@ export function ModelTable({
data,
loading,
onDelete,
onDisable,
onEdit,
onEnable,
onPageChange,
page,
pageSize,
@@ -85,24 +65,6 @@ export function ModelTable({
}: ModelTableProps) {
const { message } = AntApp.useApp();
const handleEnable = async (id: string) => {
try {
await onEnable(id);
message.success("模型已启用");
} catch (err) {
message.error((err as Error).message);
}
};
const handleDisable = async (id: string) => {
try {
await onDisable(id);
message.success("模型已禁用");
} catch (err) {
message.error((err as Error).message);
}
};
const handleDelete = async (id: string) => {
try {
await onDelete(id);
@@ -123,23 +85,11 @@ export function ModelTable({
const operationColumn: ColumnsType<Model>[number] = {
dataIndex: "op",
fixed: "right",
render: (_value: unknown, record: Model) => (
<Space size="small">
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
</Button>
{record.enabled ? (
<Popconfirm onConfirm={() => void handleDisable(record.id)} title="确认禁用此模型?">
<Button color="orange" icon={<StopOutlined />} size="small" variant="link">
</Button>
</Popconfirm>
) : (
<Button icon={<CheckCircleOutlined />} onClick={() => void handleEnable(record.id)} size="small" type="link">
</Button>
)}
<Popconfirm
description="此操作不可恢复。"
onConfirm={() => void handleDelete(record.id)}
@@ -152,7 +102,7 @@ export function ModelTable({
</Space>
),
title: "操作",
width: 220,
width: 180,
};
return (
@@ -169,13 +119,6 @@ export function ModelTable({
total: data?.total ?? 0,
}}
rowKey="id"
scroll={{ x: 1100 }}
/>
);
}
function formatDatetime(dateStr: string): string {
const d = new Date(dateStr);
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}

View File

@@ -1,34 +0,0 @@
import { PlusOutlined } from "@ant-design/icons";
import { Button, Flex, Input } from "antd";
import { useState } from "react";
interface ModelToolbarProps {
keyword: string;
onSearch: (value: string) => void;
onSearchClear: () => void;
openCreateDialog: () => void;
}
export function ModelToolbar({ keyword, onSearch, onSearchClear, openCreateDialog }: ModelToolbarProps) {
const [draftKeyword, setDraftKeyword] = useState(keyword);
return (
<Flex align="center" gap="small" justify="space-between" wrap="wrap">
<Input.Search
allowClear
enterButton="搜索"
onChange={(event) => setDraftKeyword(event.target.value)}
onClear={() => {
setDraftKeyword("");
onSearchClear();
}}
onSearch={(value) => onSearch(value)}
placeholder="搜索模型名称或 ID"
value={draftKeyword}
/>
<Button icon={<PlusOutlined />} onClick={openCreateDialog} type="primary">
</Button>
</Flex>
);
}

View File

@@ -0,0 +1,53 @@
import { PlusOutlined } from "@ant-design/icons";
import { Button, Flex, Input, Tabs } from "antd";
import { useState } from "react";
interface ModelsToolbarProps {
activeTab: string;
keyword: string;
onSearch: (value: string) => void;
onSearchClear: () => void;
onTabChange: (key: string) => void;
openCreateDialog: () => void;
}
const TAB_ITEMS = [
{ key: "models", label: "模型" },
{ key: "providers", label: "供应商" },
];
export function ModelsToolbar({
activeTab,
keyword,
onSearch,
onSearchClear,
onTabChange,
openCreateDialog,
}: ModelsToolbarProps) {
const [draftKeyword, setDraftKeyword] = useState(keyword);
const placeholder = activeTab === "providers" ? "搜索供应商名称" : "搜索模型名称或 ID";
const createLabel = activeTab === "providers" ? "新建供应商" : "新建模型";
return (
<Flex align="center" gap="var(--ant-margin-lg)" justify="space-between" wrap="wrap">
<Tabs activeKey={activeTab} items={TAB_ITEMS} onChange={onTabChange} />
<Flex align="center" gap="small">
<Input.Search
allowClear
enterButton="搜索"
onChange={(event) => setDraftKeyword(event.target.value)}
onClear={() => {
setDraftKeyword("");
onSearchClear();
}}
onSearch={(value) => onSearch(value)}
placeholder={placeholder}
value={draftKeyword}
/>
<Button icon={<PlusOutlined />} onClick={openCreateDialog} type="primary">
{createLabel}
</Button>
</Flex>
</Flex>
);
}

View File

@@ -1,25 +1,16 @@
import type { ColumnsType } from "antd/es/table";
import {
CheckCircleOutlined,
DeleteOutlined,
EditOutlined,
StopOutlined,
ThunderboltOutlined,
} from "@ant-design/icons";
import { App as AntApp, Button, Popconfirm, Space, Table, Tag, Tooltip } from "antd";
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
import { App as AntApp, Button, Popconfirm, Space, Table } from "antd";
import type { Provider, ProviderListResponse, ProviderTestResponse } from "../../../../shared/api";
import type { Provider, ProviderListResponse } from "../../../../shared/api";
interface ProviderTableProps {
data: ProviderListResponse | undefined;
loading: boolean;
onDelete: (id: string) => Promise<unknown>;
onDisable: (id: string) => Promise<unknown>;
onEdit: (provider: Provider) => void;
onEnable: (id: string) => Promise<unknown>;
onPageChange: (page: number, pageSize: number) => void;
onTest: (id: string) => Promise<ProviderTestResponse>;
page: number;
pageSize: number;
}
@@ -31,63 +22,19 @@ const TYPE_LABELS: Record<Provider["type"], string> = {
};
const COLUMNS: ColumnsType<Provider> = [
{ dataIndex: "name", ellipsis: true, title: "供应商名称", width: 160 },
{ dataIndex: "name", ellipsis: true, title: "名称", width: 180 },
{
align: "center",
dataIndex: "type",
render: (value: Provider["type"]) => TYPE_LABELS[value] ?? value,
title: "类型",
width: 130,
width: 140,
},
{ dataIndex: "baseUrl", ellipsis: true, title: "Base URL" },
{
align: "center",
dataIndex: "enabled",
render: (value: boolean) => (value ? <Tag color="blue"></Tag> : <Tag></Tag>),
title: "状态",
width: 100,
},
{
align: "center",
dataIndex: "createdAt",
render: (_value: unknown, record: Provider) => formatDatetime(record.createdAt),
title: "创建时间",
width: 185,
},
];
export function ProviderTable({
data,
loading,
onDelete,
onDisable,
onEdit,
onEnable,
onPageChange,
onTest,
page,
pageSize,
}: ProviderTableProps) {
export function ProviderTable({ data, loading, onDelete, onEdit, onPageChange, page, pageSize }: ProviderTableProps) {
const { message } = AntApp.useApp();
const handleEnable = async (id: string) => {
try {
await onEnable(id);
message.success("供应商已启用");
} catch (err) {
message.error((err as Error).message);
}
};
const handleDisable = async (id: string) => {
try {
await onDisable(id);
message.success("供应商已禁用");
} catch (err) {
message.error((err as Error).message);
}
};
const handleDelete = async (id: string) => {
try {
await onDelete(id);
@@ -97,49 +44,15 @@ export function ProviderTable({
}
};
const handleTest = async (id: string) => {
try {
const result = await onTest(id);
if (result.ok) {
message.success(result.message);
} else {
message.error(result.message);
}
} catch (err) {
message.error((err as Error).message);
}
};
const operationColumn: ColumnsType<Provider>[number] = {
dataIndex: "op",
fixed: "right",
render: (_value: unknown, record: Provider) => (
<Space size="small">
<Tooltip title="测试连接">
<Button
aria-label="测试连接"
icon={<ThunderboltOutlined />}
onClick={() => void handleTest(record.id)}
size="small"
type="link"
/>
</Tooltip>
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
</Button>
{record.enabled ? (
<Popconfirm onConfirm={() => void handleDisable(record.id)} title="确认禁用此供应商?">
<Button color="orange" icon={<StopOutlined />} size="small" variant="link">
</Button>
</Popconfirm>
) : (
<Button icon={<CheckCircleOutlined />} onClick={() => void handleEnable(record.id)} size="small" type="link">
</Button>
)}
<Popconfirm
description="该供应商下存在模型时无法删除。"
description="该供应商下存在模型时无法删除,请先删除或迁移相关模型。"
onConfirm={() => void handleDelete(record.id)}
title="确认删除此供应商?"
>
@@ -150,7 +63,7 @@ export function ProviderTable({
</Space>
),
title: "操作",
width: 280,
width: 180,
};
return (
@@ -167,13 +80,6 @@ export function ProviderTable({
total: data?.total ?? 0,
}}
rowKey="id"
scroll={{ x: 900 }}
/>
);
}
function formatDatetime(dateStr: string): string {
const d = new Date(dateStr);
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}

View File

@@ -1,34 +0,0 @@
import { PlusOutlined } from "@ant-design/icons";
import { Button, Flex, Input } from "antd";
import { useState } from "react";
interface ProviderToolbarProps {
keyword: string;
onSearch: (value: string) => void;
onSearchClear: () => void;
openCreateDialog: () => void;
}
export function ProviderToolbar({ keyword, onSearch, onSearchClear, openCreateDialog }: ProviderToolbarProps) {
const [draftKeyword, setDraftKeyword] = useState(keyword);
return (
<Flex align="center" gap="small" justify="space-between" wrap="wrap">
<Input.Search
allowClear
enterButton="搜索"
onChange={(event) => setDraftKeyword(event.target.value)}
onClear={() => {
setDraftKeyword("");
onSearchClear();
}}
onSearch={(value) => onSearch(value)}
placeholder="搜索供应商名称"
value={draftKeyword}
/>
<Button icon={<PlusOutlined />} onClick={openCreateDialog} type="primary">
</Button>
</Flex>
);
}

View File

@@ -1,35 +1,31 @@
import { Flex, Tabs } from "antd";
import { Flex } from "antd";
import { useState } from "react";
import type { Model, Provider } from "../../../shared/api";
import type { Model, Provider, TestModelRequest } from "../../../shared/api";
import {
useCreateModel,
useDeleteModel,
useDisableModel,
useEnableModel,
useModelList,
useTestModelConnection,
useUpdateModel,
} from "../../hooks/use-models";
import {
useCreateProvider,
useDeleteProvider,
useDisableProvider,
useEnableProvider,
useProviderList,
useProviderOptions,
useTestProviderConfig,
useTestProviderConnection,
useUpdateProvider,
} from "../../hooks/use-providers";
import { ModelFormModal } from "./components/ModelFormModal";
import { ModelsToolbar } from "./components/ModelsToolbar";
import { ModelTable } from "./components/ModelTable";
import { ModelToolbar } from "./components/ModelToolbar";
import { ProviderFormModal } from "./components/ProviderFormModal";
import { ProviderTable } from "./components/ProviderTable";
import { ProviderToolbar } from "./components/ProviderToolbar";
export function ModelsPage() {
const [activeTab, setActiveTab] = useState<string>("providers");
const [activeTab, setActiveTab] = useState<string>("models");
const [providerPage, setProviderPage] = useState(1);
const [providerPageSize, setProviderPageSize] = useState(20);
@@ -49,10 +45,12 @@ export function ModelsPage() {
pageSize: providerPageSize,
});
const { data: modelProviderData, isLoading: modelProviderLoading } = useProviderList({
page: 1,
pageSize: 1000,
});
const {
data: providerOptionsData,
error: providerOptionsError,
isError: providerOptionsIsError,
isLoading: providerOptionsLoading,
} = useProviderOptions();
const { data: modelData, isLoading: modelLoading } = useModelList({
keyword: modelKeyword || undefined,
@@ -63,69 +61,81 @@ export function ModelsPage() {
const createProviderMutation = useCreateProvider();
const updateProviderMutation = useUpdateProvider();
const deleteProviderMutation = useDeleteProvider();
const enableProviderMutation = useEnableProvider();
const disableProviderMutation = useDisableProvider();
const testProviderMutation = useTestProviderConnection();
const testProviderConfigMutation = useTestProviderConfig();
const createModelMutation = useCreateModel();
const updateModelMutation = useUpdateModel();
const deleteModelMutation = useDeleteModel();
const enableModelMutation = useEnableModel();
const disableModelMutation = useDisableModel();
const testModelMutation = useTestModelConnection();
const isProviderSubmitting = createProviderMutation.isPending || updateProviderMutation.isPending;
const isProviderActionPending =
deleteProviderMutation.isPending || enableProviderMutation.isPending || disableProviderMutation.isPending;
const isProviderActionPending = deleteProviderMutation.isPending;
const isModelSubmitting = createModelMutation.isPending || updateModelMutation.isPending;
const isModelActionPending =
deleteModelMutation.isPending || enableModelMutation.isPending || disableModelMutation.isPending;
const modelProviders = modelProviderData?.items ?? [];
const isModelActionPending = deleteModelMutation.isPending;
const modelProviders = providerOptionsData?.items ?? [];
const currentKeyword = activeTab === "providers" ? providerKeyword : modelKeyword;
const handleSearch =
activeTab === "providers"
? (value: string) => {
setProviderKeyword(value);
setProviderPage(1);
}
: (value: string) => {
setModelKeyword(value);
setModelPage(1);
};
const handleSearchClear =
activeTab === "providers"
? () => {
setProviderKeyword("");
setProviderPage(1);
}
: () => {
setModelKeyword("");
setModelPage(1);
};
const handleOpenCreate =
activeTab === "providers"
? () => {
setEditingProvider(null);
setProviderDialogOpen(true);
}
: () => {
setEditingModel(null);
setModelDialogOpen(true);
};
return (
<Flex flex={1} gap="var(--ant-margin-lg)" vertical>
<Tabs
activeKey={activeTab}
items={[
{ key: "providers", label: "供应商" },
{ key: "models", label: "模型" },
]}
onChange={(key) => setActiveTab(key)}
<ModelsToolbar
activeTab={activeTab}
key={activeTab}
keyword={currentKeyword}
onSearch={handleSearch}
onSearchClear={handleSearchClear}
onTabChange={(key) => setActiveTab(key)}
openCreateDialog={handleOpenCreate}
/>
{activeTab === "providers" && (
<>
<ProviderToolbar
keyword={providerKeyword}
onSearch={(value) => {
setProviderKeyword(value);
setProviderPage(1);
}}
onSearchClear={() => {
setProviderKeyword("");
setProviderPage(1);
}}
openCreateDialog={() => {
setEditingProvider(null);
setProviderDialogOpen(true);
}}
/>
<ProviderTable
data={providerData}
loading={providerLoading || isProviderActionPending}
onDelete={(id) => deleteProviderMutation.mutateAsync(id)}
onDisable={(id) => disableProviderMutation.mutateAsync(id)}
onEdit={(provider) => {
setEditingProvider(provider);
setProviderDialogOpen(true);
}}
onEnable={(id) => enableProviderMutation.mutateAsync(id)}
onPageChange={(p, ps) => {
setProviderPage(p);
setProviderPageSize(ps);
}}
onTest={(id) => testProviderMutation.mutateAsync(id)}
page={providerPage}
pageSize={providerPageSize}
/>
@@ -137,38 +147,21 @@ export function ModelsPage() {
onTest={(data) => testProviderConfigMutation.mutateAsync(data)}
onUpdate={(args) => updateProviderMutation.mutateAsync(args)}
open={providerDialogOpen}
submitting={isProviderSubmitting || testProviderConfigMutation.isPending}
submitting={isProviderSubmitting}
/>
</>
)}
{activeTab === "models" && (
<>
<ModelToolbar
keyword={modelKeyword}
onSearch={(value) => {
setModelKeyword(value);
setModelPage(1);
}}
onSearchClear={() => {
setModelKeyword("");
setModelPage(1);
}}
openCreateDialog={() => {
setEditingModel(null);
setModelDialogOpen(true);
}}
/>
<ModelTable
data={modelData}
loading={modelLoading || modelProviderLoading || isModelActionPending}
loading={modelLoading || providerOptionsLoading || isModelActionPending}
onDelete={(id) => deleteModelMutation.mutateAsync(id)}
onDisable={(id) => disableModelMutation.mutateAsync(id)}
onEdit={(model) => {
setEditingModel(model);
setModelDialogOpen(true);
}}
onEnable={(id) => enableModelMutation.mutateAsync(id)}
onPageChange={(p, ps) => {
setModelPage(p);
setModelPageSize(ps);
@@ -185,8 +178,10 @@ export function ModelsPage() {
onUpdate={(args) => updateModelMutation.mutateAsync(args)}
open={modelDialogOpen}
providers={modelProviders}
providersError={providerOptionsIsError ? providerOptionsError : null}
providersLoading={providerOptionsLoading}
submitting={isModelSubmitting}
testConnection={(id: string) => testProviderMutation.mutateAsync(id)}
testModelConnection={(data: TestModelRequest) => testModelMutation.mutateAsync(data)}
/>
</>
)}