nex_docus/frontend/src/pages/System/ModelConfigs.jsx

956 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { useEffect, useRef, useState } from 'react'
import {
Button,
Card,
Form,
Grid,
Input,
InputNumber,
Modal,
Popconfirm,
Radio,
Select,
Slider,
Space,
Switch,
Tabs,
Tag,
} from 'antd'
import {
CloudServerOutlined,
CommentOutlined,
DeploymentUnitOutlined,
ExperimentOutlined,
PlusOutlined,
ReloadOutlined,
DeleteOutlined,
EditOutlined,
StarOutlined,
} from '@ant-design/icons'
import {
createLLMModelConfig,
deleteLLMModelConfig,
getLLMModelConfigDetail,
getLLMModelConfigs,
getLLMProviderCatalog,
setDefaultLLMModelConfig,
testLLMModelConfig,
updateLLMModelConfig,
updateLLMModelConfigStatus,
} from '@/api/llmModelConfigs'
import ListTable from '@/components/ListTable/ListTable'
import PageHeader from '@/components/PageHeader/PageHeader'
import Toast from '@/components/Toast/Toast'
import './ModelConfigs.css'
import '@/pages/System/AdminPages.css'
const { Search, TextArea } = Input
const MAX_TOKENS_OPTIONS = [
{ label: '4K', value: 4096 },
{ label: '8K', value: 8192 },
{ label: '16K', value: 16384 },
{ label: '32K', value: 32768 },
]
const DEFAULT_MAX_TOKENS = 8192
const MODEL_TYPE_META = {
chat: {
label: '对话模型',
icon: <CommentOutlined />,
description: '用于知识库对话、问答生成的大语言模型。',
addText: '新增对话模型',
},
embedding: {
label: '向量模型',
icon: <DeploymentUnitOutlined />,
description: '用于文档向量化ZVec与语义检索的 Embedding 模型。',
addText: '新增向量模型',
},
}
function buildModelCode(provider, llmModelName, modelType) {
const providerPart = (provider || 'custom').trim().toLowerCase()
const modelPart = (llmModelName || '').trim().toLowerCase()
const sanitized = []
let previousSeparator = false
for (const char of modelPart) {
if (/[a-z0-9]/.test(char)) {
sanitized.push(char)
previousSeparator = false
} else if (!previousSeparator) {
sanitized.push('_')
previousSeparator = true
}
}
const suffix = sanitized.join('').replace(/^_+|_+$/g, '') || 'model'
const base = `llm_${providerPart}_${suffix}`
return modelType === 'embedding' ? `emb_${base}` : base
}
function buildModelName(providerMeta, provider, llmModelName) {
const label = providerMeta?.label || provider || '自定义模型'
const modelPart = (llmModelName || '').trim()
if (!modelPart) {
return label
}
return `${label} ${modelPart}`
}
function formatDateTime(value) {
if (!value) return '-'
return new Date(value).toLocaleString('zh-CN')
}
function ModelConfigs() {
const [form] = Form.useForm()
const screens = Grid.useBreakpoint()
const [activeType, setActiveType] = useState('chat')
const [editingType, setEditingType] = useState('chat')
const [loading, setLoading] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [testing, setTesting] = useState(false)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [total, setTotal] = useState(0)
const [keyword, setKeyword] = useState('')
const [providerFilter, setProviderFilter] = useState(undefined)
const [statusFilter, setStatusFilter] = useState(undefined)
const [configs, setConfigs] = useState([])
const [providerCatalog, setProviderCatalog] = useState([])
const [modalVisible, setModalVisible] = useState(false)
const [editingConfigId, setEditingConfigId] = useState(null)
const [autoFillFlags, setAutoFillFlags] = useState({
endpointUrl: true,
modelName: true,
modelCode: true,
})
const loadRequestIdRef = useRef(0)
const providerValue = Form.useWatch('provider', form)
const llmModelNameValue = Form.useWatch('llm_model_name', form)
const isEmbedding = editingType === 'embedding'
const isLocalProvider = providerValue === 'local'
const localModels = providerCatalog.find((item) => item.value === 'local')?.models || []
const availableProviders = providerCatalog.filter((item) => (
!item.model_types || item.model_types.includes(editingType)
))
useEffect(() => {
loadProviderCatalog()
}, [])
useEffect(() => {
loadConfigs()
}, [activeType, page, pageSize, keyword, providerFilter, statusFilter])
useEffect(() => {
if (!modalVisible || !providerValue) {
return
}
const providerMeta = providerCatalog.find((item) => item.value === providerValue)
if (!providerMeta) {
return
}
const currentValues = form.getFieldsValue()
const nextValues = {}
if (autoFillFlags.endpointUrl) {
const nextEndpointUrl = providerMeta.default_endpoint_url || ''
if (nextEndpointUrl !== currentValues.endpoint_url) {
nextValues.endpoint_url = nextEndpointUrl
}
}
if (llmModelNameValue) {
if (autoFillFlags.modelName) {
const nextModelName = buildModelName(providerMeta, providerValue, llmModelNameValue)
if (nextModelName !== currentValues.model_name) {
nextValues.model_name = nextModelName
}
}
if (autoFillFlags.modelCode) {
const nextModelCode = buildModelCode(providerValue, llmModelNameValue, editingType)
if (nextModelCode !== currentValues.model_code) {
nextValues.model_code = nextModelCode
}
}
}
if (Object.keys(nextValues).length > 0) {
form.setFieldsValue(nextValues)
}
}, [modalVisible, providerValue, llmModelNameValue, providerCatalog, autoFillFlags, editingType, form])
const loadProviderCatalog = async () => {
try {
const res = await getLLMProviderCatalog()
setProviderCatalog(res.data || [])
} catch (error) {
console.error('Load LLM provider catalog error:', error)
Toast.error('加载提供方列表失败')
}
}
const loadConfigs = async () => {
const requestId = ++loadRequestIdRef.current
try {
setLoading(true)
const params = {
page,
page_size: pageSize,
model_type: activeType,
}
if (keyword) params.keyword = keyword
if (providerFilter) params.provider = providerFilter
if (statusFilter !== undefined) params.is_active = statusFilter
const res = await getLLMModelConfigs(params)
if (requestId !== loadRequestIdRef.current) return
setConfigs(res.data || [])
setTotal(res.total || 0)
} catch (error) {
console.error('Load llm model configs error:', error)
Toast.error('加载模型配置失败')
} finally {
if (requestId === loadRequestIdRef.current) {
setLoading(false)
}
}
}
const getProviderMeta = (provider) => providerCatalog.find((item) => item.value === provider)
const handleTabChange = (key) => {
setActiveType(key)
setPage(1)
setKeyword('')
setProviderFilter(undefined)
setStatusFilter(undefined)
}
const openCreateModal = () => {
const compatibleProviders = providerCatalog.filter((item) => (
!item.model_types || item.model_types.includes(activeType)
))
const defaultProvider = activeType === 'embedding'
? (compatibleProviders.find((item) => item.value === 'local')?.value || compatibleProviders[0]?.value)
: compatibleProviders[0]?.value
const resolvedProvider = defaultProvider || 'openai'
const defaultEndpointUrl = getProviderMeta(resolvedProvider)?.default_endpoint_url || ''
const providerDefaults = getProviderMeta(resolvedProvider)
setEditingConfigId(null)
setEditingType(activeType)
setAutoFillFlags({
endpointUrl: true,
modelName: true,
modelCode: true,
})
form.setFieldsValue({
model_type: activeType,
provider: resolvedProvider,
endpoint_url: defaultEndpointUrl,
llm_timeout: activeType === 'embedding' ? 60 : 120,
llm_temperature: 0.7,
llm_top_p: 0.9,
llm_max_tokens: DEFAULT_MAX_TOKENS,
embedding_dimension: undefined,
chunk_size: providerDefaults?.default_chunk_size || 800,
chunk_overlap: providerDefaults?.default_chunk_overlap ?? 150,
is_active: true,
is_default: false,
description: '',
llm_system_prompt: '',
model_name: '',
model_code: '',
llm_model_name: '',
api_key: '',
})
setModalVisible(true)
}
const openEditModal = async (record) => {
try {
const res = await getLLMModelConfigDetail(record.config_id)
const detail = res.data
const detailType = detail.model_type || 'chat'
const providerMeta = getProviderMeta(detail.provider)
setEditingConfigId(record.config_id)
setEditingType(detailType)
setAutoFillFlags({
endpointUrl: !detail.endpoint_url || detail.endpoint_url === (providerMeta?.default_endpoint_url || ''),
modelName: !detail.model_name || detail.model_name === buildModelName(providerMeta, detail.provider, detail.llm_model_name),
modelCode: !detail.model_code || detail.model_code === buildModelCode(detail.provider, detail.llm_model_name, detailType),
})
form.setFieldsValue({
...detail,
api_key: detail.api_key || '',
})
setModalVisible(true)
} catch (error) {
console.error('Load llm model config detail error:', error)
Toast.error('加载模型配置详情失败')
}
}
const closeModal = () => {
setModalVisible(false)
setEditingConfigId(null)
form.resetFields()
}
const handleSubmit = async (values) => {
try {
setSubmitting(true)
const payload = { ...values, model_type: editingType }
if (editingConfigId) {
await updateLLMModelConfig(editingConfigId, payload)
Toast.success('模型配置更新成功')
} else {
await createLLMModelConfig(payload)
Toast.success('模型配置创建成功')
}
closeModal()
loadConfigs()
} catch (error) {
Toast.error(error.response?.data?.detail || '保存模型配置失败')
} finally {
setSubmitting(false)
}
}
const handleDelete = async (record) => {
try {
await deleteLLMModelConfig(record.config_id)
Toast.success('模型配置删除成功')
loadConfigs()
} catch (error) {
Toast.error(error.response?.data?.detail || '删除模型配置失败')
}
}
const handleStatusChange = async (record, checked) => {
try {
await updateLLMModelConfigStatus(record.config_id, checked)
Toast.success(checked ? '模型已启用' : '模型已停用')
loadConfigs()
} catch (error) {
Toast.error(error.response?.data?.detail || '更新状态失败')
}
}
const handleSetDefault = (record) => {
const isVectorModel = record.model_type === 'embedding'
Modal.confirm({
title: isVectorModel ? '切换默认向量模型?' : '切换默认对话模型?',
content: isVectorModel
? '新文件和后续检索将使用该模型。已有项目向量由旧模型生成,请在切换后对相关项目执行一次全量向量化。'
: '新建知识库问答将默认选中该模型,已有会话不会改变。',
okText: '设为默认',
cancelText: '取消',
onOk: async () => {
try {
await setDefaultLLMModelConfig(record.config_id)
setConfigs((current) => current
.map((item) => ({
...item,
is_default: item.config_id === record.config_id,
}))
.sort((left, right) => Number(right.is_default) - Number(left.is_default)))
Toast.success(isVectorModel ? '默认向量模型已切换,请重建已有项目向量' : '默认对话模型已切换')
await loadConfigs()
} catch (error) {
Toast.error(error.response?.data?.detail || '切换默认模型失败')
}
},
})
}
const showTestResult = (result) => {
const providerLabel = getProviderMeta(result.provider)?.label || result.provider
const lines = [
`提供方:${providerLabel}`,
`模型:${result.llm_model_name}`,
`延迟:${result.latency_ms} ms`,
]
if (result.dimension) {
lines.push(`向量维度:${result.dimension}`)
}
if (result.preview) {
lines.push(`返回预览:${result.preview}`)
}
const description = (
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{lines.map((line, index) => (
<div key={index}>{line}</div>
))}
</div>
)
Toast.success('模型测试成功', description, 5)
}
const handleFormTest = async () => {
try {
const values = await form.validateFields()
setTesting(true)
const res = await testLLMModelConfig({ ...values, model_type: editingType })
showTestResult(res.data)
} catch (error) {
// 表单校验未通过:不弹提示,仅高亮表单项
// 接口失败:已由全局请求拦截器统一弹出错误 Toast这里不重复提示
} finally {
setTesting(false)
}
}
const getColumns = (type) => {
const baseColumns = [
{
title: '模型名称',
dataIndex: 'model_name',
key: 'model_name',
width: 240,
render: (_, record) => (
<Space direction="vertical" size={2}>
<Space size={6} wrap>
<span>{record.model_name}</span>
{record.is_default && <Tag color="green">默认</Tag>}
</Space>
<span className="model-config-code">{record.model_code}</span>
</Space>
),
},
{
title: '提供方',
dataIndex: 'provider',
key: 'provider',
width: 140,
render: (provider) => (
<Tag color="blue">{getProviderMeta(provider)?.label || provider || '-'}</Tag>
),
},
{
title: '模型标识',
dataIndex: 'llm_model_name',
key: 'llm_model_name',
width: 180,
},
]
if (type === 'embedding') {
baseColumns.push({
title: '向量维度',
dataIndex: 'embedding_dimension',
key: 'embedding_dimension',
width: 110,
render: (value) => (value ? <Tag color="purple">{value}</Tag> : <Tag></Tag>),
})
baseColumns.push({
title: '分块 / 重叠',
key: 'chunk_options',
width: 130,
render: (_, record) => `${record.chunk_size} / ${record.chunk_overlap}`,
})
}
baseColumns.push(
{
title: 'Base URL',
dataIndex: 'endpoint_url',
key: 'endpoint_url',
ellipsis: true,
render: (value) => value || '-',
},
{
title: '状态',
dataIndex: 'is_active',
key: 'is_active',
width: 100,
render: (value, record) => (
<Switch
checked={value}
checkedChildren="启用"
unCheckedChildren="停用"
onChange={(checked) => handleStatusChange(record, checked)}
/>
),
},
{
title: '更新时间',
dataIndex: 'updated_at',
key: 'updated_at',
width: 180,
render: (value) => formatDateTime(value),
},
{
title: '操作',
key: 'action',
fixed: screens.md ? 'right' : undefined,
width: screens.md ? 260 : 220,
render: (_, record) => (
<Space size="small" className="model-config-actions">
{!record.is_default && (
<Button
type="link"
size="small"
icon={<StarOutlined />}
onClick={() => handleSetDefault(record)}
>
设为默认
</Button>
)}
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEditModal(record)}
>
编辑
</Button>
<Popconfirm
title="确认删除该模型配置?"
description="删除后将无法恢复。"
okText="删除"
cancelText="取消"
onConfirm={() => handleDelete(record)}
>
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
>
删除
</Button>
</Popconfirm>
</Space>
),
},
)
return baseColumns
}
const renderListPanel = (type) => (
<>
<div className="admin-toolbar">
<div className="admin-toolbar-left">
<Search
allowClear
placeholder="搜索模型名称、编码或模型标识"
value={keyword}
style={{ width: 320, maxWidth: '100%' }}
onChange={(event) => {
setPage(1)
setKeyword(event.target.value)
}}
onSearch={(value) => {
setPage(1)
setKeyword(value)
}}
/>
<Select
allowClear
placeholder="筛选提供方"
style={{ width: 180 }}
value={providerFilter}
options={providerCatalog.map((item) => ({
label: item.label,
value: item.value,
}))}
onChange={(value) => {
setPage(1)
setProviderFilter(value)
}}
/>
<Select
allowClear
placeholder="筛选状态"
style={{ width: 140 }}
value={statusFilter}
options={[
{ label: '启用', value: true },
{ label: '停用', value: false },
]}
onChange={(value) => {
setPage(1)
setStatusFilter(value)
}}
/>
</div>
<div className="admin-toolbar-right">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={openCreateModal}
>
{MODEL_TYPE_META[type].addText}
</Button>
<Button icon={<ReloadOutlined />} onClick={loadConfigs}>
刷新
</Button>
</div>
</div>
<ListTable
rowKey="config_id"
className="model-config-table"
loading={loading}
columns={getColumns(type)}
dataSource={configs}
scroll={{ x: 1280 }}
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
showQuickJumper: true,
onChange: (nextPage, nextPageSize) => {
setPage(nextPage)
setPageSize(nextPageSize)
},
showTotal: (value) => `${value}`,
}}
/>
</>
)
const tabItems = Object.entries(MODEL_TYPE_META).map(([type, meta]) => ({
key: type,
label: (
<span>
{meta.icon}
{meta.label}
</span>
),
children: (
<div className="model-config-tab-panel">
<p className="model-config-tab-desc">{meta.description}</p>
{activeType === type ? renderListPanel(type) : null}
</div>
),
}))
return (
<div className="admin-page">
<PageHeader
title="模型配置"
description="分类管理对话模型与向量模型的提供方、接口地址、参数模板与连通性测试。"
icon={<CloudServerOutlined />}
/>
<Card className="admin-card model-config-card">
<Tabs
items={tabItems}
activeKey={activeType}
onChange={handleTabChange}
tabPosition={screens.md ? 'left' : 'top'}
className="model-config-tabs"
/>
</Card>
<Modal
title={`${editingConfigId ? '编辑' : '新增'}${MODEL_TYPE_META[editingType].label}`}
open={modalVisible}
width={860}
className="model-config-modal"
style={{ maxWidth: 'calc(100vw - 32px)' }}
destroyOnClose
onCancel={closeModal}
onOk={() => form.submit()}
confirmLoading={submitting}
okText={editingConfigId ? '保存修改' : '创建'}
cancelText="取消"
styles={{
body: {
maxHeight: 'calc(80vh - 180px)',
overflowY: 'auto',
paddingRight: 8,
},
}}
footer={(_, { OkBtn, CancelBtn }) => (
<>
<CancelBtn />
<Button
icon={<ExperimentOutlined />}
loading={testing}
onClick={handleFormTest}
>
测试模型
</Button>
<OkBtn />
</>
)}
>
<div className="model-config-modal-hint">
<strong>自动生成规则</strong> `base_url`
如果你手动改过这些字段后续就不会再被自动覆盖
{isEmbedding && (
<>
<br />
<strong>索引参数</strong>
</>
)}
</div>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{
llm_timeout: 120,
llm_temperature: 0.7,
llm_top_p: 0.9,
llm_max_tokens: DEFAULT_MAX_TOKENS,
is_active: true,
}}
>
<Form.Item name="model_type" hidden>
<Input />
</Form.Item>
<Form.Item name="is_default" hidden>
<Input />
</Form.Item>
<Space direction="vertical" style={{ width: '100%' }} size={4}>
<Form.Item
label="提供方"
name="provider"
rules={[{ required: true, message: '请选择模型提供方' }]}
>
<Select
showSearch
placeholder="请选择模型提供方"
optionFilterProp="label"
options={availableProviders.map((item) => ({
label: item.label,
value: item.value,
}))}
/>
</Form.Item>
{!isLocalProvider && (
<Form.Item
label="Base URL"
name="endpoint_url"
rules={[{ required: true, message: '请输入 base_url' }]}
>
<Input
placeholder="选择提供方后自动带出,也可以手动覆盖"
onChange={() => {
setAutoFillFlags((current) => ({ ...current, endpointUrl: false }))
}}
/>
</Form.Item>
)}
</Space>
<Space style={{ width: '100%' }} size={16} align="start" wrap>
<Form.Item
label="模型标识"
name="llm_model_name"
style={{ flex: 1 }}
rules={[{ required: true, message: '请输入模型标识或部署名' }]}
>
{isLocalProvider ? (
<Select
showSearch
optionFilterProp="label"
placeholder="选择 backend/models 中的模型"
options={localModels.map((item) => ({
value: item.name,
label: `${item.name}${item.dimension ? ` (${item.dimension} 维)` : ''}${item.ready ? '' : ' - 权重未就绪'}`,
disabled: !item.ready,
}))}
onChange={(value) => {
const selected = localModels.find((item) => item.name === value)
if (selected?.dimension) {
form.setFieldValue('embedding_dimension', selected.dimension)
}
}}
/>
) : (
<Input
placeholder={
isEmbedding
? '如 text-embedding-3-small / text-embedding-v4'
: '如 gpt-4.1-mini / qwen3.6-plus / claude-3-5-sonnet-latest'
}
/>
)}
</Form.Item>
<Form.Item
label="请求超时(秒)"
name="llm_timeout"
style={{ width: 180 }}
rules={[{ required: true, message: '请输入超时时间' }]}
>
<InputNumber min={5} max={600} style={{ width: '100%' }} />
</Form.Item>
</Space>
<Space style={{ width: '100%' }} size={16} align="start" wrap>
<Form.Item
label="模型名称"
name="model_name"
style={{ flex: 1 }}
rules={[{ required: true, message: '请输入模型名称' }]}
>
<Input
placeholder="会根据提供方和模型标识自动生成"
onChange={() => {
setAutoFillFlags((current) => ({ ...current, modelName: false }))
}}
/>
</Form.Item>
<Form.Item
label="模型编码"
name="model_code"
style={{ flex: 1 }}
rules={[{ required: true, message: '请输入模型编码' }]}
>
<Input
placeholder="会自动生成,支持手动调整"
onChange={() => {
setAutoFillFlags((current) => ({ ...current, modelCode: false }))
}}
/>
</Form.Item>
</Space>
{!isLocalProvider && (
<Form.Item label="API Key" name="api_key">
<Input.Password
placeholder="支持留空后稍后补齐;测试非 Ollama 模型时建议填写"
autoComplete="new-password"
/>
</Form.Item>
)}
{isEmbedding ? (
<Space className="embedding-options-row" style={{ width: '100%' }} size={16} align="start" wrap>
<Form.Item
label="向量维度"
name="embedding_dimension"
style={{ flex: 1 }}
extra="填写后会校验模型实际输出维度。"
>
<InputNumber
min={1}
max={8192}
step={1}
style={{ width: '100%' }}
placeholder="如 384 / 768 / 1536"
/>
</Form.Item>
<Form.Item
label="分块字符数"
name="chunk_size"
style={{ flex: 1 }}
rules={[{ required: true, message: '请输入分块字符数' }]}
>
<InputNumber min={100} max={10000} step={50} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
label="重叠字符数"
name="chunk_overlap"
style={{ flex: 1 }}
dependencies={['chunk_size']}
rules={[
{ required: true, message: '请输入重叠字符数' },
({ getFieldValue }) => ({
validator(_, value) {
if (value < getFieldValue('chunk_size')) return Promise.resolve()
return Promise.reject(new Error('必须小于分块字符数'))
},
}),
]}
>
<InputNumber min={0} max={5000} step={25} style={{ width: '100%' }} />
</Form.Item>
</Space>
) : (
<>
<Space style={{ width: '100%' }} size={24} align="start" wrap>
<Form.Item
label="Temperature"
name="llm_temperature"
style={{ flex: 1 }}
tooltip="数值越高回答越发散,越低越稳定保守"
rules={[{ required: true, message: '请设置 temperature' }]}
>
<Slider
min={0}
max={2}
step={0.05}
marks={{ 0: '0', 0.7: '0.7', 1: '1', 2: '2' }}
tooltip={{ open: undefined }}
/>
</Form.Item>
<Form.Item
label="Top P"
name="llm_top_p"
style={{ flex: 1 }}
tooltip="核采样阈值,控制候选词的累计概率范围"
rules={[{ required: true, message: '请设置 top_p' }]}
>
<Slider
min={0}
max={1}
step={0.05}
marks={{ 0: '0', 0.5: '0.5', 0.9: '0.9', 1: '1' }}
tooltip={{ open: undefined }}
/>
</Form.Item>
</Space>
<Form.Item
label="Max Tokens"
name="llm_max_tokens"
tooltip="单次回复的最大输出长度"
rules={[{ required: true, message: '请选择 max_tokens' }]}
>
<Radio.Group
className="max-tokens-group"
optionType="button"
buttonStyle="solid"
options={MAX_TOKENS_OPTIONS}
/>
</Form.Item>
<Form.Item label="系统提示词" name="llm_system_prompt">
<TextArea
rows={4}
placeholder="可选。对话与测试时会作为 system prompt 一并发送。"
/>
</Form.Item>
</>
)}
<Form.Item label="描述" name="description">
<TextArea rows={2} placeholder="可填写用途、场景、适用业务等说明" />
</Form.Item>
<Form.Item label="启用状态" name="is_active" valuePropName="checked">
<Switch checkedChildren="启用" unCheckedChildren="停用" />
</Form.Item>
</Form>
</Modal>
</div>
)
}
export default ModelConfigs