feat: optimized JSONEditor in duplicate key handling

Merge pull request #1534 from HynoR/feat/je
This commit is contained in:
同語
2025-08-09 13:22:16 +08:00
committed by GitHub

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Button, Button,
@@ -15,12 +15,14 @@ import {
Row, Row,
Col, Col,
Divider, Divider,
Tooltip,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
IconCode, IconCode,
IconPlus, IconPlus,
IconDelete, IconDelete,
IconRefresh, IconRefresh,
IconAlertTriangle,
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
const { Text } = Typography; const { Text } = Typography;
@@ -43,24 +45,44 @@ const JSONEditor = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
// 初始化JSON数据 // 将对象转换为键值对数组包含唯一ID
const [jsonData, setJsonData] = useState(() => { const objectToKeyValueArray = useCallback((obj) => {
// 初始化时解析JSON数据 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(() => {
if (typeof value === 'string' && value.trim()) { if (typeof value === 'string' && value.trim()) {
try { try {
const parsed = JSON.parse(value); const parsed = JSON.parse(value);
return parsed; return objectToKeyValueArray(parsed);
} catch (error) { } catch (error) {
return {}; return [];
} }
} }
if (typeof value === 'object' && value !== null) { if (typeof value === 'object' && value !== null) {
return value; return objectToKeyValueArray(value);
} }
return {}; return [];
}); });
// 手动模式下的本地文本缓冲,避免无效 JSON 时被外部值重置 // 手动模式下的本地文本缓冲
const [manualText, setManualText] = useState(() => { const [manualText, setManualText] = useState(() => {
if (typeof value === 'string') return value; if (typeof value === 'string') return value;
if (value && typeof value === 'object') return JSON.stringify(value, null, 2); if (value && typeof value === 'object') return JSON.stringify(value, null, 2);
@@ -69,22 +91,38 @@ const JSONEditor = ({
// 根据键数量决定默认编辑模式 // 根据键数量决定默认编辑模式
const [editMode, setEditMode] = useState(() => { const [editMode, setEditMode] = useState(() => {
// 如果初始JSON数据的键数量大于10个则默认使用手动模式
if (typeof value === 'string' && value.trim()) { if (typeof value === 'string' && value.trim()) {
try { try {
const parsed = JSON.parse(value); const parsed = JSON.parse(value);
const keyCount = Object.keys(parsed).length; const keyCount = Object.keys(parsed).length;
return keyCount > 10 ? 'manual' : 'visual'; return keyCount > 10 ? 'manual' : 'visual';
} catch (error) { } catch (error) {
// JSON无效时默认显示手动编辑模式
return 'manual'; return 'manual';
} }
} }
return 'visual'; return 'visual';
}); });
const [jsonError, setJsonError] = useState(''); 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(() => { useEffect(() => {
try { try {
let parsed = {}; let parsed = {};
@@ -93,16 +131,20 @@ const JSONEditor = ({
} else if (typeof value === 'object' && value !== null) { } else if (typeof value === 'object' && value !== null) {
parsed = value; parsed = value;
} }
setJsonData(parsed);
// 只在外部值真正改变时更新,避免循环更新
const currentObj = keyValueArrayToObject(keyValuePairs);
if (JSON.stringify(parsed) !== JSON.stringify(currentObj)) {
setKeyValuePairs(objectToKeyValueArray(parsed));
}
setJsonError(''); setJsonError('');
} catch (error) { } catch (error) {
console.log('JSON解析失败:', error.message); console.log('JSON解析失败:', error.message);
setJsonError(error.message); setJsonError(error.message);
// JSON格式错误时不更新jsonData
} }
}, [value]); }, [value]);
// 外部 value 变化时,若不在手动模式,则同步手动文本;在手动模式下不打断用户输入 // 外部 value 变化时,若不在手动模式,则同步手动文本
useEffect(() => { useEffect(() => {
if (editMode !== 'manual') { if (editMode !== 'manual') {
if (typeof value === 'string') setManualText(value); if (typeof value === 'string') setManualText(value);
@@ -112,45 +154,47 @@ const JSONEditor = ({
}, [value, editMode]); }, [value, editMode]);
// 处理可视化编辑的数据变化 // 处理可视化编辑的数据变化
const handleVisualChange = useCallback((newData) => { const handleVisualChange = useCallback((newPairs) => {
setJsonData(newData); setKeyValuePairs(newPairs);
const jsonObject = keyValueArrayToObject(newPairs);
const jsonString = Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2);
setJsonError(''); setJsonError('');
const jsonString = Object.keys(newData).length === 0 ? '' : JSON.stringify(newData, null, 2);
// 通过formApi设置值(如果提供的话) // 通过formApi设置值
if (formApi && field) { if (formApi && field) {
formApi.setValue(field, jsonString); formApi.setValue(field, jsonString);
} }
onChange?.(jsonString); onChange?.(jsonString);
}, [onChange, formApi, field]); }, [onChange, formApi, field, keyValueArrayToObject]);
// 处理手动编辑的数据变化(无效 JSON 不阻断输入,也不立刻回传上游) // 处理手动编辑的数据变化
const handleManualChange = useCallback((newValue) => { const handleManualChange = useCallback((newValue) => {
setManualText(newValue); setManualText(newValue);
if (newValue && newValue.trim()) { if (newValue && newValue.trim()) {
try { try {
JSON.parse(newValue); const parsed = JSON.parse(newValue);
setKeyValuePairs(objectToKeyValueArray(parsed));
setJsonError(''); setJsonError('');
onChange?.(newValue); onChange?.(newValue);
} catch (error) { } catch (error) {
setJsonError(error.message); setJsonError(error.message);
// 无效 JSON 时不回传,避免外部值把输入重置
} }
} else { } else {
setKeyValuePairs([]);
setJsonError(''); setJsonError('');
onChange?.(''); onChange?.('');
} }
}, [onChange]); }, [onChange, objectToKeyValueArray]);
// 切换编辑模式 // 切换编辑模式
const toggleEditMode = useCallback(() => { const toggleEditMode = useCallback(() => {
if (editMode === 'visual') { if (editMode === 'visual') {
// 从可视化模式切换到手动模式 const jsonObject = keyValueArrayToObject(keyValuePairs);
setManualText(Object.keys(jsonData).length === 0 ? '' : JSON.stringify(jsonData, null, 2)); setManualText(Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2));
setEditMode('manual'); setEditMode('manual');
} else { } else {
// 从手动模式切换到可视化模式需要验证JSON
try { try {
let parsed = {}; let parsed = {};
if (manualText && manualText.trim()) { if (manualText && manualText.trim()) {
@@ -160,98 +204,166 @@ const JSONEditor = ({
} else if (typeof value === 'object' && value !== null) { } else if (typeof value === 'object' && value !== null) {
parsed = value; parsed = value;
} }
setJsonData(parsed); setKeyValuePairs(objectToKeyValueArray(parsed));
setJsonError(''); setJsonError('');
setEditMode('visual'); setEditMode('visual');
} catch (error) { } catch (error) {
setJsonError(error.message); setJsonError(error.message);
// JSON格式错误时不切换模式
return; return;
} }
} }
}, [editMode, value, manualText, jsonData]); }, [editMode, value, manualText, keyValuePairs, keyValueArrayToObject, objectToKeyValueArray]);
// 添加键值对 // 添加键值对
const addKeyValue = useCallback(() => { const addKeyValue = useCallback(() => {
const newData = { ...jsonData }; const newPairs = [...keyValuePairs];
const keys = Object.keys(newData); const existingKeys = newPairs.map(p => p.key);
let counter = 1; let counter = 1;
let newKey = `field_${counter}`; let newKey = `field_${counter}`;
while (newData.hasOwnProperty(newKey)) { while (existingKeys.includes(newKey)) {
counter += 1; counter += 1;
newKey = `field_${counter}`; newKey = `field_${counter}`;
} }
newData[newKey] = ''; newPairs.push({
handleVisualChange(newData); id: `${Date.now()}_${Math.random()}`,
}, [jsonData, handleVisualChange]); key: newKey,
value: ''
});
handleVisualChange(newPairs);
}, [keyValuePairs, handleVisualChange]);
// 删除键值对 // 删除键值对
const removeKeyValue = useCallback((keyToRemove) => { const removeKeyValue = useCallback((id) => {
const newData = { ...jsonData }; const newPairs = keyValuePairs.filter(pair => pair.id !== id);
delete newData[keyToRemove]; handleVisualChange(newPairs);
handleVisualChange(newData); }, [keyValuePairs, handleVisualChange]);
}, [jsonData, handleVisualChange]);
// 更新键名 // 更新键名
const updateKey = useCallback((oldKey, newKey) => { const updateKey = useCallback((id, newKey) => {
if (oldKey === newKey || !newKey) return; const newPairs = keyValuePairs.map(pair =>
const newData = {}; pair.id === id ? { ...pair, key: newKey } : pair
Object.entries(jsonData).forEach(([k, v]) => { );
if (k === oldKey) { handleVisualChange(newPairs);
newData[newKey] = v; }, [keyValuePairs, handleVisualChange]);
} else {
newData[k] = v;
}
});
handleVisualChange(newData);
}, [jsonData, handleVisualChange]);
// 更新值 // 更新值
const updateValue = useCallback((key, newValue) => { const updateValue = useCallback((id, newValue) => {
const newData = { ...jsonData }; const newPairs = keyValuePairs.map(pair =>
newData[key] = newValue; pair.id === id ? { ...pair, value: newValue } : pair
handleVisualChange(newData); );
}, [jsonData, handleVisualChange]); handleVisualChange(newPairs);
}, [keyValuePairs, handleVisualChange]);
// 填入模板 // 填入模板
const fillTemplate = useCallback(() => { const fillTemplate = useCallback(() => {
if (template) { if (template) {
const templateString = JSON.stringify(template, null, 2); const templateString = JSON.stringify(template, null, 2);
// 通过formApi设置值如果提供的话
if (formApi && field) { if (formApi && field) {
formApi.setValue(field, templateString); formApi.setValue(field, templateString);
} }
// 同步内部与外部值,避免出现杂字符
setManualText(templateString); setManualText(templateString);
setJsonData(template); setKeyValuePairs(objectToKeyValueArray(template));
onChange?.(templateString); onChange?.(templateString);
// 清除错误状态
setJsonError(''); setJsonError('');
} }
}, [template, onChange, editMode, formApi, field]); }, [template, onChange, formApi, field, objectToKeyValueArray]);
// 渲染键值对编辑器 // 渲染值输入控件(支持嵌套)
const renderKeyValueEditor = () => { const renderValueInput = (pairId, value) => {
if (typeof jsonData !== 'object' || jsonData === null) { const valueType = typeof value;
if (valueType === 'boolean') {
return ( return (
<div className="text-center py-6 px-4"> <div className="flex items-center">
<div className="text-gray-400 mb-2"> <Switch
<IconCode size={32} /> checked={value}
</div> onChange={(newValue) => updateValue(pairId, newValue)}
<Text type="tertiary" className="text-gray-500 text-sm"> />
{t('无效的JSON数据请检查格式')} <Text type="tertiary" className="ml-2">
{value ? t('true') : t('false')}
</Text> </Text>
</div> </div>
); );
} }
const entries = Object.entries(jsonData);
if (valueType === 'number') {
return (
<InputNumber
value={value}
onChange={(newValue) => updateValue(pairId, newValue)}
style={{ width: '100%' }}
placeholder={t('输入数字')}
/>
);
}
if (valueType === 'object' && value !== null) {
// 简化嵌套对象的处理使用TextArea
return (
<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 ( return (
<div className="space-y-1"> <div className="space-y-1">
{entries.length === 0 && ( {/* 重复键警告 */}
{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"> <div className="text-center py-6 px-4">
<Text type="tertiary" className="text-gray-500 text-sm"> <Text type="tertiary" className="text-gray-500 text-sm">
{t('暂无数据,点击下方按钮添加键值对')} {t('暂无数据,点击下方按钮添加键值对')}
@@ -259,29 +371,55 @@ const JSONEditor = ({
</div> </div>
)} )}
{entries.map(([key, value], index) => ( {keyValuePairs.map((pair, index) => {
<Row key={index} gutter={8} align="middle"> const isDuplicate = duplicateKeys.has(pair.key);
<Col span={6}> const isLastDuplicate = isDuplicate &&
<Input keyValuePairs.slice(index + 1).every(p => p.key !== pair.key);
placeholder={t('键名')}
value={key} return (
onChange={(newKey) => updateKey(key, newKey)} <Row key={pair.id} gutter={8} align="middle">
/> <Col span={6}>
</Col> <div className="relative">
<Col span={16}> <Input
{renderValueInput(key, value)} placeholder={t('键名')}
</Col> value={pair.key}
<Col span={2}> onChange={(newKey) => updateKey(pair.id, newKey)}
<Button status={isDuplicate ? 'warning' : undefined}
icon={<IconDelete />} />
type="danger" {isDuplicate && (
theme="borderless" <Tooltip
onClick={() => removeKeyValue(key)} content={
style={{ width: '100%' }} isLastDuplicate
/> ? t('这是重复键中的最后一个,其值将被使用')
</Col> : t('重复的键名,此值将被后面的同名键覆盖')
</Row> }
))} >
<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
icon={<IconDelete />}
type="danger"
theme="borderless"
onClick={() => removeKeyValue(pair.id)}
style={{ width: '100%' }}
/>
</Col>
</Row>
);
})}
<div className="mt-2 flex justify-center"> <div className="mt-2 flex justify-center">
<Button <Button
@@ -297,249 +435,96 @@ const JSONEditor = ({
); );
}; };
// 添加嵌套对象 // 渲染区域编辑器(特殊格式)- 也需要改造以支持重复键
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)}
/>
<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)}
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);
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 (
<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>
);
};
// 渲染区域编辑器(特殊格式)
const renderRegionEditor = () => { const renderRegionEditor = () => {
const entries = Object.entries(jsonData); const defaultPair = keyValuePairs.find(pair => pair.key === 'default');
const defaultEntry = entries.find(([key]) => key === 'default'); const modelPairs = keyValuePairs.filter(pair => pair.key !== 'default');
const modelEntries = entries.filter(([key]) => key !== 'default');
return ( return (
<div className="space-y-2"> <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('默认区域')}> <Form.Slot label={t('默认区域')}>
<Input <Input
placeholder={t('默认区域,如: us-central1')} placeholder={t('默认区域,如: us-central1')}
value={defaultEntry ? defaultEntry[1] : ''} value={defaultPair ? defaultPair.value : ''}
onChange={(value) => updateValue('default', value)} onChange={(value) => {
if (defaultPair) {
updateValue(defaultPair.id, value);
} else {
const newPairs = [...keyValuePairs, {
id: `${Date.now()}_${Math.random()}`,
key: 'default',
value: value
}];
handleVisualChange(newPairs);
}
}}
/> />
</Form.Slot> </Form.Slot>
{/* 模型专用区域 */} {/* 模型专用区域 */}
<Form.Slot label={t('模型专用区域')}> <Form.Slot label={t('模型专用区域')}>
<div> <div>
{modelEntries.map(([modelName, region], index) => ( {modelPairs.map((pair) => {
<Row key={index} gutter={8} align="middle" className="mb-2"> const isDuplicate = duplicateKeys.has(pair.key);
<Col span={10}> return (
<Input <Row key={pair.id} gutter={8} align="middle" className="mb-2">
placeholder={t('模型名称')} <Col span={10}>
value={modelName} <div className="relative">
onChange={(newKey) => updateKey(modelName, newKey)} <Input
/> placeholder={t('模型名称')}
</Col> value={pair.key}
<Col span={12}> onChange={(newKey) => updateKey(pair.id, newKey)}
<Input status={isDuplicate ? 'warning' : undefined}
placeholder={t('区域')} />
value={region} {isDuplicate && (
onChange={(newValue) => updateValue(modelName, newValue)} <Tooltip content={t('重复的键名')}>
/> <IconAlertTriangle
</Col> className="absolute right-2 top-1/2 transform -translate-y-1/2"
<Col span={2}> style={{ color: '#faad14', fontSize: '14px' }}
<Button />
icon={<IconDelete />} </Tooltip>
type="danger" )}
theme="borderless" </div>
onClick={() => removeKeyValue(modelName)} </Col>
style={{ width: '100%' }} <Col span={12}>
/> <Input
</Col> placeholder={t('区域')}
</Row> 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"> <div className="mt-2 flex justify-center">
<Button <Button
@@ -666,4 +651,4 @@ const JSONEditor = ({
); );
}; };
export default JSONEditor; export default JSONEditor;