From 2471367c9205720cc985aa1578ce63a290508a74 Mon Sep 17 00:00:00 2001 From: HynoR <20227709+HynoR@users.noreply.github.com> Date: Fri, 8 Aug 2025 19:00:02 +0800 Subject: [PATCH] feat: optimized Json Visual Editor(JSONEditor) when detected duplicate key --- web/src/components/common/ui/JSONEditor.js | 641 ++++++++++----------- 1 file changed, 313 insertions(+), 328 deletions(-) diff --git a/web/src/components/common/ui/JSONEditor.js b/web/src/components/common/ui/JSONEditor.js index f4f5eee9..5d4d4d32 100644 --- a/web/src/components/common/ui/JSONEditor.js +++ b/web/src/components/common/ui/JSONEditor.js @@ -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 { Button, @@ -15,12 +15,14 @@ import { Row, Col, Divider, + Tooltip, } from '@douyinfe/semi-ui'; import { IconCode, IconPlus, IconDelete, IconRefresh, + IconAlertTriangle, } from '@douyinfe/semi-icons'; const { Text } = Typography; @@ -43,24 +45,44 @@ const JSONEditor = ({ }) => { const { t } = useTranslation(); - // 初始化JSON数据 - const [jsonData, setJsonData] = useState(() => { - // 初始化时解析JSON数据 + // 将对象转换为键值对数组(包含唯一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(() => { 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 +91,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 +131,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)); + } 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 +154,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)); setJsonError(''); onChange?.(newValue); } catch (error) { setJsonError(error.message); - // 无效 JSON 时不回传,避免外部值把输入重置 } } else { + setKeyValuePairs([]); setJsonError(''); onChange?.(''); } - }, [onChange]); + }, [onChange, objectToKeyValueArray]); // 切换编辑模式 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,98 +204,166 @@ const JSONEditor = ({ } else if (typeof value === 'object' && value !== null) { parsed = value; } - setJsonData(parsed); + setKeyValuePairs(objectToKeyValueArray(parsed)); 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: `${Date.now()}_${Math.random()}`, + 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)); onChange?.(templateString); - - // 清除错误状态 setJsonError(''); } - }, [template, onChange, editMode, formApi, field]); + }, [template, onChange, formApi, field, objectToKeyValueArray]); - // 渲染键值对编辑器 - const renderKeyValueEditor = () => { - if (typeof jsonData !== 'object' || jsonData === null) { + // 渲染值输入控件(支持嵌套) + const renderValueInput = (pairId, value) => { + const valueType = typeof value; + + if (valueType === 'boolean') { return ( -