/* 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 { Button, Form, Typography, Banner, Tabs, TabPane, Card, Input, InputNumber, Switch, TextArea, Row, Col, Divider, Tooltip, } from '@douyinfe/semi-ui'; import { IconPlus, IconDelete, IconAlertTriangle } from '@douyinfe/semi-icons'; const { Text } = Typography; // 唯一 ID 生成器,确保在组件生命周期内稳定且递增 const generateUniqueId = (() => { let counter = 0; return () => `kv_${counter++}`; })(); const JSONEditor = ({ value = '', onChange, field, label, placeholder, extraText, extraFooter, showClear = true, template, templateLabel, editorType = 'keyValue', rules = [], formApi = null, ...props }) => { const { t } = useTranslation(); // 将对象转换为键值对数组(包含唯一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 objectToKeyValueArray(parsed); } catch (error) { return []; } } if (typeof value === 'object' && value !== null) { return objectToKeyValueArray(value); } return []; }); // 手动模式下的本地文本缓冲 const [manualText, setManualText] = useState(() => { if (typeof value === 'string') return value; if (value && typeof value === 'object') return JSON.stringify(value, null, 2); return ''; }); // 根据键数量决定默认编辑模式 const [editMode, setEditMode] = useState(() => { 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) { return 'manual'; } } 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; if (keyCount[pair.key] > 1) { duplicates.add(pair.key); } } }); return duplicates; }, [keyValuePairs]); // 数据同步 - 当value变化时更新键值对数组 useEffect(() => { try { let parsed = {}; if (typeof value === 'string' && value.trim()) { parsed = JSON.parse(value); } else if (typeof value === 'object' && value !== null) { parsed = value; } // 只在外部值真正改变时更新,避免循环更新 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); } }, [value]); // 外部 value 变化时,若不在手动模式,则同步手动文本 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]); // 处理可视化编辑的数据变化 const handleVisualChange = useCallback( (newPairs) => { setKeyValuePairs(newPairs); const jsonObject = keyValueArrayToObject(newPairs); const jsonString = Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2); setJsonError(''); // 通过formApi设置值 if (formApi && field) { formApi.setValue(field, jsonString); } onChange?.(jsonString); }, [onChange, formApi, field, keyValueArrayToObject], ); // 处理手动编辑的数据变化 const handleManualChange = useCallback( (newValue) => { setManualText(newValue); if (newValue && newValue.trim()) { try { const parsed = JSON.parse(newValue); setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs)); setJsonError(''); onChange?.(newValue); } catch (error) { setJsonError(error.message); } } else { setKeyValuePairs([]); setJsonError(''); onChange?.(''); } }, [onChange, objectToKeyValueArray, keyValuePairs], ); // 切换编辑模式 const toggleEditMode = useCallback(() => { if (editMode === 'visual') { const jsonObject = keyValueArrayToObject(keyValuePairs); setManualText( Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2), ); setEditMode('manual'); } else { try { let parsed = {}; if (manualText && manualText.trim()) { parsed = JSON.parse(manualText); } else if (typeof value === 'string' && value.trim()) { parsed = JSON.parse(value); } else if (typeof value === 'object' && value !== null) { parsed = value; } setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs)); setJsonError(''); setEditMode('visual'); } catch (error) { setJsonError(error.message); return; } } }, [ editMode, value, manualText, keyValuePairs, keyValueArrayToObject, objectToKeyValueArray, ]); // 添加键值对 const addKeyValue = useCallback(() => { const newPairs = [...keyValuePairs]; const existingKeys = newPairs.map((p) => p.key); let counter = 1; let newKey = `field_${counter}`; while (existingKeys.includes(newKey)) { counter += 1; newKey = `field_${counter}`; } newPairs.push({ id: generateUniqueId(), key: newKey, value: '', }); handleVisualChange(newPairs); }, [keyValuePairs, handleVisualChange]); // 删除键值对 const removeKeyValue = useCallback( (id) => { const newPairs = keyValuePairs.filter((pair) => pair.id !== id); handleVisualChange(newPairs); }, [keyValuePairs, 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( (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); if (formApi && field) { formApi.setValue(field, templateString); } setManualText(templateString); setKeyValuePairs(objectToKeyValueArray(template, keyValuePairs)); onChange?.(templateString); setJsonError(''); } }, [ template, onChange, formApi, field, objectToKeyValueArray, keyValuePairs, ]); // 渲染值输入控件(支持嵌套) const renderValueInput = (pairId, value) => { const valueType = typeof value; if (valueType === 'boolean') { return (
updateValue(pairId, newValue)} /> {value ? t('true') : t('false')}
); } if (valueType === 'number') { return ( updateValue(pairId, newValue)} style={{ width: '100%' }} placeholder={t('输入数字')} /> ); } if (valueType === 'object' && value !== null) { // 简化嵌套对象的处理,使用TextArea return (