You've already forked DataMate
feat: fix the problem in the Operator Market frontend pages
This commit is contained in:
@@ -1,392 +1,392 @@
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Table,
|
||||
Tooltip,
|
||||
Popconfirm,
|
||||
message, Switch,
|
||||
} from "antd";
|
||||
import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
createModelUsingPost,
|
||||
deleteModelByIdUsingDelete,
|
||||
queryModelListUsingGet,
|
||||
queryModelProvidersUsingGet,
|
||||
updateModelByIdUsingPut,
|
||||
} from "./settings.apis";
|
||||
|
||||
export interface ModelI {
|
||||
id: string;
|
||||
modelName: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
type: string;
|
||||
isEnabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: string;
|
||||
updatedBy: string;
|
||||
}
|
||||
|
||||
interface ProviderI {
|
||||
id: string;
|
||||
modelName: string;
|
||||
value: string;
|
||||
label: string;
|
||||
baseUrl: string;
|
||||
provider: string;
|
||||
apiKey: string;
|
||||
type: string;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
export default function ModelAccess() {
|
||||
const [form] = Form.useForm();
|
||||
const [showModelDialog, setShowModelDialog] = useState(false);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [newModel, setNewModel] = useState({
|
||||
name: "",
|
||||
provider: "openai",
|
||||
model: "",
|
||||
apiKey: "",
|
||||
endpoint: "",
|
||||
});
|
||||
const [typeOptions] = useState([
|
||||
{ value: "CHAT", label: "CHAT" },
|
||||
{ value: "EMBEDDING", label: "EMBEDDING" },
|
||||
]);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
} = useFetchData(queryModelListUsingGet);
|
||||
|
||||
const handleAddModel = async () => {
|
||||
try {
|
||||
const formValues = await form.validateFields();
|
||||
const fn = isEditMode
|
||||
? () => updateModelByIdUsingPut(newModel.id, formValues)
|
||||
: () => createModelUsingPost(formValues);
|
||||
await fn();
|
||||
setShowModelDialog(false);
|
||||
fetchData();
|
||||
message.success("模型添加成功");
|
||||
} catch (error) {
|
||||
message.error(`${error?.data?.message}:${error?.data?.data}`);
|
||||
}
|
||||
};
|
||||
const [providerOptions, setProviderOptions] = useState<ProviderI[]>([]);
|
||||
|
||||
const fetchProviderOptions = async () => {
|
||||
const { data } = await queryModelProvidersUsingGet();
|
||||
setProviderOptions(
|
||||
data.map((provider: ProviderI) => ({
|
||||
...provider,
|
||||
value: provider.provider,
|
||||
label: provider.provider,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const generateApiKey = () => {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "sk-";
|
||||
for (let i = 0; i < 48; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const handleDeleteModel = async (modelId: string) => {
|
||||
await deleteModelByIdUsingDelete(modelId);
|
||||
fetchData();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchProviderOptions();
|
||||
}, []);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "模型名称",
|
||||
dataIndex: "modelName",
|
||||
key: "modelName",
|
||||
fixed: "left",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "模型提供商",
|
||||
dataIndex: "provider",
|
||||
key: "provider",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "模型类型",
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
fixed: "right" as const,
|
||||
ellipsis: true,
|
||||
render: (_: any, record: ModelI) => {
|
||||
return [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => {
|
||||
setIsEditMode(true);
|
||||
setNewModel(record);
|
||||
form.setFieldsValue(record);
|
||||
setShowModelDialog(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
confirm: {
|
||||
title: "确定要删除该任务吗?此操作不可撤销。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
onClick: () => handleDeleteModel(record.id),
|
||||
},
|
||||
].map((op) => {
|
||||
const button = (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op?.danger}
|
||||
onClick={() => op.onClick(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
if (op.confirm) {
|
||||
return (
|
||||
<Popconfirm
|
||||
key={op.key}
|
||||
title={op.confirm.title}
|
||||
okText={op.confirm.okText}
|
||||
cancelText={op.confirm.cancelText}
|
||||
okType={op.danger ? "danger" : "primary"}
|
||||
onConfirm={() => op.onClick(record)}
|
||||
>
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button type="text" icon={op.icon} danger={op?.danger} />
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
return button;
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-top justify-between">
|
||||
<h2 className="text-lg font-medium mb-4">模型接入</h2>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setIsEditMode(false);
|
||||
form.resetFields();
|
||||
setNewModel({
|
||||
name: "",
|
||||
provider: "",
|
||||
model: "",
|
||||
apiKey: "",
|
||||
endpoint: "",
|
||||
});
|
||||
setShowModelDialog(true);
|
||||
}}
|
||||
>
|
||||
添加模型
|
||||
</Button>
|
||||
</div>
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={(newSearchTerm) =>
|
||||
setSearchParams((prev) => ({
|
||||
...prev,
|
||||
keyword: newSearchTerm,
|
||||
current: 1,
|
||||
}))
|
||||
}
|
||||
searchPlaceholder="搜索模型描述..."
|
||||
filters={[
|
||||
{
|
||||
key: "provider",
|
||||
label: "模型提供商",
|
||||
options: [{ value: "all", label: "全部" }, ...providerOptions],
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
label: "模型类型",
|
||||
options: [{ value: "all", label: "全部" }, ...typeOptions],
|
||||
},
|
||||
]}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
showViewToggle={false}
|
||||
onReload={fetchData}
|
||||
onClearFilters={() =>
|
||||
setSearchParams((prev) => ({
|
||||
...prev,
|
||||
filters: {},
|
||||
}))
|
||||
}
|
||||
className="mb-4"
|
||||
/>
|
||||
<div className="flex flex-col h-[calc(100vh-12rem)]">
|
||||
<Card className="flex-1 overflow-auto">
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
pagination={pagination}
|
||||
scroll={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
<Modal
|
||||
open={showModelDialog}
|
||||
onCancel={() => setShowModelDialog(false)}
|
||||
title={isEditMode ? "编辑模型" : "添加模型"}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setShowModelDialog(false)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="ok" type="primary" onClick={handleAddModel}>
|
||||
确定
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onValuesChange={(changedValues) => {
|
||||
setNewModel({ ...newModel, ...changedValues });
|
||||
}}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label="服务提供商"
|
||||
required
|
||||
rules={[{ required: true, message: "请选择服务提供商" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="选择服务提供商"
|
||||
options={providerOptions}
|
||||
onChange={(value) => {
|
||||
const selectedProvider = providerOptions.find(
|
||||
(p) => p.value === value
|
||||
);
|
||||
form.setFieldsValue({ baseUrl: selectedProvider?.baseUrl });
|
||||
}}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="baseUrl"
|
||||
label="接口地址"
|
||||
required
|
||||
rules={[
|
||||
{ required: true, message: "请输入接口地址" },
|
||||
{
|
||||
pattern: /^https?:\/\/.+/,
|
||||
message: "请输入有效的URL地址,必须以http://或https://开头",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="输入接口地址,如:https://api.openai.com" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="modelName"
|
||||
label="模型名称"
|
||||
required
|
||||
tooltip="请输入模型名称"
|
||||
rules={[{ required: true, message: "请输入模型名称" }]}
|
||||
>
|
||||
<Input placeholder="输入模型名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="apiKey"
|
||||
label="API密钥"
|
||||
required
|
||||
rules={[{ required: true, message: "请输入API密钥" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="输入或生成API密钥"
|
||||
addonAfter={
|
||||
<ReloadOutlined
|
||||
onClick={() => {
|
||||
form.setFieldsValue({ apiKey: generateApiKey() });
|
||||
setNewModel({ ...newModel, apiKey: generateApiKey() });
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label="模型类型"
|
||||
required
|
||||
rules={[{ required: true, message: "请选择模型类型" }]}
|
||||
>
|
||||
<Select options={typeOptions} placeholder="选择模型类型"></Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="isDefault"
|
||||
label="设为默认"
|
||||
required
|
||||
tooltip="当模型类型下仅有一个模型服务时,自动将其设为默认值。"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Table,
|
||||
Tooltip,
|
||||
Popconfirm,
|
||||
message, Switch,
|
||||
} from "antd";
|
||||
import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
createModelUsingPost,
|
||||
deleteModelByIdUsingDelete,
|
||||
queryModelListUsingGet,
|
||||
queryModelProvidersUsingGet,
|
||||
updateModelByIdUsingPut,
|
||||
} from "./settings.apis";
|
||||
|
||||
export interface ModelI {
|
||||
id: string;
|
||||
modelName: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
type: string;
|
||||
isEnabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: string;
|
||||
updatedBy: string;
|
||||
}
|
||||
|
||||
interface ProviderI {
|
||||
id: string;
|
||||
modelName: string;
|
||||
value: string;
|
||||
label: string;
|
||||
baseUrl: string;
|
||||
provider: string;
|
||||
apiKey: string;
|
||||
type: string;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
export default function ModelAccess() {
|
||||
const [form] = Form.useForm();
|
||||
const [showModelDialog, setShowModelDialog] = useState(false);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [newModel, setNewModel] = useState({
|
||||
name: "",
|
||||
provider: "openai",
|
||||
model: "",
|
||||
apiKey: "",
|
||||
endpoint: "",
|
||||
});
|
||||
const [typeOptions] = useState([
|
||||
{ value: "CHAT", label: "CHAT" },
|
||||
{ value: "EMBEDDING", label: "EMBEDDING" },
|
||||
]);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
} = useFetchData(queryModelListUsingGet);
|
||||
|
||||
const handleAddModel = async () => {
|
||||
try {
|
||||
const formValues = await form.validateFields();
|
||||
const fn = isEditMode
|
||||
? () => updateModelByIdUsingPut(newModel.id, formValues)
|
||||
: () => createModelUsingPost(formValues);
|
||||
await fn();
|
||||
setShowModelDialog(false);
|
||||
fetchData();
|
||||
message.success("模型添加成功");
|
||||
} catch (error) {
|
||||
message.error(`${error?.data?.message}:${error?.data?.data}`);
|
||||
}
|
||||
};
|
||||
const [providerOptions, setProviderOptions] = useState<ProviderI[]>([]);
|
||||
|
||||
const fetchProviderOptions = async () => {
|
||||
const { data } = await queryModelProvidersUsingGet();
|
||||
setProviderOptions(
|
||||
data.map((provider: ProviderI) => ({
|
||||
...provider,
|
||||
value: provider.provider,
|
||||
label: provider.provider,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const generateApiKey = () => {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "sk-";
|
||||
for (let i = 0; i < 48; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const handleDeleteModel = async (modelId: string) => {
|
||||
await deleteModelByIdUsingDelete(modelId);
|
||||
fetchData();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchProviderOptions();
|
||||
}, []);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "模型名称",
|
||||
dataIndex: "modelName",
|
||||
key: "modelName",
|
||||
fixed: "left",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "模型提供商",
|
||||
dataIndex: "provider",
|
||||
key: "provider",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "模型类型",
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
fixed: "right" as const,
|
||||
ellipsis: true,
|
||||
render: (_: any, record: ModelI) => {
|
||||
return [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => {
|
||||
setIsEditMode(true);
|
||||
setNewModel(record);
|
||||
form.setFieldsValue(record);
|
||||
setShowModelDialog(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
confirm: {
|
||||
title: "确定要删除该任务吗?此操作不可撤销。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
onClick: () => handleDeleteModel(record.id),
|
||||
},
|
||||
].map((op) => {
|
||||
const button = (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op?.danger}
|
||||
onClick={() => op.onClick(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
if (op.confirm) {
|
||||
return (
|
||||
<Popconfirm
|
||||
key={op.key}
|
||||
title={op.confirm.title}
|
||||
okText={op.confirm.okText}
|
||||
cancelText={op.confirm.cancelText}
|
||||
okType={op.danger ? "danger" : "primary"}
|
||||
onConfirm={() => op.onClick(record)}
|
||||
>
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button type="text" icon={op.icon} danger={op?.danger} />
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
return button;
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-top justify-between">
|
||||
<h2 className="text-lg font-medium mb-4">模型接入</h2>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setIsEditMode(false);
|
||||
form.resetFields();
|
||||
setNewModel({
|
||||
name: "",
|
||||
provider: "",
|
||||
model: "",
|
||||
apiKey: "",
|
||||
endpoint: "",
|
||||
});
|
||||
setShowModelDialog(true);
|
||||
}}
|
||||
>
|
||||
添加模型
|
||||
</Button>
|
||||
</div>
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={(newSearchTerm) =>
|
||||
setSearchParams((prev) => ({
|
||||
...prev,
|
||||
keyword: newSearchTerm,
|
||||
current: 1,
|
||||
}))
|
||||
}
|
||||
searchPlaceholder="搜索模型描述..."
|
||||
filters={[
|
||||
{
|
||||
key: "provider",
|
||||
label: "模型提供商",
|
||||
options: [{ value: "all", label: "全部" }, ...providerOptions],
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
label: "模型类型",
|
||||
options: [{ value: "all", label: "全部" }, ...typeOptions],
|
||||
},
|
||||
]}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
showViewToggle={false}
|
||||
onReload={fetchData}
|
||||
onClearFilters={() =>
|
||||
setSearchParams((prev) => ({
|
||||
...prev,
|
||||
filters: {},
|
||||
}))
|
||||
}
|
||||
className="mb-4"
|
||||
/>
|
||||
<div className="flex flex-col h-[calc(100vh-12rem)]">
|
||||
<Card className="flex-1 overflow-auto">
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
pagination={pagination}
|
||||
scroll={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
<Modal
|
||||
open={showModelDialog}
|
||||
onCancel={() => setShowModelDialog(false)}
|
||||
title={isEditMode ? "编辑模型" : "添加模型"}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setShowModelDialog(false)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="ok" type="primary" onClick={handleAddModel}>
|
||||
确定
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onValuesChange={(changedValues) => {
|
||||
setNewModel({ ...newModel, ...changedValues });
|
||||
}}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label="服务提供商"
|
||||
required
|
||||
rules={[{ required: true, message: "请选择服务提供商" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="选择服务提供商"
|
||||
options={providerOptions}
|
||||
onChange={(value) => {
|
||||
const selectedProvider = providerOptions.find(
|
||||
(p) => p.value === value
|
||||
);
|
||||
form.setFieldsValue({ baseUrl: selectedProvider?.baseUrl });
|
||||
}}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="baseUrl"
|
||||
label="接口地址"
|
||||
required
|
||||
rules={[
|
||||
{ required: true, message: "请输入接口地址" },
|
||||
{
|
||||
pattern: /^https?:\/\/.+/,
|
||||
message: "请输入有效的URL地址,必须以http://或https://开头",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="输入接口地址,如:https://api.openai.com" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="modelName"
|
||||
label="模型名称"
|
||||
required
|
||||
tooltip="请输入模型名称"
|
||||
rules={[{ required: true, message: "请输入模型名称" }]}
|
||||
>
|
||||
<Input placeholder="输入模型名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="apiKey"
|
||||
label="API密钥"
|
||||
required
|
||||
rules={[{ required: true, message: "请输入API密钥" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="输入或生成API密钥"
|
||||
addonAfter={
|
||||
<ReloadOutlined
|
||||
onClick={() => {
|
||||
form.setFieldsValue({ apiKey: generateApiKey() });
|
||||
setNewModel({ ...newModel, apiKey: generateApiKey() });
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label="模型类型"
|
||||
required
|
||||
rules={[{ required: true, message: "请选择模型类型" }]}
|
||||
>
|
||||
<Select options={typeOptions} placeholder="选择模型类型"></Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="isDefault"
|
||||
label="设为默认"
|
||||
required
|
||||
tooltip="当模型类型下仅有一个模型服务时,自动将其设为默认值。"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Menu } from "antd";
|
||||
import { SettingOutlined, ApiOutlined } from "@ant-design/icons";
|
||||
import { Component, X } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import SystemConfig from "./SystemConfig";
|
||||
import ModelAccess from "./ModelAccess";
|
||||
import WebhookConfig from "./WebhookConfig";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState("model-access");
|
||||
|
||||
return (
|
||||
<div className="h-screen flex">
|
||||
<div className="border-right h-full">
|
||||
{/* <h1 className="min-w-[200px] w-full border-bottom flex gap-2 text-lg font-bold text-gray-900 p-4">
|
||||
<Button icon={<X />} type="text" onClick={() => navigate(-1)} />
|
||||
设置中心
|
||||
</h1> */}
|
||||
<div className="h-full">
|
||||
<Menu
|
||||
mode="inline"
|
||||
items={[
|
||||
{
|
||||
key: "model-access",
|
||||
icon: <Component className="w-4 h-4" />,
|
||||
label: "模型接入",
|
||||
},
|
||||
{
|
||||
key: "system-config",
|
||||
icon: <SettingOutlined />,
|
||||
label: "参数配置",
|
||||
},
|
||||
{
|
||||
key: "webhook-config",
|
||||
icon: <ApiOutlined />,
|
||||
label: "Webhook",
|
||||
disabled: true,
|
||||
title: "暂未开放"
|
||||
},
|
||||
]}
|
||||
selectedKeys={[activeTab]}
|
||||
onClick={({ key }) => {
|
||||
setActiveTab(key);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 h-full p-4">
|
||||
{/* 内容区域,根据 activeTab 渲染不同的组件 */}
|
||||
{activeTab === "system-config" && <SystemConfig />}
|
||||
{activeTab === "model-access" && <ModelAccess />}
|
||||
{activeTab === "webhook-config" && <WebhookConfig />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useState } from "react";
|
||||
import { Button, Menu } from "antd";
|
||||
import { SettingOutlined, ApiOutlined } from "@ant-design/icons";
|
||||
import { Component, X } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import SystemConfig from "./SystemConfig";
|
||||
import ModelAccess from "./ModelAccess";
|
||||
import WebhookConfig from "./WebhookConfig";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState("model-access");
|
||||
|
||||
return (
|
||||
<div className="h-screen flex">
|
||||
<div className="border-right h-full">
|
||||
{/* <h1 className="min-w-[200px] w-full border-bottom flex gap-2 text-lg font-bold text-gray-900 p-4">
|
||||
<Button icon={<X />} type="text" onClick={() => navigate(-1)} />
|
||||
设置中心
|
||||
</h1> */}
|
||||
<div className="h-full">
|
||||
<Menu
|
||||
mode="inline"
|
||||
items={[
|
||||
{
|
||||
key: "model-access",
|
||||
icon: <Component className="w-4 h-4" />,
|
||||
label: "模型接入",
|
||||
},
|
||||
{
|
||||
key: "system-config",
|
||||
icon: <SettingOutlined />,
|
||||
label: "参数配置",
|
||||
},
|
||||
{
|
||||
key: "webhook-config",
|
||||
icon: <ApiOutlined />,
|
||||
label: "Webhook",
|
||||
disabled: true,
|
||||
title: "暂未开放"
|
||||
},
|
||||
]}
|
||||
selectedKeys={[activeTab]}
|
||||
onClick={({ key }) => {
|
||||
setActiveTab(key);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 h-full p-4">
|
||||
{/* 内容区域,根据 activeTab 渲染不同的组件 */}
|
||||
{activeTab === "system-config" && <SystemConfig />}
|
||||
{activeTab === "model-access" && <ModelAccess />}
|
||||
{activeTab === "webhook-config" && <WebhookConfig />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,228 +1,228 @@
|
||||
import { Input, Select, Switch, Button, Table, Spin } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getSysParamList, updateSysParamValue } from './settings.apis';
|
||||
|
||||
interface SystemParam {
|
||||
id: string;
|
||||
paramValue: string;
|
||||
description: string;
|
||||
isEnabled: boolean;
|
||||
paramType?: string;
|
||||
optionList?: string;
|
||||
isBuiltIn?: boolean;
|
||||
canModify?: boolean;
|
||||
}
|
||||
|
||||
export default function SystemConfig() {
|
||||
const [sysParams, setSysParams] = useState<SystemParam[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingParams, setEditingParams] = useState<Record<string, string>>({});
|
||||
const [tempEditingValues, setTempEditingValues] = useState<Record<string, string>>({});
|
||||
|
||||
// 获取系统参数列表
|
||||
const fetchSysParams = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getSysParamList();
|
||||
setSysParams(response.data || []);
|
||||
// 初始化编辑状态
|
||||
const initialEditState: Record<string, string> = {};
|
||||
response.data?.forEach((param: SystemParam) => {
|
||||
initialEditState[param.id] = param.paramValue;
|
||||
});
|
||||
setEditingParams(initialEditState);
|
||||
} catch (error) {
|
||||
console.error('获取系统参数失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 组件加载时获取数据
|
||||
useEffect(() => {
|
||||
fetchSysParams();
|
||||
}, []);
|
||||
|
||||
// 处理参数值更新 - 立即更新(用于开关和下拉框)
|
||||
const handleImmediateUpdate = async (param: SystemParam, newValue: string | boolean) => {
|
||||
try {
|
||||
const stringValue = typeof newValue === 'boolean' ? newValue.toString() : newValue;
|
||||
// 更新本地临时状态
|
||||
setTempEditingValues(prev => ({ ...prev, [param.id]: stringValue }));
|
||||
setEditingParams(prev => ({ ...prev, [param.id]: stringValue }));
|
||||
|
||||
// 调用后端更新接口 - 修改为适应新的接口格式
|
||||
await updateSysParamValue({
|
||||
id: param.id,
|
||||
paramValue: stringValue
|
||||
});
|
||||
|
||||
// 更新本地状态
|
||||
setSysParams(prev => prev.map(p =>
|
||||
p.id === param.id ? { ...p, paramValue: stringValue } : p
|
||||
));
|
||||
} catch (error) {
|
||||
console.error('更新参数失败:', error);
|
||||
// 恢复原始值
|
||||
setEditingParams(prev => ({ ...prev, [param.id]: param.paramValue }));
|
||||
setTempEditingValues(prev => ({ ...prev, [param.id]: param.paramValue }));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理输入框值变化 - 仅更新临时状态
|
||||
const handleInputChange = (param: SystemParam, newValue: string) => {
|
||||
setTempEditingValues(prev => ({ ...prev, [param.id]: newValue }));
|
||||
};
|
||||
|
||||
// 处理输入框失焦 - 发起后端请求
|
||||
const handleInputBlur = async (param: SystemParam) => {
|
||||
const newValue = tempEditingValues[param.id];
|
||||
if (newValue !== undefined && newValue !== param.paramValue) {
|
||||
try {
|
||||
// 调用后端更新接口
|
||||
await updateSysParamValue({
|
||||
id: param.id,
|
||||
paramValue: newValue
|
||||
});
|
||||
|
||||
// 更新本地状态
|
||||
setSysParams(prev => prev.map(p =>
|
||||
p.id === param.id ? { ...p, paramValue: newValue } : p
|
||||
));
|
||||
setEditingParams(prev => ({ ...prev, [param.id]: newValue }));
|
||||
} catch (error) {
|
||||
console.error('更新参数失败:', error);
|
||||
// 恢复原始值
|
||||
setTempEditingValues(prev => ({ ...prev, [param.id]: param.paramValue }));
|
||||
setEditingParams(prev => ({ ...prev, [param.id]: param.paramValue }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 获取选项列表 - 解析逗号分隔的字符串
|
||||
const getOptionList = (optionListStr?: string) => {
|
||||
if (!optionListStr) return [];
|
||||
try {
|
||||
// 按逗号分割字符串并去除首尾空格
|
||||
return optionListStr.split(',').map(option => ({
|
||||
value: option.trim(),
|
||||
label: option.trim()
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('解析选项列表失败:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: "参数名",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "参数值",
|
||||
dataIndex: "paramValue",
|
||||
key: "paramValue",
|
||||
width: 200,
|
||||
render: (value: string, record: SystemParam) => {
|
||||
// 使用临时编辑值或当前值
|
||||
const displayValue = tempEditingValues[record.id] ?? editingParams[record.id] ?? value;
|
||||
|
||||
// 对于boolean类型,使用开关按钮
|
||||
if (record.paramType === 'boolean') {
|
||||
const isChecked = displayValue.toLowerCase() === 'true';
|
||||
return (
|
||||
<Switch
|
||||
checked={isChecked}
|
||||
onChange={(checked) => handleImmediateUpdate(record, checked)}
|
||||
disabled={!record.canModify}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 对于有选项列表的参数,强制使用下拉框
|
||||
if (record.optionList && record.optionList.trim()) {
|
||||
const options = getOptionList(record.optionList);
|
||||
return (
|
||||
<Select
|
||||
value={displayValue}
|
||||
onChange={(newValue) => handleImmediateUpdate(record, newValue)}
|
||||
options={options}
|
||||
disabled={!record.canModify}
|
||||
style={{ width: '150px' }}
|
||||
placeholder="请选择值"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 对于数字类型
|
||||
if (record.paramType === 'integer' || record.paramType === 'number') {
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
value={displayValue}
|
||||
onChange={(e) => handleInputChange(record, e.target.value)}
|
||||
onBlur={() => handleInputBlur(record)}
|
||||
disabled={!record.canModify}
|
||||
style={{ width: '150px' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 默认为文本输入
|
||||
return (
|
||||
<Input
|
||||
value={displayValue}
|
||||
onChange={(e) => handleInputChange(record, e.target.value)}
|
||||
onBlur={() => handleInputBlur(record)}
|
||||
disabled={!record.canModify}
|
||||
style={{ width: '150px' }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
title: "是否启用",
|
||||
dataIndex: "isEnabled",
|
||||
key: "isEnabled",
|
||||
width: 100,
|
||||
render: (isEnabled: boolean) => (
|
||||
<Switch checked={isEnabled} disabled={true} />
|
||||
),
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-top justify-between">
|
||||
<h2 className="text-lg font-medium mb-4">系统参数配置</h2>
|
||||
<Button onClick={fetchSysParams}>刷新</Button>
|
||||
</div>
|
||||
<div className="flex-1 border-card overflow-auto p-6">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={sysParams}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
import { Input, Select, Switch, Button, Table, Spin } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getSysParamList, updateSysParamValue } from './settings.apis';
|
||||
|
||||
interface SystemParam {
|
||||
id: string;
|
||||
paramValue: string;
|
||||
description: string;
|
||||
isEnabled: boolean;
|
||||
paramType?: string;
|
||||
optionList?: string;
|
||||
isBuiltIn?: boolean;
|
||||
canModify?: boolean;
|
||||
}
|
||||
|
||||
export default function SystemConfig() {
|
||||
const [sysParams, setSysParams] = useState<SystemParam[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingParams, setEditingParams] = useState<Record<string, string>>({});
|
||||
const [tempEditingValues, setTempEditingValues] = useState<Record<string, string>>({});
|
||||
|
||||
// 获取系统参数列表
|
||||
const fetchSysParams = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getSysParamList();
|
||||
setSysParams(response.data || []);
|
||||
// 初始化编辑状态
|
||||
const initialEditState: Record<string, string> = {};
|
||||
response.data?.forEach((param: SystemParam) => {
|
||||
initialEditState[param.id] = param.paramValue;
|
||||
});
|
||||
setEditingParams(initialEditState);
|
||||
} catch (error) {
|
||||
console.error('获取系统参数失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 组件加载时获取数据
|
||||
useEffect(() => {
|
||||
fetchSysParams();
|
||||
}, []);
|
||||
|
||||
// 处理参数值更新 - 立即更新(用于开关和下拉框)
|
||||
const handleImmediateUpdate = async (param: SystemParam, newValue: string | boolean) => {
|
||||
try {
|
||||
const stringValue = typeof newValue === 'boolean' ? newValue.toString() : newValue;
|
||||
// 更新本地临时状态
|
||||
setTempEditingValues(prev => ({ ...prev, [param.id]: stringValue }));
|
||||
setEditingParams(prev => ({ ...prev, [param.id]: stringValue }));
|
||||
|
||||
// 调用后端更新接口 - 修改为适应新的接口格式
|
||||
await updateSysParamValue({
|
||||
id: param.id,
|
||||
paramValue: stringValue
|
||||
});
|
||||
|
||||
// 更新本地状态
|
||||
setSysParams(prev => prev.map(p =>
|
||||
p.id === param.id ? { ...p, paramValue: stringValue } : p
|
||||
));
|
||||
} catch (error) {
|
||||
console.error('更新参数失败:', error);
|
||||
// 恢复原始值
|
||||
setEditingParams(prev => ({ ...prev, [param.id]: param.paramValue }));
|
||||
setTempEditingValues(prev => ({ ...prev, [param.id]: param.paramValue }));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理输入框值变化 - 仅更新临时状态
|
||||
const handleInputChange = (param: SystemParam, newValue: string) => {
|
||||
setTempEditingValues(prev => ({ ...prev, [param.id]: newValue }));
|
||||
};
|
||||
|
||||
// 处理输入框失焦 - 发起后端请求
|
||||
const handleInputBlur = async (param: SystemParam) => {
|
||||
const newValue = tempEditingValues[param.id];
|
||||
if (newValue !== undefined && newValue !== param.paramValue) {
|
||||
try {
|
||||
// 调用后端更新接口
|
||||
await updateSysParamValue({
|
||||
id: param.id,
|
||||
paramValue: newValue
|
||||
});
|
||||
|
||||
// 更新本地状态
|
||||
setSysParams(prev => prev.map(p =>
|
||||
p.id === param.id ? { ...p, paramValue: newValue } : p
|
||||
));
|
||||
setEditingParams(prev => ({ ...prev, [param.id]: newValue }));
|
||||
} catch (error) {
|
||||
console.error('更新参数失败:', error);
|
||||
// 恢复原始值
|
||||
setTempEditingValues(prev => ({ ...prev, [param.id]: param.paramValue }));
|
||||
setEditingParams(prev => ({ ...prev, [param.id]: param.paramValue }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 获取选项列表 - 解析逗号分隔的字符串
|
||||
const getOptionList = (optionListStr?: string) => {
|
||||
if (!optionListStr) return [];
|
||||
try {
|
||||
// 按逗号分割字符串并去除首尾空格
|
||||
return optionListStr.split(',').map(option => ({
|
||||
value: option.trim(),
|
||||
label: option.trim()
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('解析选项列表失败:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: "参数名",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "参数值",
|
||||
dataIndex: "paramValue",
|
||||
key: "paramValue",
|
||||
width: 200,
|
||||
render: (value: string, record: SystemParam) => {
|
||||
// 使用临时编辑值或当前值
|
||||
const displayValue = tempEditingValues[record.id] ?? editingParams[record.id] ?? value;
|
||||
|
||||
// 对于boolean类型,使用开关按钮
|
||||
if (record.paramType === 'boolean') {
|
||||
const isChecked = displayValue.toLowerCase() === 'true';
|
||||
return (
|
||||
<Switch
|
||||
checked={isChecked}
|
||||
onChange={(checked) => handleImmediateUpdate(record, checked)}
|
||||
disabled={!record.canModify}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 对于有选项列表的参数,强制使用下拉框
|
||||
if (record.optionList && record.optionList.trim()) {
|
||||
const options = getOptionList(record.optionList);
|
||||
return (
|
||||
<Select
|
||||
value={displayValue}
|
||||
onChange={(newValue) => handleImmediateUpdate(record, newValue)}
|
||||
options={options}
|
||||
disabled={!record.canModify}
|
||||
style={{ width: '150px' }}
|
||||
placeholder="请选择值"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 对于数字类型
|
||||
if (record.paramType === 'integer' || record.paramType === 'number') {
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
value={displayValue}
|
||||
onChange={(e) => handleInputChange(record, e.target.value)}
|
||||
onBlur={() => handleInputBlur(record)}
|
||||
disabled={!record.canModify}
|
||||
style={{ width: '150px' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 默认为文本输入
|
||||
return (
|
||||
<Input
|
||||
value={displayValue}
|
||||
onChange={(e) => handleInputChange(record, e.target.value)}
|
||||
onBlur={() => handleInputBlur(record)}
|
||||
disabled={!record.canModify}
|
||||
style={{ width: '150px' }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
title: "是否启用",
|
||||
dataIndex: "isEnabled",
|
||||
key: "isEnabled",
|
||||
width: 100,
|
||||
render: (isEnabled: boolean) => (
|
||||
<Switch checked={isEnabled} disabled={true} />
|
||||
),
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-top justify-between">
|
||||
<h2 className="text-lg font-medium mb-4">系统参数配置</h2>
|
||||
<Button onClick={fetchSysParams}>刷新</Button>
|
||||
</div>
|
||||
<div className="flex-1 border-card overflow-auto p-6">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={sysParams}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,313 +1,313 @@
|
||||
import { Button, Card, Checkbox, Form, Input, Modal, Badge } from "antd";
|
||||
import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
KeyOutlined,
|
||||
ReloadOutlined,
|
||||
ExperimentOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useState } from "react";
|
||||
|
||||
interface WebhookEvent {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
interface WebhookConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
events: string[];
|
||||
status: "active" | "inactive";
|
||||
secret: string;
|
||||
retryCount: number;
|
||||
}
|
||||
|
||||
const availableEvents: WebhookEvent[] = [
|
||||
{
|
||||
id: "project_created",
|
||||
name: "项目创建",
|
||||
description: "新项目被创建时触发",
|
||||
category: "项目管理",
|
||||
},
|
||||
{
|
||||
id: "project_updated",
|
||||
name: "项目更新",
|
||||
description: "项目信息被修改时触发",
|
||||
category: "项目管理",
|
||||
},
|
||||
{
|
||||
id: "project_deleted",
|
||||
name: "项目删除",
|
||||
description: "项目被删除时触发",
|
||||
category: "项目管理",
|
||||
},
|
||||
{
|
||||
id: "task_created",
|
||||
name: "任务创建",
|
||||
description: "新任务被创建时触发",
|
||||
category: "任务管理",
|
||||
},
|
||||
{
|
||||
id: "task_updated",
|
||||
name: "任务更新",
|
||||
description: "任务状态或内容被更新时触发",
|
||||
category: "任务管理",
|
||||
},
|
||||
{
|
||||
id: "task_completed",
|
||||
name: "任务完成",
|
||||
description: "任务被标记为完成时触发",
|
||||
category: "任务管理",
|
||||
},
|
||||
{
|
||||
id: "annotation_created",
|
||||
name: "标注创建",
|
||||
description: "新标注被创建时触发",
|
||||
category: "标注管理",
|
||||
},
|
||||
{
|
||||
id: "annotation_updated",
|
||||
name: "标注更新",
|
||||
description: "标注被修改时触发",
|
||||
category: "标注管理",
|
||||
},
|
||||
{
|
||||
id: "annotation_deleted",
|
||||
name: "标注删除",
|
||||
description: "标注被删除时触发",
|
||||
category: "标注管理",
|
||||
},
|
||||
{
|
||||
id: "model_trained",
|
||||
name: "模型训练完成",
|
||||
description: "模型训练任务完成时触发",
|
||||
category: "模型管理",
|
||||
},
|
||||
{
|
||||
id: "prediction_created",
|
||||
name: "预测生成",
|
||||
description: "新预测结果生成时触发",
|
||||
category: "预测管理",
|
||||
},
|
||||
];
|
||||
|
||||
export default function WebhookConfig() {
|
||||
const [newWebhook, setNewWebhook] = useState({
|
||||
name: "",
|
||||
url: "",
|
||||
events: [] as string[],
|
||||
secret: "",
|
||||
retryCount: 3,
|
||||
});
|
||||
const [showWebhookDialog, setShowWebhookDialog] = useState(false);
|
||||
// Webhook State
|
||||
const [webhooks, setWebhooks] = useState<WebhookConfig[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "数据同步Webhook",
|
||||
url: "https://webhook.example.com/data-sync",
|
||||
events: ["task_created", "task_completed", "annotation_created"],
|
||||
status: "active",
|
||||
secret: "wh_secret_123456",
|
||||
retryCount: 3,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "任务通知Webhook",
|
||||
url: "https://webhook.example.com/task-notify",
|
||||
events: ["task_started", "task_completed", "task_failed"],
|
||||
status: "inactive",
|
||||
secret: "wh_secret_789012",
|
||||
retryCount: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
const handleAddWebhook = () => {
|
||||
setNewWebhook({
|
||||
name: "",
|
||||
url: "",
|
||||
events: [],
|
||||
secret: generateApiKey(),
|
||||
retryCount: 3,
|
||||
});
|
||||
setShowWebhookDialog(true);
|
||||
};
|
||||
|
||||
const generateApiKey = () => {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "sk-";
|
||||
for (let i = 0; i < 48; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Webhook 配置</h3>
|
||||
</div>
|
||||
<Button onClick={handleAddWebhook}>新增Webhook</Button>
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
{webhooks.map((webhook) => (
|
||||
<Card key={webhook.id}>
|
||||
<div className="flex items-start justify-between p-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-medium">{webhook.name}</span>
|
||||
<Badge
|
||||
status={webhook.status === "active" ? "success" : "default"}
|
||||
text={webhook.status === "active" ? "启用" : "禁用"}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 flex items-center gap-2">
|
||||
<ThunderboltOutlined />
|
||||
{webhook.url}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm text-gray-500">事件:</span>
|
||||
{webhook.events.map((event) => {
|
||||
const eventInfo = availableEvents.find(
|
||||
(e) => e.id === event
|
||||
);
|
||||
return (
|
||||
<Badge
|
||||
key={event}
|
||||
status="default"
|
||||
text={eventInfo?.name || event}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<KeyOutlined />
|
||||
Secret: {webhook.secret.substring(0, 12)}...
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<ReloadOutlined />
|
||||
重试: {webhook.retryCount}次
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button icon={<ExperimentOutlined />} size="small" />
|
||||
<Button icon={<EditOutlined />} size="small" />
|
||||
<Button icon={<DeleteOutlined />} size="small" danger />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Modal
|
||||
open={showWebhookDialog}
|
||||
onCancel={() => setShowWebhookDialog(false)}
|
||||
title="新增 Webhook"
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setShowWebhookDialog(false)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="ok"
|
||||
type="primary"
|
||||
onClick={() => setShowWebhookDialog(false)}
|
||||
>
|
||||
创建Webhook
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
initialValues={newWebhook}
|
||||
onValuesChange={(changedValues) => {
|
||||
setNewWebhook({ ...newWebhook, ...changedValues });
|
||||
}}
|
||||
>
|
||||
<Form.Item name="name" label="Webhook名称">
|
||||
<Input placeholder="输入Webhook名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="retryCount" label="重试次数">
|
||||
<Input type="number" />
|
||||
</Form.Item>
|
||||
<Form.Item name="url" label="Webhook URL">
|
||||
<Input placeholder="https://your-domain.com/webhook" />
|
||||
</Form.Item>
|
||||
<Form.Item name="secret" label="Secret Key">
|
||||
<Input
|
||||
placeholder="用于验证Webhook请求的密钥"
|
||||
addonAfter={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() =>
|
||||
setNewWebhook({ ...newWebhook, secret: generateApiKey() })
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="选择事件">
|
||||
<div className="max-h-48 overflow-y-auto border rounded-lg p-3 space-y-3">
|
||||
{Object.entries(
|
||||
availableEvents.reduce((acc, event) => {
|
||||
if (!acc[event.category]) acc[event.category] = [];
|
||||
acc[event.category].push(event);
|
||||
return acc;
|
||||
}, {} as Record<string, WebhookEvent[]>)
|
||||
).map(([category, events]) => (
|
||||
<div key={category} className="space-y-2">
|
||||
<h4 className="font-medium text-sm text-gray-700">
|
||||
{category}
|
||||
</h4>
|
||||
<div className="space-y-2 pl-4">
|
||||
{events.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="flex items-start space-x-2"
|
||||
>
|
||||
<Checkbox
|
||||
checked={newWebhook.events.includes(event.id)}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
if (checked) {
|
||||
setNewWebhook({
|
||||
...newWebhook,
|
||||
events: [...newWebhook.events, event.id],
|
||||
});
|
||||
} else {
|
||||
setNewWebhook({
|
||||
...newWebhook,
|
||||
events: newWebhook.events.filter(
|
||||
(ev) => ev !== event.id
|
||||
),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="text-sm font-medium">
|
||||
{event.name}
|
||||
</span>
|
||||
</Checkbox>
|
||||
<span className="text-xs text-gray-500">
|
||||
{event.description}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Button, Card, Checkbox, Form, Input, Modal, Badge } from "antd";
|
||||
import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
KeyOutlined,
|
||||
ReloadOutlined,
|
||||
ExperimentOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useState } from "react";
|
||||
|
||||
interface WebhookEvent {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
interface WebhookConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
events: string[];
|
||||
status: "active" | "inactive";
|
||||
secret: string;
|
||||
retryCount: number;
|
||||
}
|
||||
|
||||
const availableEvents: WebhookEvent[] = [
|
||||
{
|
||||
id: "project_created",
|
||||
name: "项目创建",
|
||||
description: "新项目被创建时触发",
|
||||
category: "项目管理",
|
||||
},
|
||||
{
|
||||
id: "project_updated",
|
||||
name: "项目更新",
|
||||
description: "项目信息被修改时触发",
|
||||
category: "项目管理",
|
||||
},
|
||||
{
|
||||
id: "project_deleted",
|
||||
name: "项目删除",
|
||||
description: "项目被删除时触发",
|
||||
category: "项目管理",
|
||||
},
|
||||
{
|
||||
id: "task_created",
|
||||
name: "任务创建",
|
||||
description: "新任务被创建时触发",
|
||||
category: "任务管理",
|
||||
},
|
||||
{
|
||||
id: "task_updated",
|
||||
name: "任务更新",
|
||||
description: "任务状态或内容被更新时触发",
|
||||
category: "任务管理",
|
||||
},
|
||||
{
|
||||
id: "task_completed",
|
||||
name: "任务完成",
|
||||
description: "任务被标记为完成时触发",
|
||||
category: "任务管理",
|
||||
},
|
||||
{
|
||||
id: "annotation_created",
|
||||
name: "标注创建",
|
||||
description: "新标注被创建时触发",
|
||||
category: "标注管理",
|
||||
},
|
||||
{
|
||||
id: "annotation_updated",
|
||||
name: "标注更新",
|
||||
description: "标注被修改时触发",
|
||||
category: "标注管理",
|
||||
},
|
||||
{
|
||||
id: "annotation_deleted",
|
||||
name: "标注删除",
|
||||
description: "标注被删除时触发",
|
||||
category: "标注管理",
|
||||
},
|
||||
{
|
||||
id: "model_trained",
|
||||
name: "模型训练完成",
|
||||
description: "模型训练任务完成时触发",
|
||||
category: "模型管理",
|
||||
},
|
||||
{
|
||||
id: "prediction_created",
|
||||
name: "预测生成",
|
||||
description: "新预测结果生成时触发",
|
||||
category: "预测管理",
|
||||
},
|
||||
];
|
||||
|
||||
export default function WebhookConfig() {
|
||||
const [newWebhook, setNewWebhook] = useState({
|
||||
name: "",
|
||||
url: "",
|
||||
events: [] as string[],
|
||||
secret: "",
|
||||
retryCount: 3,
|
||||
});
|
||||
const [showWebhookDialog, setShowWebhookDialog] = useState(false);
|
||||
// Webhook State
|
||||
const [webhooks, setWebhooks] = useState<WebhookConfig[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "数据同步Webhook",
|
||||
url: "https://webhook.example.com/data-sync",
|
||||
events: ["task_created", "task_completed", "annotation_created"],
|
||||
status: "active",
|
||||
secret: "wh_secret_123456",
|
||||
retryCount: 3,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "任务通知Webhook",
|
||||
url: "https://webhook.example.com/task-notify",
|
||||
events: ["task_started", "task_completed", "task_failed"],
|
||||
status: "inactive",
|
||||
secret: "wh_secret_789012",
|
||||
retryCount: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
const handleAddWebhook = () => {
|
||||
setNewWebhook({
|
||||
name: "",
|
||||
url: "",
|
||||
events: [],
|
||||
secret: generateApiKey(),
|
||||
retryCount: 3,
|
||||
});
|
||||
setShowWebhookDialog(true);
|
||||
};
|
||||
|
||||
const generateApiKey = () => {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "sk-";
|
||||
for (let i = 0; i < 48; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Webhook 配置</h3>
|
||||
</div>
|
||||
<Button onClick={handleAddWebhook}>新增Webhook</Button>
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
{webhooks.map((webhook) => (
|
||||
<Card key={webhook.id}>
|
||||
<div className="flex items-start justify-between p-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-medium">{webhook.name}</span>
|
||||
<Badge
|
||||
status={webhook.status === "active" ? "success" : "default"}
|
||||
text={webhook.status === "active" ? "启用" : "禁用"}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 flex items-center gap-2">
|
||||
<ThunderboltOutlined />
|
||||
{webhook.url}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm text-gray-500">事件:</span>
|
||||
{webhook.events.map((event) => {
|
||||
const eventInfo = availableEvents.find(
|
||||
(e) => e.id === event
|
||||
);
|
||||
return (
|
||||
<Badge
|
||||
key={event}
|
||||
status="default"
|
||||
text={eventInfo?.name || event}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<KeyOutlined />
|
||||
Secret: {webhook.secret.substring(0, 12)}...
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<ReloadOutlined />
|
||||
重试: {webhook.retryCount}次
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button icon={<ExperimentOutlined />} size="small" />
|
||||
<Button icon={<EditOutlined />} size="small" />
|
||||
<Button icon={<DeleteOutlined />} size="small" danger />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Modal
|
||||
open={showWebhookDialog}
|
||||
onCancel={() => setShowWebhookDialog(false)}
|
||||
title="新增 Webhook"
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setShowWebhookDialog(false)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="ok"
|
||||
type="primary"
|
||||
onClick={() => setShowWebhookDialog(false)}
|
||||
>
|
||||
创建Webhook
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
initialValues={newWebhook}
|
||||
onValuesChange={(changedValues) => {
|
||||
setNewWebhook({ ...newWebhook, ...changedValues });
|
||||
}}
|
||||
>
|
||||
<Form.Item name="name" label="Webhook名称">
|
||||
<Input placeholder="输入Webhook名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="retryCount" label="重试次数">
|
||||
<Input type="number" />
|
||||
</Form.Item>
|
||||
<Form.Item name="url" label="Webhook URL">
|
||||
<Input placeholder="https://your-domain.com/webhook" />
|
||||
</Form.Item>
|
||||
<Form.Item name="secret" label="Secret Key">
|
||||
<Input
|
||||
placeholder="用于验证Webhook请求的密钥"
|
||||
addonAfter={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() =>
|
||||
setNewWebhook({ ...newWebhook, secret: generateApiKey() })
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="选择事件">
|
||||
<div className="max-h-48 overflow-y-auto border rounded-lg p-3 space-y-3">
|
||||
{Object.entries(
|
||||
availableEvents.reduce((acc, event) => {
|
||||
if (!acc[event.category]) acc[event.category] = [];
|
||||
acc[event.category].push(event);
|
||||
return acc;
|
||||
}, {} as Record<string, WebhookEvent[]>)
|
||||
).map(([category, events]) => (
|
||||
<div key={category} className="space-y-2">
|
||||
<h4 className="font-medium text-sm text-gray-700">
|
||||
{category}
|
||||
</h4>
|
||||
<div className="space-y-2 pl-4">
|
||||
{events.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="flex items-start space-x-2"
|
||||
>
|
||||
<Checkbox
|
||||
checked={newWebhook.events.includes(event.id)}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
if (checked) {
|
||||
setNewWebhook({
|
||||
...newWebhook,
|
||||
events: [...newWebhook.events, event.id],
|
||||
});
|
||||
} else {
|
||||
setNewWebhook({
|
||||
...newWebhook,
|
||||
events: newWebhook.events.filter(
|
||||
(ev) => ev !== event.id
|
||||
),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="text-sm font-medium">
|
||||
{event.name}
|
||||
</span>
|
||||
</Checkbox>
|
||||
<span className="text-xs text-gray-500">
|
||||
{event.description}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
// 模型相关接口
|
||||
export function queryModelProvidersUsingGet(params?: any) {
|
||||
return get("/api/models/providers", params);
|
||||
}
|
||||
|
||||
export function queryModelListUsingGet(data: any) {
|
||||
return get("/api/models/list", data);
|
||||
}
|
||||
|
||||
export function queryModelDetailByIdUsingGet(id: string | number) {
|
||||
return get(`/api/models/${id}`);
|
||||
}
|
||||
|
||||
export function updateModelByIdUsingPut(
|
||||
id: string | number,
|
||||
data: any
|
||||
) {
|
||||
return put(`/api/models/${id}`, data);
|
||||
}
|
||||
|
||||
export function createModelUsingPost(data: any) {
|
||||
return post("/api/models/create", data);
|
||||
}
|
||||
|
||||
export function deleteModelByIdUsingDelete(id: string | number) {
|
||||
return del(`/api/models/${id}`);
|
||||
}
|
||||
|
||||
|
||||
// 获取系统参数列表
|
||||
export function getSysParamList() {
|
||||
return get('/api/sys-param/list');
|
||||
}
|
||||
|
||||
// 更新系统参数值
|
||||
export const updateSysParamValue = async (params: { id: string; paramValue: string }) => {
|
||||
return put(`/api/sys-param/${params.id}`, params);
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
// 模型相关接口
|
||||
export function queryModelProvidersUsingGet(params?: any) {
|
||||
return get("/api/models/providers", params);
|
||||
}
|
||||
|
||||
export function queryModelListUsingGet(data: any) {
|
||||
return get("/api/models/list", data);
|
||||
}
|
||||
|
||||
export function queryModelDetailByIdUsingGet(id: string | number) {
|
||||
return get(`/api/models/${id}`);
|
||||
}
|
||||
|
||||
export function updateModelByIdUsingPut(
|
||||
id: string | number,
|
||||
data: any
|
||||
) {
|
||||
return put(`/api/models/${id}`, data);
|
||||
}
|
||||
|
||||
export function createModelUsingPost(data: any) {
|
||||
return post("/api/models/create", data);
|
||||
}
|
||||
|
||||
export function deleteModelByIdUsingDelete(id: string | number) {
|
||||
return del(`/api/models/${id}`);
|
||||
}
|
||||
|
||||
|
||||
// 获取系统参数列表
|
||||
export function getSysParamList() {
|
||||
return get('/api/sys-param/list');
|
||||
}
|
||||
|
||||
// 更新系统参数值
|
||||
export const updateSysParamValue = async (params: { id: string; paramValue: string }) => {
|
||||
return put(`/api/sys-param/${params.id}`, params);
|
||||
};
|
||||
Reference in New Issue
Block a user