feat: 拆分模型/供应商为独立路由页面,侧边栏支持 SubMenu 分组

This commit is contained in:
2026-06-04 11:11:32 +08:00
parent f67cfa84ef
commit 61b479e2be
13 changed files with 689 additions and 353 deletions

View File

@@ -0,0 +1,97 @@
import { Space } from "antd";
import { useState } from "react";
import type { Model, TestModelRequest } from "../../../shared/api";
import {
useCreateModel,
useDeleteModel,
useModelList,
useTestModelConnection,
useUpdateModel,
} from "../../shared/hooks/use-models";
import { useProviderOptions } from "../../shared/hooks/use-providers";
import { ModelFormModal } from "./components/ModelFormModal";
import { ModelTable } from "./components/ModelTable";
import { ModelToolbar } from "./components/ModelToolbar";
export function ModelListPage() {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [keyword, setKeyword] = useState("");
const [dialogOpen, setDialogOpen] = useState(false);
const [editingModel, setEditingModel] = useState<Model | null>(null);
const { data: modelData, isLoading: modelLoading } = useModelList({
keyword: keyword || undefined,
page,
pageSize,
});
const {
data: providerOptionsData,
error: providerOptionsError,
isError: providerOptionsIsError,
isLoading: providerOptionsLoading,
} = useProviderOptions();
const createModelMutation = useCreateModel();
const updateModelMutation = useUpdateModel();
const deleteModelMutation = useDeleteModel();
const testModelMutation = useTestModelConnection();
const isSubmitting = createModelMutation.isPending || updateModelMutation.isPending;
const isActionPending = deleteModelMutation.isPending;
const modelProviders = providerOptionsData?.items ?? [];
return (
<Space className="app-page-flex" orientation="vertical" size="large">
<ModelToolbar
keyword={keyword}
onSearch={(value) => {
setKeyword(value);
setPage(1);
}}
onSearchClear={() => {
setKeyword("");
setPage(1);
}}
openCreateDialog={() => {
setEditingModel(null);
setDialogOpen(true);
}}
/>
<ModelTable
data={modelData}
loading={modelLoading || providerOptionsLoading || isActionPending}
onDelete={(id) => deleteModelMutation.mutateAsync(id)}
onEdit={(model) => {
setEditingModel(model);
setDialogOpen(true);
}}
onPageChange={(p, ps) => {
setPage(p);
setPageSize(ps);
}}
page={page}
pageSize={pageSize}
providers={modelProviders}
/>
<ModelFormModal
editingModel={editingModel}
onCancel={() => setDialogOpen(false)}
onCreate={(data) => createModelMutation.mutateAsync(data)}
onOpenChange={setDialogOpen}
onUpdate={(args) => updateModelMutation.mutateAsync(args)}
open={dialogOpen}
providers={modelProviders}
providersError={providerOptionsIsError ? providerOptionsError : null}
providersLoading={providerOptionsLoading}
submitting={isSubmitting}
testModelConnection={(data: TestModelRequest) => testModelMutation.mutateAsync(data)}
/>
</Space>
);
}

View File

