diff --git a/web/src/components/common/ui/JSONEditor.js b/web/src/components/common/ui/JSONEditor.js index 5d4d4d32..4acbe270 100644 --- a/web/src/components/common/ui/JSONEditor.js +++ b/web/src/components/common/ui/JSONEditor.js @@ -1,3 +1,22 @@ +/* +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 . + +For commercial licensing, please contact support@quantumnous.com +*/ + import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -18,15 +37,19 @@ import { 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, @@ -46,13 +69,20 @@ const JSONEditor = ({ const { t } = useTranslation(); // 将对象转换为键值对数组(包含唯一ID) - const objectToKeyValueArray = useCallback((obj) => { + const objectToKeyValueArray = useCallback((obj, prevPairs = []) => { if (!obj || typeof obj !== 'object') return []; - return Object.entries(obj).map(([key, value], index) => ({ - id: `${Date.now()}_${index}_${Math.random()}`, // 唯一ID - key, - value - })); + + 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, + }; + }); }, []); // 将键值对数组转换为对象(重复键时后面的会覆盖前面的) @@ -102,14 +132,14 @@ const JSONEditor = ({ } return 'visual'; }); - + const [jsonError, setJsonError] = useState(''); // 计算重复的键 const duplicateKeys = useMemo(() => { const keyCount = {}; const duplicates = new Set(); - + keyValuePairs.forEach(pair => { if (pair.key) { keyCount[pair.key] = (keyCount[pair.key] || 0) + 1; @@ -118,7 +148,7 @@ const JSONEditor = ({ } } }); - + return duplicates; }, [keyValuePairs]); @@ -131,11 +161,11 @@ const JSONEditor = ({ } else if (typeof value === 'object' && value !== null) { parsed = value; } - + // 只在外部值真正改变时更新,避免循环更新 const currentObj = keyValueArrayToObject(keyValuePairs); if (JSON.stringify(parsed) !== JSON.stringify(currentObj)) { - setKeyValuePairs(objectToKeyValueArray(parsed)); + setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs)); } setJsonError(''); } catch (error) { @@ -158,7 +188,7 @@ const JSONEditor = ({ setKeyValuePairs(newPairs); const jsonObject = keyValueArrayToObject(newPairs); const jsonString = Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2); - + setJsonError(''); // 通过formApi设置值 @@ -175,7 +205,7 @@ const JSONEditor = ({ if (newValue && newValue.trim()) { try { const parsed = JSON.parse(newValue); - setKeyValuePairs(objectToKeyValueArray(parsed)); + setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs)); setJsonError(''); onChange?.(newValue); } catch (error) { @@ -186,7 +216,7 @@ const JSONEditor = ({ setJsonError(''); onChange?.(''); } - }, [onChange, objectToKeyValueArray]); + }, [onChange, objectToKeyValueArray, keyValuePairs]); // 切换编辑模式 const toggleEditMode = useCallback(() => { @@ -204,7 +234,7 @@ const JSONEditor = ({ } else if (typeof value === 'object' && value !== null) { parsed = value; } - setKeyValuePairs(objectToKeyValueArray(parsed)); + setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs)); setJsonError(''); setEditMode('visual'); } catch (error) { @@ -225,7 +255,7 @@ const JSONEditor = ({ newKey = `field_${counter}`; } newPairs.push({ - id: `${Date.now()}_${Math.random()}`, + id: generateUniqueId(), key: newKey, value: '' }); @@ -240,7 +270,7 @@ const JSONEditor = ({ // 更新键名 const updateKey = useCallback((id, newKey) => { - const newPairs = keyValuePairs.map(pair => + const newPairs = keyValuePairs.map(pair => pair.id === id ? { ...pair, key: newKey } : pair ); handleVisualChange(newPairs); @@ -264,11 +294,11 @@ const JSONEditor = ({ } setManualText(templateString); - setKeyValuePairs(objectToKeyValueArray(template)); + setKeyValuePairs(objectToKeyValueArray(template, keyValuePairs)); onChange?.(templateString); setJsonError(''); } - }, [template, onChange, formApi, field, objectToKeyValueArray]); + }, [template, onChange, formApi, field, objectToKeyValueArray, keyValuePairs]); // 渲染值输入控件(支持嵌套) const renderValueInput = (pairId, value) => { @@ -327,7 +357,7 @@ const JSONEditor = ({ let convertedValue = newValue; if (newValue === 'true') convertedValue = true; else if (newValue === 'false') convertedValue = false; - else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') { + else if (!isNaN(newValue) && newValue !== '') { const num = Number(newValue); // 检查是否为整数 if (Number.isInteger(num)) { @@ -373,9 +403,9 @@ const JSONEditor = ({ {keyValuePairs.map((pair, index) => { const isDuplicate = duplicateKeys.has(pair.key); - const isLastDuplicate = isDuplicate && + const isLastDuplicate = isDuplicate && keyValuePairs.slice(index + 1).every(p => p.key !== pair.key); - + return ( @@ -389,14 +419,14 @@ const JSONEditor = ({ {isDuplicate && (