2025-08-08 19:00:02 +08:00
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
2025-07-31 12:54:07 +08:00
|
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
|
import {
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Form,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
Banner,
|
2025-08-08 02:34:15 +08:00
|
|
|
|
Tabs,
|
|
|
|
|
|
TabPane,
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Input,
|
2025-07-31 12:54:07 +08:00
|
|
|
|
InputNumber,
|
|
|
|
|
|
Switch,
|
2025-08-08 02:34:15 +08:00
|
|
|
|
TextArea,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Col,
|
2025-08-08 02:59:45 +08:00
|
|
|
|
Divider,
|
2025-08-08 19:00:02 +08:00
|
|
|
|
Tooltip,
|
2025-07-31 12:54:07 +08:00
|
|
|
|
} from '@douyinfe/semi-ui';
|
|
|
|
|
|
import {
|
|
|
|
|
|
IconCode,
|
|
|
|
|
|
IconPlus,
|
|
|
|
|
|
IconDelete,
|
2025-08-08 02:34:15 +08:00
|
|
|
|
IconRefresh,
|
2025-08-08 19:00:02 +08:00
|
|
|
|
IconAlertTriangle,
|
2025-07-31 12:54:07 +08:00
|
|
|
|
} from '@douyinfe/semi-icons';
|
|
|
|
|
|
|
|
|
|
|
|
const { Text } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
const JSONEditor = ({
|
|
|
|
|
|
value = '',
|
|
|
|
|
|
onChange,
|
|
|
|
|
|
field,
|
|
|
|
|
|
label,
|
|
|
|
|
|
placeholder,
|
|
|
|
|
|
extraText,
|
2025-08-08 02:59:45 +08:00
|
|
|
|
extraFooter,
|
2025-07-31 12:54:07 +08:00
|
|
|
|
showClear = true,
|
|
|
|
|
|
template,
|
|
|
|
|
|
templateLabel,
|
2025-08-08 02:34:15 +08:00
|
|
|
|
editorType = 'keyValue',
|
2025-07-31 12:54:07 +08:00
|
|
|
|
rules = [],
|
|
|
|
|
|
formApi = null,
|
|
|
|
|
|
...props
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const { t } = useTranslation();
|
2025-08-08 02:34:15 +08:00
|
|
|
|
|
2025-08-08 19:00:02 +08:00
|
|
|
|
// 将对象转换为键值对数组(包含唯一ID)
|
|
|
|
|
|
const objectToKeyValueArray = useCallback((obj) => {
|
|
|
|
|
|
if (!obj || typeof obj !== 'object') return [];
|
|
|
|
|
|
return Object.entries(obj).map(([key, value], index) => ({
|
|
|
|
|
|
id: `${Date.now()}_${index}_${Math.random()}`, // 唯一ID
|
|
|
|
|
|
key,
|
|
|
|
|
|
value
|
|
|
|
|
|
}));
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 将键值对数组转换为对象(重复键时后面的会覆盖前面的)
|
|
|
|
|
|
const keyValueArrayToObject = useCallback((arr) => {
|
|
|
|
|
|
const result = {};
|
|
|
|
|
|
arr.forEach(item => {
|
|
|
|
|
|
if (item.key) {
|
|
|
|
|
|
result[item.key] = item.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化键值对数组
|
|
|
|
|
|
const [keyValuePairs, setKeyValuePairs] = useState(() => {
|
2025-08-08 02:34:15 +08:00
|
|
|
|
if (typeof value === 'string' && value.trim()) {
|
2025-07-31 12:54:07 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(value);
|
2025-08-08 19:00:02 +08:00
|
|
|
|
return objectToKeyValueArray(parsed);
|
2025-07-31 12:54:07 +08:00
|
|
|
|
} catch (error) {
|
2025-08-08 19:00:02 +08:00
|
|
|
|
return [];
|
2025-07-31 12:54:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
if (typeof value === 'object' && value !== null) {
|
2025-08-08 19:00:02 +08:00
|
|
|
|
return objectToKeyValueArray(value);
|
2025-08-08 02:34:15 +08:00
|
|
|
|
}
|
2025-08-08 19:00:02 +08:00
|
|
|
|
return [];
|
2025-07-31 12:54:07 +08:00
|
|
|
|
});
|
2025-08-08 02:34:15 +08:00
|
|
|
|
|
2025-08-08 19:00:02 +08:00
|
|
|
|
// 手动模式下的本地文本缓冲
|
2025-08-08 04:21:50 +08:00
|
|
|
|
const [manualText, setManualText] = useState(() => {
|
|
|
|
|
|
if (typeof value === 'string') return value;
|
|
|
|
|
|
if (value && typeof value === 'object') return JSON.stringify(value, null, 2);
|
|
|
|
|
|
return '';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-31 12:54:07 +08:00
|
|
|
|
// 根据键数量决定默认编辑模式
|
|
|
|
|
|
const [editMode, setEditMode] = useState(() => {
|
2025-08-08 02:34:15 +08:00
|
|
|
|
if (typeof value === 'string' && value.trim()) {
|
2025-07-31 12:54:07 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(value);
|
|
|
|
|
|
const keyCount = Object.keys(parsed).length;
|
|
|
|
|
|
return keyCount > 10 ? 'manual' : 'visual';
|
|
|
|
|
|
} catch (error) {
|
2025-08-01 17:21:25 +08:00
|
|
|
|
return 'manual';
|
2025-07-31 12:54:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'visual';
|
|
|
|
|
|
});
|
2025-08-08 19:00:02 +08:00
|
|
|
|
|
2025-07-31 12:54:07 +08:00
|
|
|
|
const [jsonError, setJsonError] = useState('');
|
|
|
|
|
|
|
2025-08-08 19:00:02 +08:00
|
|
|
|
// 计算重复的键
|
|
|
|
|
|
const duplicateKeys = useMemo(() => {
|
|
|
|
|
|
const keyCount = {};
|
|
|
|
|
|
const duplicates = new Set();
|
|
|
|
|
|
|
|
|
|
|
|
keyValuePairs.forEach(pair => {
|
|
|
|
|
|
if (pair.key) {
|
|
|
|
|
|
keyCount[pair.key] = (keyCount[pair.key] || 0) + 1;
|
|
|
|
|
|
if (keyCount[pair.key] > 1) {
|
|
|
|
|
|
duplicates.add(pair.key);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return duplicates;
|
|
|
|
|
|
}, [keyValuePairs]);
|
|
|
|
|
|
|
|
|
|
|
|
// 数据同步 - 当value变化时更新键值对数组
|
2025-07-31 12:54:07 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
try {
|
2025-08-08 02:34:15 +08:00
|
|
|
|
let parsed = {};
|
|
|
|
|
|
if (typeof value === 'string' && value.trim()) {
|
|
|
|
|
|
parsed = JSON.parse(value);
|
|
|
|
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
|
|
|
|
parsed = value;
|
|
|
|
|
|
}
|
2025-08-08 19:00:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 只在外部值真正改变时更新,避免循环更新
|
|
|
|
|
|
const currentObj = keyValueArrayToObject(keyValuePairs);
|
|
|
|
|
|
if (JSON.stringify(parsed) !== JSON.stringify(currentObj)) {
|
|
|
|
|
|
setKeyValuePairs(objectToKeyValueArray(parsed));
|
|
|
|
|
|
}
|
2025-07-31 12:54:07 +08:00
|
|
|
|
setJsonError('');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.log('JSON解析失败:', error.message);
|
|
|
|
|
|
setJsonError(error.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [value]);
|
|
|
|
|
|
|
2025-08-08 19:00:02 +08:00
|
|
|
|
// 外部 value 变化时,若不在手动模式,则同步手动文本
|
2025-08-08 04:21:50 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (editMode !== 'manual') {
|
|
|
|
|
|
if (typeof value === 'string') setManualText(value);
|
|
|
|
|
|
else if (value && typeof value === 'object') setManualText(JSON.stringify(value, null, 2));
|
|
|
|
|
|
else setManualText('');
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [value, editMode]);
|
|
|
|
|
|
|
2025-07-31 12:54:07 +08:00
|
|
|
|
// 处理可视化编辑的数据变化
|
2025-08-08 19:00:02 +08:00
|
|
|
|
const handleVisualChange = useCallback((newPairs) => {
|
|
|
|
|
|
setKeyValuePairs(newPairs);
|
|
|
|
|
|
const jsonObject = keyValueArrayToObject(newPairs);
|
|
|
|
|
|
const jsonString = Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2);
|
|
|
|
|
|
|
2025-07-31 12:54:07 +08:00
|
|
|
|
setJsonError('');
|
2025-08-08 02:34:15 +08:00
|
|
|
|
|
2025-08-08 19:00:02 +08:00
|
|
|
|
// 通过formApi设置值
|
2025-07-31 12:54:07 +08:00
|
|
|
|
if (formApi && field) {
|
|
|
|
|
|
formApi.setValue(field, jsonString);
|
|
|
|
|
|
}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
|
2025-07-31 12:54:07 +08:00
|
|
|
|
onChange?.(jsonString);
|
2025-08-08 19:00:02 +08:00
|
|
|
|
}, [onChange, formApi, field, keyValueArrayToObject]);
|
2025-07-31 12:54:07 +08:00
|
|
|
|
|
2025-08-08 19:00:02 +08:00
|
|
|
|
// 处理手动编辑的数据变化
|
2025-07-31 12:54:07 +08:00
|
|
|
|
const handleManualChange = useCallback((newValue) => {
|
2025-08-08 04:21:50 +08:00
|
|
|
|
setManualText(newValue);
|
2025-07-31 12:54:07 +08:00
|
|
|
|
if (newValue && newValue.trim()) {
|
|
|
|
|
|
try {
|
2025-08-08 19:00:02 +08:00
|
|
|
|
const parsed = JSON.parse(newValue);
|
|
|
|
|
|
setKeyValuePairs(objectToKeyValueArray(parsed));
|
2025-07-31 12:54:07 +08:00
|
|
|
|
setJsonError('');
|
2025-08-08 04:21:50 +08:00
|
|
|
|
onChange?.(newValue);
|
2025-07-31 12:54:07 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setJsonError(error.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2025-08-08 19:00:02 +08:00
|
|
|
|
setKeyValuePairs([]);
|
2025-07-31 12:54:07 +08:00
|
|
|
|
setJsonError('');
|
2025-08-08 04:21:50 +08:00
|
|
|
|
onChange?.('');
|
2025-07-31 12:54:07 +08:00
|
|
|
|
}
|
2025-08-08 19:00:02 +08:00
|
|
|
|
}, [onChange, objectToKeyValueArray]);
|
2025-07-31 12:54:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 切换编辑模式
|
|
|
|
|
|
const toggleEditMode = useCallback(() => {
|
|
|
|
|
|
if (editMode === 'visual') {
|
2025-08-08 19:00:02 +08:00
|
|
|
|
const jsonObject = keyValueArrayToObject(keyValuePairs);
|
|
|
|
|
|
setManualText(Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2));
|
2025-07-31 12:54:07 +08:00
|
|
|
|
setEditMode('manual');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
try {
|
2025-08-08 02:34:15 +08:00
|
|
|
|
let parsed = {};
|
2025-08-08 04:21:50 +08:00
|
|
|
|
if (manualText && manualText.trim()) {
|
|
|
|
|
|
parsed = JSON.parse(manualText);
|
|
|
|
|
|
} else if (typeof value === 'string' && value.trim()) {
|
2025-08-08 02:34:15 +08:00
|
|
|
|
parsed = JSON.parse(value);
|
|
|
|
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
|
|
|
|
parsed = value;
|
|
|
|
|
|
}
|
2025-08-08 19:00:02 +08:00
|
|
|
|
setKeyValuePairs(objectToKeyValueArray(parsed));
|
2025-07-31 12:54:07 +08:00
|
|
|
|
setJsonError('');
|
|
|
|
|
|
setEditMode('visual');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setJsonError(error.message);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-08 19:00:02 +08:00
|
|
|
|
}, [editMode, value, manualText, keyValuePairs, keyValueArrayToObject, objectToKeyValueArray]);
|
2025-07-31 12:54:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加键值对
|
|
|
|
|
|
const addKeyValue = useCallback(() => {
|
2025-08-08 19:00:02 +08:00
|
|
|
|
const newPairs = [...keyValuePairs];
|
|
|
|
|
|
const existingKeys = newPairs.map(p => p.key);
|
2025-07-31 12:54:07 +08:00
|
|
|
|
let counter = 1;
|
2025-08-08 02:34:15 +08:00
|
|
|
|
let newKey = `field_${counter}`;
|
2025-08-08 19:00:02 +08:00
|
|
|
|
while (existingKeys.includes(newKey)) {
|
2025-08-08 02:34:15 +08:00
|
|
|
|
counter += 1;
|
|
|
|
|
|
newKey = `field_${counter}`;
|
2025-07-31 12:54:07 +08:00
|
|
|
|
}
|
2025-08-08 19:00:02 +08:00
|
|
|
|
newPairs.push({
|
|
|
|
|
|
id: `${Date.now()}_${Math.random()}`,
|
|
|
|
|
|
key: newKey,
|
|
|
|
|
|
value: ''
|
|
|
|
|
|
});
|
|
|
|
|
|
handleVisualChange(newPairs);
|
|
|
|
|
|
}, [keyValuePairs, handleVisualChange]);
|
2025-07-31 12:54:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 删除键值对
|
2025-08-08 19:00:02 +08:00
|
|
|
|
const removeKeyValue = useCallback((id) => {
|
|
|
|
|
|
const newPairs = keyValuePairs.filter(pair => pair.id !== id);
|
|
|
|
|
|
handleVisualChange(newPairs);
|
|
|
|
|
|
}, [keyValuePairs, handleVisualChange]);
|
2025-07-31 12:54:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新键名
|
2025-08-08 19:00:02 +08:00
|
|
|
|
const updateKey = useCallback((id, newKey) => {
|
|
|
|
|
|
const newPairs = keyValuePairs.map(pair =>
|
|
|
|
|
|
pair.id === id ? { ...pair, key: newKey } : pair
|
|
|
|
|
|
);
|
|
|
|
|
|
handleVisualChange(newPairs);
|
|
|
|
|
|
}, [keyValuePairs, handleVisualChange]);
|
2025-07-31 12:54:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新值
|
2025-08-08 19:00:02 +08:00
|
|
|
|
const updateValue = useCallback((id, newValue) => {
|
|
|
|
|
|
const newPairs = keyValuePairs.map(pair =>
|
|
|
|
|
|
pair.id === id ? { ...pair, value: newValue } : pair
|
|
|
|
|
|
);
|
|
|
|
|
|
handleVisualChange(newPairs);
|
|
|
|
|
|
}, [keyValuePairs, handleVisualChange]);
|
2025-07-31 12:54:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 填入模板
|
|
|
|
|
|
const fillTemplate = useCallback(() => {
|
|
|
|
|
|
if (template) {
|
|
|
|
|
|
const templateString = JSON.stringify(template, null, 2);
|
2025-08-08 02:34:15 +08:00
|
|
|
|
|
2025-07-31 12:54:07 +08:00
|
|
|
|
if (formApi && field) {
|
|
|
|
|
|
formApi.setValue(field, templateString);
|
|
|
|
|
|
}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
|
2025-08-08 04:21:50 +08:00
|
|
|
|
setManualText(templateString);
|
2025-08-08 19:00:02 +08:00
|
|
|
|
setKeyValuePairs(objectToKeyValueArray(template));
|
2025-07-31 12:54:07 +08:00
|
|
|
|
onChange?.(templateString);
|
|
|
|
|
|
setJsonError('');
|
|
|
|
|
|
}
|
2025-08-08 19:00:02 +08:00
|
|
|
|
}, [template, onChange, formApi, field, objectToKeyValueArray]);
|
2025-08-08 02:34:15 +08:00
|
|
|
|
|
2025-08-08 19:00:02 +08:00
|
|
|
|
// 渲染值输入控件(支持嵌套)
|
|
|
|
|
|
const renderValueInput = (pairId, value) => {
|
2025-07-31 12:54:07 +08:00
|
|
|
|
const valueType = typeof value;
|
2025-08-08 02:34:15 +08:00
|
|
|
|
|
2025-07-31 12:54:07 +08:00
|
|
|
|
if (valueType === 'boolean') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
|
<Switch
|
|
|
|
|
|
checked={value}
|
2025-08-08 19:00:02 +08:00
|
|
|
|
onChange={(newValue) => updateValue(pairId, newValue)}
|
2025-07-31 12:54:07 +08:00
|
|
|
|
/>
|
2025-08-08 02:34:15 +08:00
|
|
|
|
<Text type="tertiary" className="ml-2">
|
2025-07-31 12:54:07 +08:00
|
|
|
|
{value ? t('true') : t('false')}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
|
2025-07-31 12:54:07 +08:00
|
|
|
|
if (valueType === 'number') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<InputNumber
|
|
|
|
|
|
value={value}
|
2025-08-08 19:00:02 +08:00
|
|
|
|
onChange={(newValue) => updateValue(pairId, newValue)}
|
2025-07-31 12:54:07 +08:00
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
|
placeholder={t('输入数字')}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
|
|
|
|
|
|
if (valueType === 'object' && value !== null) {
|
2025-08-08 19:00:02 +08:00
|
|
|
|
// 简化嵌套对象的处理,使用TextArea
|
2025-08-08 02:34:15 +08:00
|
|
|
|
return (
|
2025-08-08 19:00:02 +08:00
|
|
|
|
<TextArea
|
|
|
|
|
|
rows={2}
|
|
|
|
|
|
value={JSON.stringify(value, null, 2)}
|
|
|
|
|
|
onChange={(txt) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const obj = txt.trim() ? JSON.parse(txt) : {};
|
|
|
|
|
|
updateValue(pairId, obj);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 忽略解析错误
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder={t('输入JSON对象')}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 字符串或其他原始类型
|
|
|
|
|
|
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') {
|
|
|
|
|
|
const num = Number(newValue);
|
|
|
|
|
|
// 检查是否为整数
|
|
|
|
|
|
if (Number.isInteger(num)) {
|
|
|
|
|
|
convertedValue = num;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
updateValue(pairId, convertedValue);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染键值对编辑器
|
|
|
|
|
|
const renderKeyValueEditor = () => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
{/* 重复键警告 */}
|
|
|
|
|
|
{duplicateKeys.size > 0 && (
|
|
|
|
|
|
<Banner
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
icon={<IconAlertTriangle />}
|
|
|
|
|
|
description={
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Text strong>{t('存在重复的键名:')}</Text>
|
|
|
|
|
|
<Text>{Array.from(duplicateKeys).join(', ')}</Text>
|
|
|
|
|
|
<br />
|
|
|
|
|
|
<Text type="tertiary" size="small">
|
|
|
|
|
|
{t('注意:JSON中重复的键只会保留最后一个同名键的值')}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
className="mb-3"
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{keyValuePairs.length === 0 && (
|
|
|
|
|
|
<div className="text-center py-6 px-4">
|
|
|
|
|
|
<Text type="tertiary" className="text-gray-500 text-sm">
|
|
|
|
|
|
{t('暂无数据,点击下方按钮添加键值对')}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
</Text>
|
2025-08-08 19:00:02 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{keyValuePairs.map((pair, index) => {
|
|
|
|
|
|
const isDuplicate = duplicateKeys.has(pair.key);
|
|
|
|
|
|
const isLastDuplicate = isDuplicate &&
|
|
|
|
|
|
keyValuePairs.slice(index + 1).every(p => p.key !== pair.key);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Row key={pair.id} gutter={8} align="middle">
|
|
|
|
|
|
<Col span={6}>
|
|
|
|
|
|
<div className="relative">
|
2025-08-08 02:34:15 +08:00
|
|
|
|
<Input
|
2025-08-08 19:00:02 +08:00
|
|
|
|
placeholder={t('键名')}
|
|
|
|
|
|
value={pair.key}
|
|
|
|
|
|
onChange={(newKey) => updateKey(pair.id, newKey)}
|
|
|
|
|
|
status={isDuplicate ? 'warning' : undefined}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
/>
|
2025-08-08 19:00:02 +08:00
|
|
|
|
{isDuplicate && (
|
|
|
|
|
|
<Tooltip
|
|
|
|
|
|
content={
|
|
|
|
|
|
isLastDuplicate
|
|
|
|
|
|
? t('这是重复键中的最后一个,其值将被使用')
|
|
|
|
|
|
: t('重复的键名,此值将被后面的同名键覆盖')
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<IconAlertTriangle
|
|
|
|
|
|
className="absolute right-2 top-1/2 transform -translate-y-1/2"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
color: isLastDuplicate ? '#ff7d00' : '#faad14',
|
|
|
|
|
|
fontSize: '14px'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={16}>
|
|
|
|
|
|
{renderValueInput(pair.id, pair.value)}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={2}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<IconDelete />}
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
theme="borderless"
|
2025-08-08 19:00:02 +08:00
|
|
|
|
onClick={() => removeKeyValue(pair.id)}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2025-08-08 19:00:02 +08:00
|
|
|
|
);
|
|
|
|
|
|
})}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
|
2025-08-08 19:00:02 +08:00
|
|
|
|
<div className="mt-2 flex justify-center">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<IconPlus />}
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
theme="outline"
|
|
|
|
|
|
onClick={addKeyValue}
|
|
|
|
|
|
>
|
|
|
|
|
|
{t('添加键值对')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2025-08-08 02:34:15 +08:00
|
|
|
|
</div>
|
2025-07-31 12:54:07 +08:00
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-08 19:00:02 +08:00
|
|
|
|
// 渲染区域编辑器(特殊格式)- 也需要改造以支持重复键
|
2025-07-31 12:54:07 +08:00
|
|
|
|
const renderRegionEditor = () => {
|
2025-08-08 19:00:02 +08:00
|
|
|
|
const defaultPair = keyValuePairs.find(pair => pair.key === 'default');
|
|
|
|
|
|
const modelPairs = keyValuePairs.filter(pair => pair.key !== 'default');
|
2025-08-08 02:34:15 +08:00
|
|
|
|
|
2025-07-31 12:54:07 +08:00
|
|
|
|
return (
|
2025-08-08 02:34:15 +08:00
|
|
|
|
<div className="space-y-2">
|
2025-08-08 19:00:02 +08:00
|
|
|
|
{/* 重复键警告 */}
|
|
|
|
|
|
{duplicateKeys.size > 0 && (
|
|
|
|
|
|
<Banner
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
icon={<IconAlertTriangle />}
|
|
|
|
|
|
description={
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Text strong>{t('存在重复的键名:')}</Text>
|
|
|
|
|
|
<Text>{Array.from(duplicateKeys).join(', ')}</Text>
|
|
|
|
|
|
<br />
|
|
|
|
|
|
<Text type="tertiary" size="small">
|
|
|
|
|
|
{t('注意:JSON中重复的键只会保留最后一个同名键的值')}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
className="mb-3"
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-07-31 12:54:07 +08:00
|
|
|
|
{/* 默认区域 */}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
<Form.Slot label={t('默认区域')}>
|
2025-07-31 12:54:07 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
placeholder={t('默认区域,如: us-central1')}
|
2025-08-08 19:00:02 +08:00
|
|
|
|
value={defaultPair ? defaultPair.value : ''}
|
|
|
|
|
|
onChange={(value) => {
|
|
|
|
|
|
if (defaultPair) {
|
|
|
|
|
|
updateValue(defaultPair.id, value);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const newPairs = [...keyValuePairs, {
|
|
|
|
|
|
id: `${Date.now()}_${Math.random()}`,
|
|
|
|
|
|
key: 'default',
|
|
|
|
|
|
value: value
|
|
|
|
|
|
}];
|
|
|
|
|
|
handleVisualChange(newPairs);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
2025-07-31 12:54:07 +08:00
|
|
|
|
/>
|
2025-08-08 02:34:15 +08:00
|
|
|
|
</Form.Slot>
|
|
|
|
|
|
|
2025-07-31 12:54:07 +08:00
|
|
|
|
{/* 模型专用区域 */}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
<Form.Slot label={t('模型专用区域')}>
|
|
|
|
|
|
<div>
|
2025-08-08 19:00:02 +08:00
|
|
|
|
{modelPairs.map((pair) => {
|
|
|
|
|
|
const isDuplicate = duplicateKeys.has(pair.key);
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Row key={pair.id} gutter={8} align="middle" className="mb-2">
|
|
|
|
|
|
<Col span={10}>
|
|
|
|
|
|
<div className="relative">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder={t('模型名称')}
|
|
|
|
|
|
value={pair.key}
|
|
|
|
|
|
onChange={(newKey) => updateKey(pair.id, newKey)}
|
|
|
|
|
|
status={isDuplicate ? 'warning' : undefined}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{isDuplicate && (
|
|
|
|
|
|
<Tooltip content={t('重复的键名')}>
|
|
|
|
|
|
<IconAlertTriangle
|
|
|
|
|
|
className="absolute right-2 top-1/2 transform -translate-y-1/2"
|
|
|
|
|
|
style={{ color: '#faad14', fontSize: '14px' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder={t('区域')}
|
|
|
|
|
|
value={pair.value}
|
|
|
|
|
|
onChange={(newValue) => updateValue(pair.id, newValue)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={2}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<IconDelete />}
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
theme="borderless"
|
|
|
|
|
|
onClick={() => removeKeyValue(pair.id)}
|
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
|
|
|
|
|
|
<div className="mt-2 flex justify-center">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<IconPlus />}
|
|
|
|
|
|
onClick={addKeyValue}
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
theme="outline"
|
|
|
|
|
|
>
|
|
|
|
|
|
{t('添加模型区域')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2025-07-31 12:54:07 +08:00
|
|
|
|
</div>
|
2025-08-08 02:34:15 +08:00
|
|
|
|
</Form.Slot>
|
2025-07-31 12:54:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染可视化编辑器
|
|
|
|
|
|
const renderVisualEditor = () => {
|
|
|
|
|
|
switch (editorType) {
|
|
|
|
|
|
case 'region':
|
|
|
|
|
|
return renderRegionEditor();
|
|
|
|
|
|
case 'object':
|
|
|
|
|
|
case 'keyValue':
|
|
|
|
|
|
default:
|
|
|
|
|
|
return renderKeyValueEditor();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const hasJsonError = jsonError && jsonError.trim() !== '';
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-08-08 02:34:15 +08:00
|
|
|
|
<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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
2025-07-31 12:54:07 +08:00
|
|
|
|
>
|
2025-08-08 02:34:15 +08:00
|
|
|
|
<TabPane tab={t('可视化')} itemKey="visual" />
|
|
|
|
|
|
<TabPane tab={t('手动编辑')} itemKey="manual" />
|
|
|
|
|
|
</Tabs>
|
2025-07-31 12:54:07 +08:00
|
|
|
|
|
2025-08-08 02:34:15 +08:00
|
|
|
|
{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"
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-07-31 12:54:07 +08:00
|
|
|
|
|
2025-08-08 02:34:15 +08:00
|
|
|
|
{/* 编辑器内容 */}
|
|
|
|
|
|
{editMode === 'visual' ? (
|
|
|
|
|
|
<div>
|
2025-07-31 12:54:07 +08:00
|
|
|
|
{renderVisualEditor()}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
{/* 隐藏的Form字段用于验证和数据绑定 */}
|
|
|
|
|
|
<Form.Input
|
|
|
|
|
|
field={field}
|
|
|
|
|
|
value={value}
|
|
|
|
|
|
rules={rules}
|
|
|
|
|
|
style={{ display: 'none' }}
|
|
|
|
|
|
noLabel={true}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<TextArea
|
|
|
|
|
|
placeholder={placeholder}
|
2025-08-08 04:21:50 +08:00
|
|
|
|
value={manualText}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
onChange={handleManualChange}
|
|
|
|
|
|
showClear={showClear}
|
2025-08-08 04:21:50 +08:00
|
|
|
|
rows={Math.max(8, manualText ? manualText.split('\n').length : 8)}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
/>
|
|
|
|
|
|
{/* 隐藏的Form字段用于验证和数据绑定 */}
|
|
|
|
|
|
<Form.Input
|
|
|
|
|
|
field={field}
|
|
|
|
|
|
value={value}
|
|
|
|
|
|
rules={rules}
|
|
|
|
|
|
style={{ display: 'none' }}
|
|
|
|
|
|
noLabel={true}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-07-31 12:54:07 +08:00
|
|
|
|
|
2025-08-08 02:34:15 +08:00
|
|
|
|
{/* 额外文本显示在卡片底部 */}
|
|
|
|
|
|
{extraText && (
|
2025-08-08 02:59:45 +08:00
|
|
|
|
<Divider margin='12px' align='center'>
|
2025-08-08 04:09:53 +08:00
|
|
|
|
<Text type="tertiary" size="small">{extraText}</Text>
|
2025-08-08 02:59:45 +08:00
|
|
|
|
</Divider>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{extraFooter && (
|
|
|
|
|
|
<div className="mt-1">
|
|
|
|
|
|
{extraFooter}
|
2025-08-08 02:34:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</Form.Slot>
|
2025-07-31 12:54:07 +08:00
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-08 19:00:02 +08:00
|
|
|
|
export default JSONEditor;
|