feat: 拆分模型/供应商为独立路由页面,侧边栏支持 SubMenu 分组
This commit is contained in:
97
src/web/features/models/ModelListPage.tsx
Normal file
97
src/web/features/models/ModelListPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
84
src/web/features/models/ProviderListPage.tsx
Normal file
84
src/web/features/models/ProviderListPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
37
src/web/features/models/components/ProviderToolbar.tsx
Normal file
37
src/web/features/models/components/ProviderToolbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user