Jelajahi Sumber

feat: optimized JSONEditor in duplicate key handling

Merge pull request #1534 from HynoR/feat/je
同語 7 bulan lalu
induk
melakukan
ae22ba593a
1 mengubah file dengan 298 tambahan dan 313 penghapusan
  1. 298 313
      web/src/components/common/ui/JSONEditor.js

+ 298 - 313
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,177 +204,74 @@ 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]);
-
-  // 渲染键值对编辑器
-  const renderKeyValueEditor = () => {
-    if (typeof jsonData !== 'object' || jsonData === null) {
-      return (
-        <div className="text-center py-6 px-4">
-          <div className="text-gray-400 mb-2">
-            <IconCode size={32} />
-          </div>
-          <Text type="tertiary" className="text-gray-500 text-sm">
-            {t('无效的JSON数据,请检查格式')}
-          </Text>
-        </div>
-      );
-    }
-    const entries = Object.entries(jsonData);
-
-    return (
-      <div className="space-y-1">
-        {entries.length === 0 && (
-          <div className="text-center py-6 px-4">
-            <Text type="tertiary" className="text-gray-500 text-sm">
-              {t('暂无数据,点击下方按钮添加键值对')}
-            </Text>
-          </div>
-        )}
+  }, [template, onChange, formApi, field, objectToKeyValueArray]);
 
-        {entries.map(([key, value], index) => (
-          <Row key={index} gutter={8} align="middle">
-            <Col span={6}>
-              <Input
-                placeholder={t('键名')}
-                value={key}
-                onChange={(newKey) => updateKey(key, newKey)}
-              />
-            </Col>
-            <Col span={16}>
-              {renderValueInput(key, value)}
-            </Col>
-            <Col span={2}>
-              <Button
-                icon={<IconDelete />}
-                type="danger"
-                theme="borderless"
-                onClick={() => removeKeyValue(key)}
-                style={{ width: '100%' }}
-              />
-            </Col>
-          </Row>
-        ))}
-
-        <div className="mt-2 flex justify-center">
-          <Button
-            icon={<IconPlus />}
-            type="primary"
-            theme="outline"
-            onClick={addKeyValue}
-          >
-            {t('添加键值对')}
-          </Button>
-        </div>
-      </div>
-    );
-  };
-
-  // 添加嵌套对象
-  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 renderValueInput = (pairId, value) => {
     const valueType = typeof value;
 
     if (valueType === 'boolean') {
@@ -338,7 +279,7 @@ const JSONEditor = ({
         <div className="flex items-center">
           <Switch
             checked={value}
-            onChange={(newValue) => updateValue(key, newValue)}
+            onChange={(newValue) => updateValue(pairId, newValue)}
           />
           <Text type="tertiary" className="ml-2">
             {value ? t('true') : t('false')}
@@ -351,195 +292,239 @@ const JSONEditor = ({
       return (
         <InputNumber
           value={value}
-          onChange={(newValue) => updateValue(key, newValue)}
+          onChange={(newValue) => updateValue(pairId, 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);
+      // 简化嵌套对象的处理,使用TextArea
       return (
-        <Card className="!rounded-2xl">
-          {entries.length === 0 && (
-            <Text type="tertiary" className="text-gray-500 text-xs">
-              {t('空对象,点击下方加号添加字段')}
+        <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 (
+      <div className="space-y-1">
+        {/* 重复键警告 */}
+        {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">
+            <Text type="tertiary" className="text-gray-500 text-sm">
+              {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
-                      }
-                    }}
-                  />
-                ) : (
+          </div>
+        )}
+
+        {keyValuePairs.map((pair, index) => {
+          const isDuplicate = duplicateKeys.has(pair.key);
+          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}>
+                <div className="relative">
                   <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);
-                    }}
+                    placeholder={t('键名')}
+                    value={pair.key}
+                    onChange={(newKey) => updateKey(pair.id, newKey)}
+                    status={isDuplicate ? 'warning' : undefined}
                   />
-                )}
+                  {isDuplicate && (
+                    <Tooltip
+                      content={
+                        isLastDuplicate 
+                          ? t('这是重复键中的最后一个,其值将被使用')
+                          : t('重复的键名,此值将被后面的同名键覆盖')
+                      }
+                    >
+                      <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
-                  size="small"
                   icon={<IconDelete />}
                   type="danger"
                   theme="borderless"
-                  onClick={() => {
-                    const newData = { ...jsonData };
-                    delete newData[key][nestedKey];
-                    handleVisualChange(newData);
-                  }}
+                  onClick={() => removeKeyValue(pair.id)}
                   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 className="mt-2 flex justify-center">
+          <Button
+            icon={<IconPlus />}
+            type="primary"
+            theme="outline"
+            onClick={addKeyValue}
+          >
+            {t('添加键值对')}
+          </Button>
+        </div>
       </div>
     );
   };
 
-  // 渲染区域编辑器(特殊格式)
+  // 渲染区域编辑器(特殊格式)- 也需要改造以支持重复键
   const renderRegionEditor = () => {
-    const entries = Object.entries(jsonData);
-    const defaultEntry = entries.find(([key]) => key === 'default');
-    const modelEntries = entries.filter(([key]) => key !== 'default');
+    const defaultPair = keyValuePairs.find(pair => pair.key === 'default');
+    const modelPairs = keyValuePairs.filter(pair => pair.key !== 'default');
 
     return (
       <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('默认区域')}>
           <Input
             placeholder={t('默认区域,如: us-central1')}
-            value={defaultEntry ? defaultEntry[1] : ''}
-            onChange={(value) => updateValue('default', value)}
+            value={defaultPair ? defaultPair.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 label={t('模型专用区域')}>
           <div>
-            {modelEntries.map(([modelName, region], index) => (
-              <Row key={index} gutter={8} align="middle" className="mb-2">
-                <Col span={10}>
-                  <Input
-                    placeholder={t('模型名称')}
-                    value={modelName}
-                    onChange={(newKey) => updateKey(modelName, newKey)}
-                  />
-                </Col>
-                <Col span={12}>
-                  <Input
-                    placeholder={t('区域')}
-                    value={region}
-                    onChange={(newValue) => updateValue(modelName, newValue)}
-                  />
-                </Col>
-                <Col span={2}>
-                  <Button
-                    icon={<IconDelete />}
-                    type="danger"
-                    theme="borderless"
-                    onClick={() => removeKeyValue(modelName)}
-                    style={{ width: '100%' }}
-                  />
-                </Col>
-              </Row>
-            ))}
+            {modelPairs.map((pair) => {
+              const isDuplicate = duplicateKeys.has(pair.key);
+              return (
+                <Row key={pair.id} gutter={8} align="middle" className="mb-2">
+                  <Col span={10}>
+                    <div className="relative">
+                      <Input
+                        placeholder={t('模型名称')}
+                        value={pair.key}
+                        onChange={(newKey) => updateKey(pair.id, newKey)}
+                        status={isDuplicate ? 'warning' : undefined}
+                      />
+                      {isDuplicate && (
+                        <Tooltip content={t('重复的键名')}>
+                          <IconAlertTriangle
+                            className="absolute right-2 top-1/2 transform -translate-y-1/2"
+                            style={{ color: '#faad14', fontSize: '14px' }}
+                          />
+                        </Tooltip>
+                      )}
+                    </div>
+                  </Col>
+                  <Col span={12}>
+                    <Input
+                      placeholder={t('区域')}
+                      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">
               <Button
@@ -666,4 +651,4 @@ const JSONEditor = ({
   );
 };
 
-export default JSONEditor; 
+export default JSONEditor;