@@ -0,0 +1,84 @@
import { Space } from "antd";
import { useState } from "react";
import type { Provider } from "../../../shared/api";
import {
useCreateProvider,
useDeleteProvider,
useProviderList,
useTestProviderConfig,
useUpdateProvider,
} from "../../shared/hooks/use-providers";
import { ProviderFormModal } from "./components/ProviderFormModal";
import { ProviderTable } from "./components/ProviderTable";
import { ProviderToolbar } from "./components/ProviderToolbar";
export function ProviderListPage() {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [keyword, setKeyword] = useState("");
const [dialogOpen, setDialogOpen] = useState(false);
const [editingProvider, setEditingProvider] = useState<null | Provider>(null);
const { data: providerData, isLoading: providerLoading } = useProviderList({
keyword: keyword || undefined,
page,
pageSize,
});
const createProviderMutation = useCreateProvider();
const updateProviderMutation = useUpdateProvider();
const deleteProviderMutation = useDeleteProvider();
const testProviderConfigMutation = useTestProviderConfig();
const isSubmitting = createProviderMutation.isPending || updateProviderMutation.isPending;
const isActionPending = deleteProviderMutation.isPending;
return (
<Space className="app-page-flex" orientation="vertical" size="large">
<ProviderToolbar
keyword={keyword}
onSearch={(value) => {
setKeyword(value);
setPage(1);
}}
onSearchClear={() => {
setKeyword("");
setPage(1);
}}
openCreateDialog={() => {
setEditingProvider(null);
setDialogOpen(true);
}}
/>
<ProviderTable
data={providerData}
loading={providerLoading || isActionPending}
onDelete={(id) => deleteProviderMutation.mutateAsync(id)}
onEdit={(provider) => {
setEditingProvider(provider);
setDialogOpen(true);
}}
onPageChange={(p, ps) => {
setPage(p);
setPageSize(ps);
}}
page={page}
pageSize={pageSize}
/>
<ProviderFormModal
editingProvider={editingProvider}
onCancel={() => setDialogOpen(false)}
onCreate={(data) => createProviderMutation.mutateAsync(data)}
onOpenChange={setDialogOpen}
onTest={(data) => testProviderConfigMutation.mutateAsync(data)}
onUpdate={(args) => updateProviderMutation.mutateAsync(args)}
open={dialogOpen}
submitting={isSubmitting}
/>
</Space>
);
}

View File

