Explorar o código

✨ style(JSONEditor): add AGPL-3.0 license header, clean imports & refine Banner UI

* Added full GNU Affero General Public License v3 header at the top of `JSONEditor.js`.
* Removed unused `IconCode` and `IconRefresh` imports to eliminate dead code.
* Set `closeIcon={null}` and applied `!rounded-md` class for `Banner`, improving visual consistency and preventing unintended dismissal.
* Normalized whitespace and line-breaks for better readability and lint compliance.
t0ng7u hai 7 meses
pai
achega
a5cbef1a61
Modificáronse 1 ficheiros con 57 adicións e 27 borrados
  1. 57 27
      web/src/components/common/ui/JSONEditor.js

+ 57 - 27
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 <https://www.gnu.org/licenses/>.
+
+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 (
             <Row key={pair.id} gutter={8} align="middle">
               <Col span={6}>
@@ -389,14 +419,14 @@ const JSONEditor = ({
                   {isDuplicate && (
                     <Tooltip
                       content={
-                        isLastDuplicate 
+                        isLastDuplicate
                           ? t('这是重复键中的最后一个,其值将被使用')
                           : t('重复的键名,此值将被后面的同名键覆盖')
                       }
                     >
                       <IconAlertTriangle
                         className="absolute right-2 top-1/2 transform -translate-y-1/2"
-                        style={{ 
+                        style={{
                           color: isLastDuplicate ? '#ff7d00' : '#faad14',
                           fontSize: '14px'
                         }}
@@ -471,7 +501,7 @@ const JSONEditor = ({
                 updateValue(defaultPair.id, value);
               } else {
                 const newPairs = [...keyValuePairs, {
-                  id: `${Date.now()}_${Math.random()}`,
+                  id: generateUniqueId(),
                   key: 'default',
                   value: value
                 }];