✨ feat(pricing+endpoints+ui): wire custom endpoint mapping end‑to‑end and overhaul visual JSON editor
Backend (Go)
- Include custom endpoints in each model’s SupportedEndpointTypes by parsing Model.Endpoints (JSON) and appending keys alongside native endpoint types.
- Build a global supportedEndpointMap map[string]EndpointInfo{path, method} by:
- Seeding with native defaults.
- Overriding/adding from models.endpoints (accepts string path → default POST, or {path, method}).
- Expose supported_endpoint at the top level of /api/pricing (vendors-like), removing per-model duplication.
- Fix default path for EndpointTypeOpenAIResponse to /v1/responses.
- Keep concurrency/caching for pricing retrieval intact.
Frontend (React)
- Fetch supported_endpoint in useModelPricingData and propagate to PricingPage → ModelDetailSideSheet → ModelEndpoints.
- ModelEndpoints
- Resolve path+method via endpointMap; replace {model} with actual model name.
- Fix mobile visibility; always show path and HTTP method.
- JSONEditor
- Wrap with Form.Slot to inherit form layout; simplify visual styles.
- Use Tabs for “Visual” / “Manual” modes.
- Unify editors: key-value editor now supports nested JSON:
- “+” to convert a primitive into an object and add nested fields.
- Add “Convert to value” for two‑way toggle back from object.
- Stable key rename without reordering rows; new rows append at bottom.
- Use Row/Col grid for clean alignment; region editor uses Form.Slot + grid.
- Editing flows
- EditModelModal / EditPrefillGroupModal use JSONEditor (editorType='object') for endpoint mappings.
- PrefillGroupManagement renders endpoint group items by JSON keys.
Data expectations / compatibility
- models.endpoints should be a JSON object mapping endpoint type → string path or {path, method}. Strings default to POST.
- No schema changes; existing TEXT field continues to store JSON.
QA
- /api/pricing now returns custom endpoint types and global supported_endpoint.
- UI shows both native and custom endpoints; paths/methods render on mobile; nested editing works and preserves order.
This commit is contained in:
@@ -1,25 +1,25 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Space,
|
||||
Button,
|
||||
Form,
|
||||
Card,
|
||||
Typography,
|
||||
Banner,
|
||||
Row,
|
||||
Col,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Card,
|
||||
Input,
|
||||
InputNumber,
|
||||
Switch,
|
||||
Select,
|
||||
Input,
|
||||
TextArea,
|
||||
Row,
|
||||
Col,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconCode,
|
||||
IconEdit,
|
||||
IconPlus,
|
||||
IconDelete,
|
||||
IconSetting,
|
||||
IconRefresh,
|
||||
} from '@douyinfe/semi-icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -34,18 +34,17 @@ const JSONEditor = ({
|
||||
showClear = true,
|
||||
template,
|
||||
templateLabel,
|
||||
editorType = 'keyValue', // keyValue, object, region
|
||||
autosize = true,
|
||||
editorType = 'keyValue',
|
||||
rules = [],
|
||||
formApi = null,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
// 初始化JSON数据
|
||||
const [jsonData, setJsonData] = useState(() => {
|
||||
// 初始化时解析JSON数据
|
||||
if (value && value.trim()) {
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed;
|
||||
@@ -53,13 +52,16 @@ const JSONEditor = ({
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
|
||||
// 根据键数量决定默认编辑模式
|
||||
const [editMode, setEditMode] = useState(() => {
|
||||
// 如果初始JSON数据的键数量大于10个,则默认使用手动模式
|
||||
if (value && value.trim()) {
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
const keyCount = Object.keys(parsed).length;
|
||||
@@ -76,7 +78,12 @@ const JSONEditor = ({
|
||||
// 数据同步 - 当value变化时总是更新jsonData(如果JSON有效)
|
||||
useEffect(() => {
|
||||
try {
|
||||
const parsed = value && value.trim() ? JSON.parse(value) : {};
|
||||
let parsed = {};
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
parsed = JSON.parse(value);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
parsed = value;
|
||||
}
|
||||
setJsonData(parsed);
|
||||
setJsonError('');
|
||||
} catch (error) {
|
||||
@@ -86,18 +93,17 @@ const JSONEditor = ({
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
|
||||
// 处理可视化编辑的数据变化
|
||||
const handleVisualChange = useCallback((newData) => {
|
||||
setJsonData(newData);
|
||||
setJsonError('');
|
||||
const jsonString = Object.keys(newData).length === 0 ? '' : JSON.stringify(newData, null, 2);
|
||||
|
||||
|
||||
// 通过formApi设置值(如果提供的话)
|
||||
if (formApi && field) {
|
||||
formApi.setValue(field, jsonString);
|
||||
}
|
||||
|
||||
|
||||
onChange?.(jsonString);
|
||||
}, [onChange, formApi, field]);
|
||||
|
||||
@@ -127,7 +133,12 @@ const JSONEditor = ({
|
||||
} else {
|
||||
// 从手动模式切换到可视化模式,需要验证JSON
|
||||
try {
|
||||
const parsed = value && value.trim() ? JSON.parse(value) : {};
|
||||
let parsed = {};
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
parsed = JSON.parse(value);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
parsed = value;
|
||||
}
|
||||
setJsonData(parsed);
|
||||
setJsonError('');
|
||||
setEditMode('visual');
|
||||
@@ -143,11 +154,11 @@ const JSONEditor = ({
|
||||
const addKeyValue = useCallback(() => {
|
||||
const newData = { ...jsonData };
|
||||
const keys = Object.keys(newData);
|
||||
let newKey = 'key';
|
||||
let counter = 1;
|
||||
let newKey = `field_${counter}`;
|
||||
while (newData.hasOwnProperty(newKey)) {
|
||||
newKey = `key${counter}`;
|
||||
counter++;
|
||||
counter += 1;
|
||||
newKey = `field_${counter}`;
|
||||
}
|
||||
newData[newKey] = '';
|
||||
handleVisualChange(newData);
|
||||
@@ -162,11 +173,15 @@ const JSONEditor = ({
|
||||
|
||||
// 更新键名
|
||||
const updateKey = useCallback((oldKey, newKey) => {
|
||||
if (oldKey === newKey) return;
|
||||
const newData = { ...jsonData };
|
||||
const value = newData[oldKey];
|
||||
delete newData[oldKey];
|
||||
newData[newKey] = value;
|
||||
if (oldKey === newKey || !newKey) return;
|
||||
const newData = {};
|
||||
Object.entries(jsonData).forEach(([k, v]) => {
|
||||
if (k === oldKey) {
|
||||
newData[newKey] = v;
|
||||
} else {
|
||||
newData[k] = v;
|
||||
}
|
||||
});
|
||||
handleVisualChange(newData);
|
||||
}, [jsonData, handleVisualChange]);
|
||||
|
||||
@@ -181,20 +196,20 @@ const JSONEditor = ({
|
||||
const fillTemplate = useCallback(() => {
|
||||
if (template) {
|
||||
const templateString = JSON.stringify(template, null, 2);
|
||||
|
||||
|
||||
// 通过formApi设置值(如果提供的话)
|
||||
if (formApi && field) {
|
||||
formApi.setValue(field, templateString);
|
||||
}
|
||||
|
||||
|
||||
// 无论哪种模式都要更新值
|
||||
onChange?.(templateString);
|
||||
|
||||
|
||||
// 如果是可视化模式,同时更新jsonData
|
||||
if (editMode === 'visual') {
|
||||
setJsonData(template);
|
||||
}
|
||||
|
||||
|
||||
// 清除错误状态
|
||||
setJsonError('');
|
||||
}
|
||||
@@ -215,69 +230,47 @@ const JSONEditor = ({
|
||||
);
|
||||
}
|
||||
const entries = Object.entries(jsonData);
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{entries.length === 0 && (
|
||||
<div className="text-center py-6 px-4">
|
||||
<div className="text-gray-400 mb-2">
|
||||
<IconCode size={32} />
|
||||
</div>
|
||||
<Text type="tertiary" className="text-gray-500 text-sm">
|
||||
{t('暂无数据,点击下方按钮添加键值对')}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{entries.map(([key, value], index) => (
|
||||
<Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
|
||||
<Row gutter={12} align="middle">
|
||||
<Col span={10}>
|
||||
<div className="space-y-1">
|
||||
<Text type="tertiary" size="small">{t('键名')}</Text>
|
||||
<Input
|
||||
placeholder={t('键名')}
|
||||
value={key}
|
||||
onChange={(newKey) => updateKey(key, newKey)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={11}>
|
||||
<div className="space-y-1">
|
||||
<Text type="tertiary" size="small">{t('值')}</Text>
|
||||
<Input
|
||||
placeholder={t('值')}
|
||||
value={value}
|
||||
onChange={(newValue) => updateValue(key, newValue)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type="danger"
|
||||
theme="borderless"
|
||||
size="small"
|
||||
onClick={() => removeKeyValue(key)}
|
||||
className="hover:bg-red-50"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
<Row key={index} gutter={8} align="middle">
|
||||
<Col span={6}>
|
||||
<Input
|
||||
placeholder={t('键名')}
|
||||
value={key}
|
||||
onChange={(newKey) => updateKey(key, newKey)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
{renderValueInput(key, value)}
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type="danger"
|
||||
theme="borderless"
|
||||
onClick={() => removeKeyValue(key)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
|
||||
<div className="flex justify-center pt-1">
|
||||
|
||||
<div className="mt-2 flex justify-center">
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
onClick={addKeyValue}
|
||||
size="small"
|
||||
theme="solid"
|
||||
type="primary"
|
||||
className="shadow-sm hover:shadow-md transition-shadow px-4"
|
||||
theme="outline"
|
||||
onClick={addKeyValue}
|
||||
>
|
||||
{t('添加键值对')}
|
||||
</Button>
|
||||
@@ -286,100 +279,61 @@ const JSONEditor = ({
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染对象编辑器(用于复杂JSON)
|
||||
const renderObjectEditor = () => {
|
||||
const entries = Object.entries(jsonData);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{entries.length === 0 && (
|
||||
<div className="text-center py-6 px-4">
|
||||
<div className="text-gray-400 mb-2">
|
||||
<IconSetting size={32} />
|
||||
</div>
|
||||
<Text type="tertiary" className="text-gray-500 text-sm">
|
||||
{t('暂无参数,点击下方按钮添加请求参数')}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{entries.map(([key, value], index) => (
|
||||
<Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
|
||||
<Row gutter={12} align="middle">
|
||||
<Col span={8}>
|
||||
<div className="space-y-1">
|
||||
<Text type="tertiary" size="small">{t('参数名')}</Text>
|
||||
<Input
|
||||
placeholder={t('参数名')}
|
||||
value={key}
|
||||
onChange={(newKey) => updateKey(key, newKey)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={13}>
|
||||
<div className="space-y-1">
|
||||
<Text type="tertiary" size="small">{t('参数值')} ({typeof value})</Text>
|
||||
{renderValueInput(key, value)}
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type="danger"
|
||||
theme="borderless"
|
||||
size="small"
|
||||
onClick={() => removeKeyValue(key)}
|
||||
className="hover:bg-red-50"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<div className="flex justify-center pt-1">
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
onClick={addKeyValue}
|
||||
size="small"
|
||||
theme="solid"
|
||||
type="primary"
|
||||
className="shadow-sm hover:shadow-md transition-shadow px-4"
|
||||
>
|
||||
{t('添加参数')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// 添加嵌套对象
|
||||
const flattenObject = useCallback((parentKey) => {
|
||||
const newData = { ...jsonData };
|
||||
let primitive = '';
|
||||
const obj = newData[parentKey];
|
||||
if (obj && typeof obj === 'object') {
|
||||
const firstKey = Object.keys(obj)[0];
|
||||
if (firstKey !== undefined) {
|
||||
const firstVal = obj[firstKey];
|
||||
if (typeof firstVal !== 'object') primitive = firstVal;
|
||||
}
|
||||
}
|
||||
newData[parentKey] = primitive;
|
||||
handleVisualChange(newData);
|
||||
}, [jsonData, handleVisualChange]);
|
||||
|
||||
// 渲染参数值输入控件
|
||||
const addNestedObject = useCallback((parentKey) => {
|
||||
const newData = { ...jsonData };
|
||||
if (typeof newData[parentKey] !== 'object' || newData[parentKey] === null) {
|
||||
newData[parentKey] = {};
|
||||
}
|
||||
const existingKeys = Object.keys(newData[parentKey]);
|
||||
let counter = 1;
|
||||
let newKey = `field_${counter}`;
|
||||
while (newData[parentKey].hasOwnProperty(newKey)) {
|
||||
counter += 1;
|
||||
newKey = `field_${counter}`;
|
||||
}
|
||||
newData[parentKey][newKey] = '';
|
||||
handleVisualChange(newData);
|
||||
}, [jsonData, handleVisualChange]);
|
||||
|
||||
// 渲染参数值输入控件(支持嵌套)
|
||||
const renderValueInput = (key, value) => {
|
||||
const valueType = typeof value;
|
||||
|
||||
|
||||
if (valueType === 'boolean') {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Switch
|
||||
checked={value}
|
||||
onChange={(newValue) => updateValue(key, newValue)}
|
||||
size="small"
|
||||
/>
|
||||
<Text type="tertiary" size="small" className="ml-2">
|
||||
<Text type="tertiary" className="ml-2">
|
||||
{value ? t('true') : t('false')}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (valueType === 'number') {
|
||||
return (
|
||||
<InputNumber
|
||||
value={value}
|
||||
onChange={(newValue) => updateValue(key, newValue)}
|
||||
size="small"
|
||||
style={{ width: '100%' }}
|
||||
step={key === 'temperature' ? 0.1 : 1}
|
||||
precision={key === 'temperature' ? 2 : 0}
|
||||
@@ -387,25 +341,137 @@ const JSONEditor = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 字符串类型或其他类型
|
||||
|
||||
if (valueType === 'object' && value !== null) {
|
||||
// 渲染嵌套对象
|
||||
const entries = Object.entries(value);
|
||||
return (
|
||||
<Card className="!rounded-2xl">
|
||||
{entries.length === 0 && (
|
||||
<Text type="tertiary" className="text-gray-500 text-xs">
|
||||
{t('空对象,点击下方加号添加字段')}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{entries.map(([nestedKey, nestedValue], index) => (
|
||||
<Row key={index} gutter={4} align="middle" className="mb-1">
|
||||
<Col span={8}>
|
||||
<Input
|
||||
size="small"
|
||||
placeholder={t('键名')}
|
||||
value={nestedKey}
|
||||
onChange={(newKey) => {
|
||||
const newData = { ...jsonData };
|
||||
const oldValue = newData[key][nestedKey];
|
||||
delete newData[key][nestedKey];
|
||||
newData[key][newKey] = oldValue;
|
||||
handleVisualChange(newData);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={14}>
|
||||
{typeof nestedValue === 'object' && nestedValue !== null ? (
|
||||
<TextArea
|
||||
size="small"
|
||||
rows={2}
|
||||
value={JSON.stringify(nestedValue, null, 2)}
|
||||
onChange={(txt) => {
|
||||
try {
|
||||
const obj = txt.trim() ? JSON.parse(txt) : {};
|
||||
const newData = { ...jsonData };
|
||||
newData[key][nestedKey] = obj;
|
||||
handleVisualChange(newData);
|
||||
} catch {
|
||||
// ignore parse error
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
size="small"
|
||||
placeholder={t('值')}
|
||||
value={String(nestedValue)}
|
||||
onChange={(newValue) => {
|
||||
const newData = { ...jsonData };
|
||||
let convertedValue = newValue;
|
||||
if (newValue === 'true') convertedValue = true;
|
||||
else if (newValue === 'false') convertedValue = false;
|
||||
else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') {
|
||||
convertedValue = Number(newValue);
|
||||
}
|
||||
newData[key][nestedKey] = convertedValue;
|
||||
handleVisualChange(newData);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<IconDelete />}
|
||||
type="danger"
|
||||
theme="borderless"
|
||||
onClick={() => {
|
||||
const newData = { ...jsonData };
|
||||
delete newData[key][nestedKey];
|
||||
handleVisualChange(newData);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
|
||||
<div className="flex justify-center mt-1 gap-2">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<IconPlus />}
|
||||
type="tertiary"
|
||||
onClick={() => addNestedObject(key)}
|
||||
>
|
||||
{t('添加字段')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<IconRefresh />}
|
||||
type="tertiary"
|
||||
onClick={() => flattenObject(key)}
|
||||
>
|
||||
{t('转换为值')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// 字符串或其他原始类型
|
||||
return (
|
||||
<Input
|
||||
placeholder={t('参数值')}
|
||||
value={String(value)}
|
||||
onChange={(newValue) => {
|
||||
// 尝试转换为适当的类型
|
||||
let convertedValue = newValue;
|
||||
if (newValue === 'true') convertedValue = true;
|
||||
else if (newValue === 'false') convertedValue = false;
|
||||
else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') {
|
||||
convertedValue = Number(newValue);
|
||||
}
|
||||
|
||||
updateValue(key, convertedValue);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
placeholder={t('参数值')}
|
||||
value={String(value)}
|
||||
onChange={(newValue) => {
|
||||
let convertedValue = newValue;
|
||||
if (newValue === 'true') convertedValue = true;
|
||||
else if (newValue === 'false') convertedValue = false;
|
||||
else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') {
|
||||
convertedValue = Number(newValue);
|
||||
}
|
||||
updateValue(key, convertedValue);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
type="tertiary"
|
||||
onClick={() => {
|
||||
// 将当前值转换为对象
|
||||
const newData = { ...jsonData };
|
||||
newData[key] = { '1': value };
|
||||
handleVisualChange(newData);
|
||||
}}
|
||||
title={t('转换为对象')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -414,79 +480,61 @@ const JSONEditor = ({
|
||||
const entries = Object.entries(jsonData);
|
||||
const defaultEntry = entries.find(([key]) => key === 'default');
|
||||
const modelEntries = entries.filter(([key]) => key !== 'default');
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
{/* 默认区域 */}
|
||||
<Card className="!p-2 !border-blue-200 !bg-blue-50">
|
||||
<div className="flex items-center mb-1">
|
||||
<Text strong size="small" className="text-blue-700">{t('默认区域')}</Text>
|
||||
</div>
|
||||
<Form.Slot label={t('默认区域')}>
|
||||
<Input
|
||||
placeholder={t('默认区域,如: us-central1')}
|
||||
value={defaultEntry ? defaultEntry[1] : ''}
|
||||
onChange={(value) => updateValue('default', value)}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
</Form.Slot>
|
||||
|
||||
{/* 模型专用区域 */}
|
||||
<div className="space-y-1">
|
||||
<Text strong size="small">{t('模型专用区域')}</Text>
|
||||
{modelEntries.map(([modelName, region], index) => (
|
||||
<Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
|
||||
<Row gutter={12} align="middle">
|
||||
<Form.Slot label={t('模型专用区域')}>
|
||||
<div>
|
||||
{modelEntries.map(([modelName, region], index) => (
|
||||
<Row key={index} gutter={8} align="middle" className="mb-2">
|
||||
<Col span={10}>
|
||||
<div className="space-y-1">
|
||||
<Text type="tertiary" size="small">{t('模型名称')}</Text>
|
||||
<Input
|
||||
placeholder={t('模型名称')}
|
||||
value={modelName}
|
||||
onChange={(newKey) => updateKey(modelName, newKey)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t('模型名称')}
|
||||
value={modelName}
|
||||
onChange={(newKey) => updateKey(modelName, newKey)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={11}>
|
||||
<div className="space-y-1">
|
||||
<Text type="tertiary" size="small">{t('区域')}</Text>
|
||||
<Input
|
||||
placeholder={t('区域')}
|
||||
value={region}
|
||||
onChange={(newValue) => updateValue(modelName, newValue)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<Col span={12}>
|
||||
<Input
|
||||
placeholder={t('区域')}
|
||||
value={region}
|
||||
onChange={(newValue) => updateValue(modelName, newValue)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type="danger"
|
||||
theme="borderless"
|
||||
size="small"
|
||||
onClick={() => removeKeyValue(modelName)}
|
||||
className="hover:bg-red-50"
|
||||
/>
|
||||
</div>
|
||||
<Col span={2}>
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type="danger"
|
||||
theme="borderless"
|
||||
onClick={() => removeKeyValue(modelName)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<div className="flex justify-center pt-1">
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
onClick={addKeyValue}
|
||||
size="small"
|
||||
theme="solid"
|
||||
type="primary"
|
||||
className="shadow-sm hover:shadow-md transition-shadow px-4"
|
||||
>
|
||||
{t('添加模型区域')}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
<div className="mt-2 flex justify-center">
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
onClick={addKeyValue}
|
||||
type="primary"
|
||||
theme="outline"
|
||||
>
|
||||
{t('添加模型区域')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Slot>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -497,7 +545,6 @@ const JSONEditor = ({
|
||||
case 'region':
|
||||
return renderRegionEditor();
|
||||
case 'object':
|
||||
return renderObjectEditor();
|
||||
case 'keyValue':
|
||||
default:
|
||||
return renderKeyValueEditor();
|
||||
@@ -507,115 +554,92 @@ const JSONEditor = ({
|
||||
const hasJsonError = jsonError && jsonError.trim() !== '';
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{/* Label统一显示在上方 */}
|
||||
{label && (
|
||||
<div className="flex items-center">
|
||||
<Text className="text-sm font-medium text-gray-900">{label}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 编辑模式切换 */}
|
||||
<div className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
|
||||
<div className="flex items-center gap-2">
|
||||
{editMode === 'visual' && (
|
||||
<Text type="tertiary" size="small" className="bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-xs">
|
||||
{t('可视化模式')}
|
||||
</Text>
|
||||
)}
|
||||
{editMode === 'manual' && (
|
||||
<Text type="tertiary" size="small" className="bg-green-100 text-green-700 px-2 py-0.5 rounded text-xs">
|
||||
{t('手动编辑模式')}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{template && templateLabel && (
|
||||
<Button
|
||||
size="small"
|
||||
type="tertiary"
|
||||
onClick={fillTemplate}
|
||||
className="!text-semi-color-primary hover:bg-blue-50 text-xs"
|
||||
<Form.Slot label={label}>
|
||||
<Card
|
||||
header={
|
||||
<div className="flex justify-between items-center">
|
||||
<Tabs
|
||||
type="slash"
|
||||
activeKey={editMode}
|
||||
onChange={(key) => {
|
||||
if (key === 'manual' && editMode === 'visual') {
|
||||
setEditMode('manual');
|
||||
} else if (key === 'visual' && editMode === 'manual') {
|
||||
toggleEditMode();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{templateLabel}
|
||||
</Button>
|
||||
)}
|
||||
<Space size="tight">
|
||||
<Button
|
||||
size="small"
|
||||
type={editMode === 'visual' ? 'primary' : 'tertiary'}
|
||||
icon={<IconEdit />}
|
||||
onClick={toggleEditMode}
|
||||
disabled={editMode === 'manual' && hasJsonError}
|
||||
className={editMode === 'visual' ? 'shadow-sm' : ''}
|
||||
>
|
||||
{t('可视化')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type={editMode === 'manual' ? 'primary' : 'tertiary'}
|
||||
icon={<IconCode />}
|
||||
onClick={toggleEditMode}
|
||||
className={editMode === 'manual' ? 'shadow-sm' : ''}
|
||||
>
|
||||
{t('手动编辑')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
<TabPane tab={t('可视化')} itemKey="visual" />
|
||||
<TabPane tab={t('手动编辑')} itemKey="manual" />
|
||||
</Tabs>
|
||||
|
||||
{/* JSON错误提示 */}
|
||||
{hasJsonError && (
|
||||
<Banner
|
||||
type="danger"
|
||||
description={`JSON 格式错误: ${jsonError}`}
|
||||
className="!rounded-md text-sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 编辑器内容 */}
|
||||
{editMode === 'visual' ? (
|
||||
<div>
|
||||
<Card className="!p-3 !border-gray-200 !shadow-sm !rounded-md bg-white">
|
||||
{renderVisualEditor()}
|
||||
</Card>
|
||||
{/* 可视化模式下的额外文本显示在下方 */}
|
||||
{extraText && (
|
||||
<div className="text-xs text-gray-600 mt-0.5">
|
||||
{extraText}
|
||||
</div>
|
||||
)}
|
||||
{/* 隐藏的Form字段用于验证和数据绑定 */}
|
||||
<Form.Input
|
||||
field={field}
|
||||
value={value}
|
||||
rules={rules}
|
||||
style={{ display: 'none' }}
|
||||
noLabel={true}
|
||||
{...props}
|
||||
{template && templateLabel && (
|
||||
<Button
|
||||
type="tertiary"
|
||||
onClick={fillTemplate}
|
||||
size="small"
|
||||
>
|
||||
{templateLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
headerStyle={{ padding: '12px 16px' }}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
className="!rounded-2xl"
|
||||
>
|
||||
{/* JSON错误提示 */}
|
||||
{hasJsonError && (
|
||||
<Banner
|
||||
type="danger"
|
||||
description={`JSON 格式错误: ${jsonError}`}
|
||||
className="mb-3"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Form.TextArea
|
||||
field={field}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={handleManualChange}
|
||||
showClear={showClear}
|
||||
rows={Math.max(8, value ? value.split('\n').length : 8)}
|
||||
rules={rules}
|
||||
noLabel={true}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* 额外文本在手动编辑模式下显示 */}
|
||||
{extraText && editMode === 'manual' && (
|
||||
<div className="text-xs text-gray-600">
|
||||
{extraText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 编辑器内容 */}
|
||||
{editMode === 'visual' ? (
|
||||
<div>
|
||||
{renderVisualEditor()}
|
||||
{/* 隐藏的Form字段用于验证和数据绑定 */}
|
||||
<Form.Input
|
||||
field={field}
|
||||
value={value}
|
||||
rules={rules}
|
||||
style={{ display: 'none' }}
|
||||
noLabel={true}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<TextArea
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={handleManualChange}
|
||||
showClear={showClear}
|
||||
rows={Math.max(8, value ? value.split('\n').length : 8)}
|
||||
/>
|
||||
{/* 隐藏的Form字段用于验证和数据绑定 */}
|
||||
<Form.Input
|
||||
field={field}
|
||||
value={value}
|
||||
rules={rules}
|
||||
style={{ display: 'none' }}
|
||||
noLabel={true}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 额外文本显示在卡片底部 */}
|
||||
{extraText && (
|
||||
<div className="text-gray-600 mt-3 pt-3">
|
||||
{extraText}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Form.Slot>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user