@@ -1,36 +1,20 @@
import { PlusOutlined } from "@ant-design/icons";
import { Button, Flex, Input, Space, Tabs } from "antd";
import { Button, Flex, Input, Space } from "antd";
import { useState } from "react";
interface ModelsToolbarProps {
activeTab: string;
interface ModelToolbarProps {
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) {
export function ModelToolbar({ keyword, onSearch, onSearchClear, openCreateDialog }: ModelToolbarProps) {
const [draftKeyword, setDraftKeyword] = useState(keyword);
const placeholder = activeTab === "providers" ? "搜索供应商名称" : "搜索模型名称或 ID";
const createLabel = activeTab === "providers" ? "新建供应商" : "新建模型";
return (
<Flex align="center" gap="large" justify="space-between" wrap="wrap">
<Tabs activeKey={activeTab} items={TAB_ITEMS} onChange={onTabChange} />
<div />
<Space size="small">
<Input.Search
allowClear
@@ -41,11 +25,11 @@ export function ModelsToolbar({
onSearchClear();
}}
onSearch={(value) => onSearch(value)}
placeholder={placeholder}
placeholder="搜索模型名称或 ID"
value={draftKeyword}
/>
<Button icon={<PlusOutlined />} onClick={openCreateDialog} type="primary">
{createLabel}
</Button>
</Space>
</Flex>

View File

@@ -0,0 +1,37 @@
import { PlusOutlined } from "@ant-design/icons";
import { Button, Flex, Input, Space } 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="large" justify="space-between" wrap="wrap">
<div />
<Space size="small">
<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>
</Space>
</Flex>
);
}

View File

@@ -1,190 +0,0 @@
import { Space } from "antd";
import { useState } from "react";
import type { Model, Provider, TestModelRequest } from "../../../shared/api";
import {
useCreateModel,
useDeleteModel,
useModelList,
useTestModelConnection,
useUpdateModel,
} from "../../shared/hooks/use-models";
import {
useCreateProvider,
useDeleteProvider,
useProviderList,
useProviderOptions,
useTestProviderConfig,
useUpdateProvider,
} from "../../shared/hooks/use-providers";
import { ModelFormModal } from "./components/ModelFormModal";
import { ModelsToolbar } from "./components/ModelsToolbar";
import { ModelTable } from "./components/ModelTable";
import { ProviderFormModal } from "./components/ProviderFormModal";
import { ProviderTable } from "./components/ProviderTable";
export function ModelsPage() {
const [activeTab, setActiveTab] = useState<string>("models");
const [providerPage, setProviderPage] = useState(1);
const [providerPageSize, setProviderPageSize] = useState(20);
const [providerKeyword, setProviderKeyword] = useState("");
const [providerDialogOpen, setProviderDialogOpen] = useState(false);
const [editingProvider, setEditingProvider] = useState<null | Provider>(null);
const [modelPage, setModelPage] = useState(1);
const [modelPageSize, setModelPageSize] = useState(20);
const [modelKeyword, setModelKeyword] = useState("");
const [modelDialogOpen, setModelDialogOpen] = useState(false);
const [editingModel, setEditingModel] = useState<Model | null>(null);
const { data: providerData, isLoading: providerLoading } = useProviderList({
keyword: providerKeyword || undefined,
page: providerPage,
pageSize: providerPageSize,
});
const {
data: providerOptionsData,
error: providerOptionsError,
isError: providerOptionsIsError,
isLoading: providerOptionsLoading,
} = useProviderOptions();
const { data: modelData, isLoading: modelLoading } = useModelList({
keyword: modelKeyword || undefined,
page: modelPage,
pageSize: modelPageSize,
});
const createProviderMutation = useCreateProvider();
const updateProviderMutation = useUpdateProvider();
const deleteProviderMutation = useDeleteProvider();
const testProviderConfigMutation = useTestProviderConfig();
const createModelMutation = useCreateModel();
const updateModelMutation = useUpdateModel();
const deleteModelMutation = useDeleteModel();
const testModelMutation = useTestModelConnection();
const isProviderSubmitting = createProviderMutation.isPending || updateProviderMutation.isPending;
const isProviderActionPending = deleteProviderMutation.isPending;
const isModelSubmitting = createModelMutation.isPending || updateModelMutation.isPending;
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 (
<Space className="app-page-flex" orientation="vertical" size="large">
<ModelsToolbar
activeTab={activeTab}
key={activeTab}
keyword={currentKeyword}
onSearch={handleSearch}
onSearchClear={handleSearchClear}
onTabChange={(key) => setActiveTab(key)}
openCreateDialog={handleOpenCreate}
/>
{activeTab === "providers" && (
<>
<ProviderTable
data={providerData}
loading={providerLoading || isProviderActionPending}
onDelete={(id) => deleteProviderMutation.mutateAsync(id)}
onEdit={(provider) => {
setEditingProvider(provider);
setProviderDialogOpen(true);
}}
onPageChange={(p, ps) => {
setProviderPage(p);
setProviderPageSize(ps);
}}
page={providerPage}
pageSize={providerPageSize}
/>
<ProviderFormModal
editingProvider={editingProvider}
onCancel={() => setProviderDialogOpen(false)}
onCreate={(data) => createProviderMutation.mutateAsync(data)}
onOpenChange={setProviderDialogOpen}
onTest={(data) => testProviderConfigMutation.mutateAsync(data)}
onUpdate={(args) => updateProviderMutation.mutateAsync(args)}
open={providerDialogOpen}
submitting={isProviderSubmitting}
/>
</>
)}
{activeTab === "models" && (
<>
<ModelTable
data={modelData}
loading={modelLoading || providerOptionsLoading || isModelActionPending}
onDelete={(id) => deleteModelMutation.mutateAsync(id)}
onEdit={(model) => {
setEditingModel(model);
setModelDialogOpen(true);
}}
onPageChange={(p, ps) => {
setModelPage(p);
setModelPageSize(ps);
}}
page={modelPage}
pageSize={modelPageSize}
providers={modelProviders}
/>
<ModelFormModal
editingModel={editingModel}
onCancel={() => setModelDialogOpen(false)}
onCreate={(data) => createModelMutation.mutateAsync(data)}
onOpenChange={setModelDialogOpen}
onUpdate={(args) => updateModelMutation.mutateAsync(args)}
open={modelDialogOpen}
providers={modelProviders}
providersError={providerOptionsIsError ? providerOptionsError : null}
providersLoading={providerOptionsLoading}
submitting={isModelSubmitting}
testModelConnection={(data: TestModelRequest) => testModelMutation.mutateAsync(data)}
/>
</>
)}
</Space>
);
}