|
|
@@ -1,4 +1,23 @@
|
|
|
-import React, { useState, useEffect, useCallback } from 'react';
|
|
|
+/*
|
|
|
+Copyright (C) 2025 QuantumNous
|
|
|
+
|
|
|
+This program is free software: you can redistribute it and/or modify
|
|
|
+it under the terms of the GNU Affero General Public License as
|
|
|
+published by the Free Software Foundation, either version 3 of the
|
|
|
+License, or (at your option) any later version.
|
|
|
+
|
|
|
+This program is distributed in the hope that it will be useful,
|
|
|
+but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
+GNU Affero General Public License for more details.
|
|
|
+
|
|
|
+You should have received a copy of the GNU Affero General Public License
|
|
|
+along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
+
|
|
|
+For commercial licensing, please contact [email protected]
|
|
|
+*/
|
|
|
+
|
|
|
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
import {
|
|
|
Button,
|
|
|
@@ -15,16 +34,22 @@ import {
|
|
|
Row,
|
|
|
Col,
|
|
|
Divider,
|
|
|
+ Tooltip,
|
|
|
} from '@douyinfe/semi-ui';
|
|
|
import {
|
|
|
- IconCode,
|
|
|
IconPlus,
|
|
|
IconDelete,
|
|
|
- IconRefresh,
|
|
|
+ IconAlertTriangle,
|
|
|
} from '@douyinfe/semi-icons';
|
|
|
|
|
|
const { Text } = Typography;
|
|
|
|
|
|
+// 唯一 ID 生成器,确保在组件生命周期内稳定且递增
|
|
|
+const generateUniqueId = (() => {
|
|
|
+ let counter = 0;
|
|
|
+ return () => `kv_${counter++}`;
|
|
|
+})();
|
|
|
+
|
|
|
const JSONEditor = ({
|
|
|
value = '',
|
|
|
onChange,
|
|
|
@@ -43,24 +68,51 @@ const JSONEditor = ({
|
|
|
}) => {
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
- // 初始化JSON数据
|
|
|
- const [jsonData, setJsonData] = useState(() => {
|
|
|
- // 初始化时解析JSON数据
|
|
|
+ // 将对象转换为键值对数组(包含唯一ID)
|
|
|
+ const objectToKeyValueArray = useCallback((obj, prevPairs = []) => {
|
|
|
+ if (!obj || typeof obj !== 'object') return [];
|
|
|
+
|
|
|
+ const entries = Object.entries(obj);
|
|
|
+ return entries.map(([key, value], index) => {
|
|
|
+ // 如果上一次转换后同位置的键一致,则沿用其 id,保持 React key 稳定
|
|
|
+ const prev = prevPairs[index];
|
|
|
+ const shouldReuseId = prev && prev.key === key;
|
|
|
+ return {
|
|
|
+ id: shouldReuseId ? prev.id : generateUniqueId(),
|
|
|
+ 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(() => {
|
|
|
if (typeof value === 'string' && value.trim()) {
|
|
|
try {
|
|
|
const parsed = JSON.parse(value);
|
|
|
- return parsed;
|
|
|
+ return objectToKeyValueArray(parsed);
|
|
|
} catch (error) {
|
|
|
- return {};
|
|
|
+ return [];
|
|
|
}
|
|
|
}
|
|
|
if (typeof value === 'object' && value !== null) {
|
|
|
- return value;
|
|
|
+ return objectToKeyValueArray(value);
|
|
|
}
|
|
|
- return {};
|
|
|
+ return [];
|
|
|
});
|
|
|
|
|
|
- // 手动模式下的本地文本缓冲,避免无效 JSON 时被外部值重置
|
|
|
+ // 手动模式下的本地文本缓冲
|
|
|
const [manualText, setManualText] = useState(() => {
|
|
|
if (typeof value === 'string') return value;
|
|
|
if (value && typeof value === 'object') return JSON.stringify(value, null, 2);
|
|
|
@@ -69,22 +121,38 @@ const JSONEditor = ({
|
|
|
|
|
|
// 根据键数量决定默认编辑模式
|
|
|
const [editMode, setEditMode] = useState(() => {
|
|
|
- // 如果初始JSON数据的键数量大于10个,则默认使用手动模式
|
|
|
if (typeof value === 'string' && value.trim()) {
|
|
|
try {
|
|
|
const parsed = JSON.parse(value);
|
|
|
const keyCount = Object.keys(parsed).length;
|
|
|
return keyCount > 10 ? 'manual' : 'visual';
|
|
|
} catch (error) {
|
|
|
- // JSON无效时默认显示手动编辑模式
|
|
|
return 'manual';
|
|
|
}
|
|
|
}
|
|
|
return 'visual';
|
|
|
});
|
|
|
+
|
|
|
const [jsonError, setJsonError] = useState('');
|
|
|
|
|
|
- // 数据同步 - 当value变化时总是更新jsonData(如果JSON有效)
|
|
|
+ // 计算重复的键
|
|
|
+ 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变化时更新键值对数组
|
|
|
useEffect(() => {
|
|
|
try {
|
|
|
let parsed = {};
|
|
|
@@ -93,16 +161,20 @@ const JSONEditor = ({
|
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
|
parsed = value;
|
|
|
}
|
|
|
- setJsonData(parsed);
|
|
|
+
|
|
|
+ // 只在外部值真正改变时更新,避免循环更新
|
|
|
+ const currentObj = keyValueArrayToObject(keyValuePairs);
|
|
|
+ if (JSON.stringify(parsed) !== JSON.stringify(currentObj)) {
|
|
|
+ setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs));
|
|
|
+ }
|
|
|
setJsonError('');
|
|
|
} catch (error) {
|
|
|
console.log('JSON解析失败:', error.message);
|
|
|
setJsonError(error.message);
|
|
|
- // JSON格式错误时不更新jsonData
|
|
|
}
|
|
|
}, [value]);
|
|
|
|
|
|
- // 外部 value 变化时,若不在手动模式,则同步手动文本;在手动模式下不打断用户输入
|
|
|
+ // 外部 value 变化时,若不在手动模式,则同步手动文本
|
|
|
useEffect(() => {
|
|
|
if (editMode !== 'manual') {
|
|
|
if (typeof value === 'string') setManualText(value);
|
|
|
@@ -112,45 +184,47 @@ const JSONEditor = ({
|
|
|
}, [value, editMode]);
|
|
|
|
|
|
// 处理可视化编辑的数据变化
|
|
|
- const handleVisualChange = useCallback((newData) => {
|
|
|
- setJsonData(newData);
|
|
|
+ const handleVisualChange = useCallback((newPairs) => {
|
|
|
+ setKeyValuePairs(newPairs);
|
|
|
+ const jsonObject = keyValueArrayToObject(newPairs);
|
|
|
+ const jsonString = Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2);
|
|
|
+
|
|
|
setJsonError('');
|
|
|
- const jsonString = Object.keys(newData).length === 0 ? '' : JSON.stringify(newData, null, 2);
|
|
|
|
|
|
- // 通过formApi设置值(如果提供的话)
|
|
|
+ // 通过formApi设置值
|
|
|
if (formApi && field) {
|
|
|
formApi.setValue(field, jsonString);
|
|
|
}
|
|
|
|
|
|
onChange?.(jsonString);
|
|
|
- }, [onChange, formApi, field]);
|
|
|
+ }, [onChange, formApi, field, keyValueArrayToObject]);
|
|
|
|
|
|
- // 处理手动编辑的数据变化(无效 JSON 不阻断输入,也不立刻回传上游)
|
|
|
+ // 处理手动编辑的数据变化
|
|
|
const handleManualChange = useCallback((newValue) => {
|
|
|
setManualText(newValue);
|
|
|
if (newValue && newValue.trim()) {
|
|
|
try {
|
|
|
- JSON.parse(newValue);
|
|
|
+ const parsed = JSON.parse(newValue);
|
|
|
+ setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs));
|
|
|
setJsonError('');
|
|
|
onChange?.(newValue);
|
|
|
} catch (error) {
|
|
|
setJsonError(error.message);
|
|
|
- // 无效 JSON 时不回传,避免外部值把输入重置
|
|
|
}
|
|
|
} else {
|
|
|
+ setKeyValuePairs([]);
|
|
|
setJsonError('');
|
|
|
onChange?.('');
|
|
|
}
|
|
|
- }, [onChange]);
|
|
|
+ }, [onChange, objectToKeyValueArray, keyValuePairs]);
|
|
|
|
|
|
// 切换编辑模式
|
|
|
const toggleEditMode = useCallback(() => {
|
|
|
if (editMode === 'visual') {
|
|
|
- // 从可视化模式切换到手动模式
|
|
|
- setManualText(Object.keys(jsonData).length === 0 ? '' : JSON.stringify(jsonData, null, 2));
|
|
|
+ const jsonObject = keyValueArrayToObject(keyValuePairs);
|
|
|
+ setManualText(Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2));
|
|
|
setEditMode('manual');
|
|
|
} else {
|
|
|
- // 从手动模式切换到可视化模式,需要验证JSON
|
|
|
try {
|
|
|
let parsed = {};
|
|
|
if (manualText && manualText.trim()) {
|
|
|
@@ -160,177 +234,74 @@ const JSONEditor = ({
|
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
|
parsed = value;
|
|
|
}
|
|
|
- setJsonData(parsed);
|
|
|
+ setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs));
|
|
|
setJsonError('');
|
|
|
setEditMode('visual');
|
|
|
} catch (error) {
|
|
|
setJsonError(error.message);
|
|
|
- // JSON格式错误时不切换模式
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
- }, [editMode, value, manualText, jsonData]);
|
|
|
+ }, [editMode, value, manualText, keyValuePairs, keyValueArrayToObject, objectToKeyValueArray]);
|
|
|
|
|
|
// 添加键值对
|
|
|
const addKeyValue = useCallback(() => {
|
|
|
- const newData = { ...jsonData };
|
|
|
- const keys = Object.keys(newData);
|
|
|
+ const newPairs = [...keyValuePairs];
|
|
|
+ const existingKeys = newPairs.map(p => p.key);
|
|
|
let counter = 1;
|
|
|
let newKey = `field_${counter}`;
|
|
|
- while (newData.hasOwnProperty(newKey)) {
|
|
|
+ while (existingKeys.includes(newKey)) {
|
|
|
counter += 1;
|
|
|
newKey = `field_${counter}`;
|
|
|
}
|
|
|
- newData[newKey] = '';
|
|
|
- handleVisualChange(newData);
|
|
|
- }, [jsonData, handleVisualChange]);
|
|
|
+ newPairs.push({
|
|
|
+ id: generateUniqueId(),
|
|
|
+ key: newKey,
|
|
|
+ value: ''
|
|
|
+ });
|
|
|
+ handleVisualChange(newPairs);
|
|
|
+ }, [keyValuePairs, handleVisualChange]);
|
|
|
|
|
|
// 删除键值对
|
|
|
- const removeKeyValue = useCallback((keyToRemove) => {
|
|
|
- const newData = { ...jsonData };
|
|
|
- delete newData[keyToRemove];
|
|
|
- handleVisualChange(newData);
|
|
|
- }, [jsonData, handleVisualChange]);
|
|
|
+ const removeKeyValue = useCallback((id) => {
|
|
|
+ const newPairs = keyValuePairs.filter(pair => pair.id !== id);
|
|
|
+ handleVisualChange(newPairs);
|
|
|
+ }, [keyValuePairs, handleVisualChange]);
|
|
|
|
|
|
// 更新键名
|
|
|
- const updateKey = useCallback((oldKey, newKey) => {
|
|
|
- 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]);
|
|
|
+ const updateKey = useCallback((id, newKey) => {
|
|
|
+ const newPairs = keyValuePairs.map(pair =>
|
|
|
+ pair.id === id ? { ...pair, key: newKey } : pair
|
|
|
+ );
|
|
|
+ handleVisualChange(newPairs);
|
|
|
+ }, [keyValuePairs, handleVisualChange]);
|
|
|
|
|
|
// 更新值
|
|
|
- const updateValue = useCallback((key, newValue) => {
|
|
|
- const newData = { ...jsonData };
|
|
|
- newData[key] = newValue;
|
|
|
- handleVisualChange(newData);
|
|
|
- }, [jsonData, handleVisualChange]);
|
|
|
+ const updateValue = useCallback((id, newValue) => {
|
|
|
+ const newPairs = keyValuePairs.map(pair =>
|
|
|
+ pair.id === id ? { ...pair, value: newValue } : pair
|
|
|
+ );
|
|
|
+ handleVisualChange(newPairs);
|
|
|
+ }, [keyValuePairs, handleVisualChange]);
|
|
|
|
|
|
// 填入模板
|
|
|
const fillTemplate = useCallback(() => {
|
|
|
if (template) {
|
|
|
const templateString = JSON.stringify(template, null, 2);
|
|
|
|
|
|
- // 通过formApi设置值(如果提供的话)
|
|
|
if (formApi && field) {
|
|
|
formApi.setValue(field, templateString);
|
|
|
}
|
|
|
|
|
|
- // 同步内部与外部值,避免出现杂字符
|
|
|
setManualText(templateString);
|
|
|
- setJsonData(template);
|
|
|
+ setKeyValuePairs(objectToKeyValueArray(template, keyValuePairs));
|
|
|
onChange?.(templateString);
|
|
|
-
|
|
|
- // 清除错误状态
|
|
|
setJsonError('');
|
|
|
}
|
|
|
- }, [template, onChange, editMode, formApi, field]);
|
|
|
-
|
|
|
- // 渲染键值对编辑器
|
|
|
- const renderKeyValueEditor = () => {
|
|
|
- if (typeof jsonData !== 'object' || jsonData === null) {
|
|
|
- return (
|
|
|
- <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('无效的JSON数据,请检查格式')}
|
|
|
- </Text>
|
|
|
- </div>
|
|
|
- );
|
|
|
- }
|
|
|
- const entries = Object.entries(jsonData);
|
|
|
-
|
|
|
- return (
|
|
|
- <div className="space-y-1">
|
|
|
- {entries.length === 0 && (
|
|
|
- <div className="text-center py-6 px-4">
|
|
|
- <Text type="tertiary" className="text-gray-500 text-sm">
|
|
|
- {t('暂无数据,点击下方按钮添加键值对')}
|
|
|
- </Text>
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
- {entries.map(([key, value], index) => (
|
|
|
- <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="mt-2 flex justify-center">
|
|
|
- <Button
|
|
|
- icon={<IconPlus />}
|
|
|
- type="primary"
|
|
|
- theme="outline"
|
|
|
- onClick={addKeyValue}
|
|
|
- >
|
|
|
- {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]);
|
|
|
+ }, [template, onChange, formApi, field, objectToKeyValueArray, keyValuePairs]);
|
|
|
|
|
|
- // 渲染参数值输入控件(支持嵌套)
|
|
|
- const renderValueInput = (key, value) => {
|
|
|
+ // 渲染值输入控件(支持嵌套)
|
|
|
+ const renderValueInput = (pairId, value) => {
|
|
|
const valueType = typeof value;
|
|
|
|
|
|
if (valueType === 'boolean') {
|
|
|
@@ -338,7 +309,7 @@ const JSONEditor = ({
|
|
|
<div className="flex items-center">
|
|
|
<Switch
|
|
|
checked={value}
|
|
|
- onChange={(newValue) => updateValue(key, newValue)}
|
|
|
+ onChange={(newValue) => updateValue(pairId, newValue)}
|
|
|
/>
|
|
|
<Text type="tertiary" className="ml-2">
|
|
|
{value ? t('true') : t('false')}
|
|
|
@@ -351,195 +322,239 @@ const JSONEditor = ({
|
|
|
return (
|
|
|
<InputNumber
|
|
|
value={value}
|
|
|
- onChange={(newValue) => updateValue(key, newValue)}
|
|
|
+ onChange={(newValue) => updateValue(pairId, newValue)}
|
|
|
style={{ width: '100%' }}
|
|
|
- step={key === 'temperature' ? 0.1 : 1}
|
|
|
- precision={key === 'temperature' ? 2 : 0}
|
|
|
placeholder={t('输入数字')}
|
|
|
/>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
if (valueType === 'object' && value !== null) {
|
|
|
- // 渲染嵌套对象
|
|
|
- const entries = Object.entries(value);
|
|
|
+ // 简化嵌套对象的处理,使用TextArea
|
|
|
return (
|
|
|
- <Card className="!rounded-2xl">
|
|
|
- {entries.length === 0 && (
|
|
|
- <Text type="tertiary" className="text-gray-500 text-xs">
|
|
|
- {t('空对象,点击下方加号添加字段')}
|
|
|
+ <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 !== '') {
|
|
|
+ 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('暂无数据,点击下方按钮添加键值对')}
|
|
|
</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
|
|
|
- }
|
|
|
- }}
|
|
|
- />
|
|
|
- ) : (
|
|
|
+ </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">
|
|
|
<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);
|
|
|
- }}
|
|
|
+ placeholder={t('键名')}
|
|
|
+ value={pair.key}
|
|
|
+ onChange={(newKey) => updateKey(pair.id, newKey)}
|
|
|
+ status={isDuplicate ? 'warning' : undefined}
|
|
|
/>
|
|
|
- )}
|
|
|
+ {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)}
|
|
|
</Col>
|
|
|
<Col span={2}>
|
|
|
<Button
|
|
|
- size="small"
|
|
|
icon={<IconDelete />}
|
|
|
type="danger"
|
|
|
theme="borderless"
|
|
|
- onClick={() => {
|
|
|
- const newData = { ...jsonData };
|
|
|
- delete newData[key][nestedKey];
|
|
|
- handleVisualChange(newData);
|
|
|
- }}
|
|
|
+ onClick={() => removeKeyValue(pair.id)}
|
|
|
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 (
|
|
|
- <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 className="mt-2 flex justify-center">
|
|
|
+ <Button
|
|
|
+ icon={<IconPlus />}
|
|
|
+ type="primary"
|
|
|
+ theme="outline"
|
|
|
+ onClick={addKeyValue}
|
|
|
+ >
|
|
|
+ {t('添加键值对')}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
- // 渲染区域编辑器(特殊格式)
|
|
|
+ // 渲染区域编辑器(特殊格式)- 也需要改造以支持重复键
|
|
|
const renderRegionEditor = () => {
|
|
|
- const entries = Object.entries(jsonData);
|
|
|
- const defaultEntry = entries.find(([key]) => key === 'default');
|
|
|
- const modelEntries = entries.filter(([key]) => key !== 'default');
|
|
|
+ const defaultPair = keyValuePairs.find(pair => pair.key === 'default');
|
|
|
+ const modelPairs = keyValuePairs.filter(pair => pair.key !== 'default');
|
|
|
|
|
|
return (
|
|
|
<div className="space-y-2">
|
|
|
+ {/* 重复键警告 */}
|
|
|
+ {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"
|
|
|
+ />
|
|
|
+ )}
|
|
|
+
|
|
|
{/* 默认区域 */}
|
|
|
<Form.Slot label={t('默认区域')}>
|
|
|
<Input
|
|
|
placeholder={t('默认区域,如: us-central1')}
|
|
|
- value={defaultEntry ? defaultEntry[1] : ''}
|
|
|
- onChange={(value) => updateValue('default', value)}
|
|
|
+ value={defaultPair ? defaultPair.value : ''}
|
|
|
+ onChange={(value) => {
|
|
|
+ if (defaultPair) {
|
|
|
+ updateValue(defaultPair.id, value);
|
|
|
+ } else {
|
|
|
+ const newPairs = [...keyValuePairs, {
|
|
|
+ id: generateUniqueId(),
|
|
|
+ key: 'default',
|
|
|
+ value: value
|
|
|
+ }];
|
|
|
+ handleVisualChange(newPairs);
|
|
|
+ }
|
|
|
+ }}
|
|
|
/>
|
|
|
</Form.Slot>
|
|
|
|
|
|
{/* 模型专用区域 */}
|
|
|
<Form.Slot label={t('模型专用区域')}>
|
|
|
<div>
|
|
|
- {modelEntries.map(([modelName, region], index) => (
|
|
|
- <Row key={index} gutter={8} align="middle" className="mb-2">
|
|
|
- <Col span={10}>
|
|
|
- <Input
|
|
|
- placeholder={t('模型名称')}
|
|
|
- value={modelName}
|
|
|
- onChange={(newKey) => updateKey(modelName, newKey)}
|
|
|
- />
|
|
|
- </Col>
|
|
|
- <Col span={12}>
|
|
|
- <Input
|
|
|
- placeholder={t('区域')}
|
|
|
- value={region}
|
|
|
- onChange={(newValue) => updateValue(modelName, newValue)}
|
|
|
- />
|
|
|
- </Col>
|
|
|
- <Col span={2}>
|
|
|
- <Button
|
|
|
- icon={<IconDelete />}
|
|
|
- type="danger"
|
|
|
- theme="borderless"
|
|
|
- onClick={() => removeKeyValue(modelName)}
|
|
|
- style={{ width: '100%' }}
|
|
|
- />
|
|
|
- </Col>
|
|
|
- </Row>
|
|
|
- ))}
|
|
|
+ {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>
|
|
|
+ );
|
|
|
+ })}
|
|
|
|
|
|
<div className="mt-2 flex justify-center">
|
|
|
<Button
|
|
|
@@ -666,4 +681,4 @@ const JSONEditor = ({
|
|
|
);
|
|
|
};
|
|
|
|
|
|
-export default JSONEditor;
|
|
|
+export default JSONEditor;
|