فهرست منبع

feat: 添加手动输入和文件上传模式切换功能,支持密钥搜索和高亮显示

CaIon 7 ماه پیش
والد
کامیت
a5da09dfb9
1فایلهای تغییر یافته به همراه227 افزوده شده و 67 حذف شده
  1. 227 67
      web/src/pages/Channel/EditChannel.js

+ 227 - 67
web/src/pages/Channel/EditChannel.js

@@ -26,6 +26,7 @@ import {
   Form,
   Row,
   Col,
+  Highlight,
 } from '@douyinfe/semi-ui';
 import { getChannelModels, copy, getChannelIcon, getModelCategories } from '../../helpers';
 import {
@@ -122,6 +123,8 @@ const EditChannel = (props) => {
   const [vertexFileList, setVertexFileList] = useState([]);
   const vertexErroredNames = useRef(new Set()); // 避免重复报错
   const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false);
+  const [channelSearchValue, setChannelSearchValue] = useState('');
+  const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式
   const getInitValues = () => ({ ...originInputs });
   const handleInputChange = (name, value) => {
     if (formApiRef.current) {
@@ -190,6 +193,9 @@ const EditChannel = (props) => {
         setInputs((inputs) => ({ ...inputs, models: localModels }));
       }
       setBasicModels(localModels);
+      
+      // 重置手动输入模式状态
+      setUseManualInput(false);
     }
     //setAutoBan
   };
@@ -418,6 +424,8 @@ const EditChannel = (props) => {
       } else {
         formApiRef.current?.setValues(getInitValues());
       }
+      // 重置手动输入模式状态
+      setUseManualInput(false);
     } else {
       formApiRef.current?.reset();
     }
@@ -468,41 +476,60 @@ const EditChannel = (props) => {
     let localInputs = { ...formValues };
 
     if (localInputs.type === 41) {
-      let keys = vertexKeys;
-
-      // 若当前未选择文件,尝试从已上传文件列表解析(异步读取)
-      if (keys.length === 0 && vertexFileList.length > 0) {
-        try {
-          const parsed = await Promise.all(
-            vertexFileList.map(async (item) => {
-              const fileObj = item.fileInstance;
-              if (!fileObj) return null;
-              const txt = await fileObj.text();
-              return JSON.parse(txt);
-            })
-          );
-          keys = parsed.filter(Boolean);
-        } catch (err) {
-          showError(t('解析密钥文件失败: {{msg}}', { msg: err.message }));
-          return;
-        }
-      }
-
-      // 创建模式必须上传密钥;编辑模式可选
-      if (keys.length === 0) {
-        if (!isEdit) {
-          showInfo(t('请上传密钥文件!'));
+      if (useManualInput) {
+        // 手动输入模式
+        if (localInputs.key && localInputs.key.trim() !== '') {
+          try {
+            // 验证 JSON 格式
+            const parsedKey = JSON.parse(localInputs.key);
+            // 确保是有效的密钥格式
+            localInputs.key = JSON.stringify(parsedKey);
+          } catch (err) {
+            showError(t('密钥格式无效,请输入有效的 JSON 格式密钥'));
+            return;
+          }
+        } else if (!isEdit) {
+          showInfo(t('请输入密钥!'));
           return;
-        } else {
-          // 编辑模式且未上传新密钥,不修改 key
-          delete localInputs.key;
         }
       } else {
-        // 有新密钥,则覆盖
-        if (batch) {
-          localInputs.key = JSON.stringify(keys);
+        // 文件上传模式
+        let keys = vertexKeys;
+
+        // 若当前未选择文件,尝试从已上传文件列表解析(异步读取)
+        if (keys.length === 0 && vertexFileList.length > 0) {
+          try {
+            const parsed = await Promise.all(
+              vertexFileList.map(async (item) => {
+                const fileObj = item.fileInstance;
+                if (!fileObj) return null;
+                const txt = await fileObj.text();
+                return JSON.parse(txt);
+              })
+            );
+            keys = parsed.filter(Boolean);
+          } catch (err) {
+            showError(t('解析密钥文件失败: {{msg}}', { msg: err.message }));
+            return;
+          }
+        }
+
+        // 创建模式必须上传密钥;编辑模式可选
+        if (keys.length === 0) {
+          if (!isEdit) {
+            showInfo(t('请上传密钥文件!'));
+            return;
+          } else {
+            // 编辑模式且未上传新密钥,不修改 key
+            delete localInputs.key;
+          }
         } else {
-          localInputs.key = JSON.stringify(keys[0]);
+          // 有新密钥,则覆盖
+          if (batch) {
+            localInputs.key = JSON.stringify(keys);
+          } else {
+            localInputs.key = JSON.stringify(keys[0]);
+          }
         }
       }
     }
@@ -646,23 +673,33 @@ const EditChannel = (props) => {
           if (!checked) {
             setMultiToSingle(false);
             setMultiKeyMode('random');
+          } else {
+            // 批量模式下禁用手动输入,并清空手动输入的内容
+            setUseManualInput(false);
+            if (inputs.type === 41) {
+              // 清空手动输入的密钥内容
+              if (formApiRef.current) {
+                formApiRef.current.setValue('key', '');
+              }
+              handleInputChange('key', '');
+            }
           }
         }}
       >{t('批量创建')}</Checkbox>
-      {batch && (
-        <Checkbox disabled={isEdit} checked={multiToSingle} onChange={() => {
-          setMultiToSingle(prev => !prev);
-          setInputs(prev => {
-            const newInputs = { ...prev };
-            if (!multiToSingle) {
-              newInputs.multi_key_mode = multiKeyMode;
-            } else {
-              delete newInputs.multi_key_mode;
-            }
-            return newInputs;
-          });
-        }}>{t('密钥聚合模式')}</Checkbox>
-      )}
+      {/*{batch && (*/}
+      {/*  <Checkbox disabled={isEdit} checked={multiToSingle} onChange={() => {*/}
+      {/*    setMultiToSingle(prev => !prev);*/}
+      {/*    setInputs(prev => {*/}
+      {/*      const newInputs = { ...prev };*/}
+      {/*      if (!multiToSingle) {*/}
+      {/*        newInputs.multi_key_mode = multiKeyMode;*/}
+      {/*      } else {*/}
+      {/*        delete newInputs.multi_key_mode;*/}
+      {/*      }*/}
+      {/*      return newInputs;*/}
+      {/*    });*/}
+      {/*  }}>{t('密钥聚合模式')}</Checkbox>*/}
+      {/*)}*/}
     </Space>
   ) : null;
 
@@ -670,16 +707,68 @@ const EditChannel = (props) => {
     () =>
       CHANNEL_OPTIONS.map((opt) => ({
         ...opt,
-        label: (
-          <span className="flex items-center gap-2">
-            {getChannelIcon(opt.value)}
-            {opt.label}
-          </span>
-        ),
+        // 保持 label 为纯文本以支持搜索
+        label: opt.label,
       })),
     [],
   );
 
+  const renderChannelOption = (renderProps) => {
+    const {
+      disabled,
+      selected,
+      label,
+      value,
+      focused,
+      className,
+      style,
+      onMouseEnter,
+      onClick,
+      ...rest
+    } = renderProps;
+    
+    const searchWords = channelSearchValue ? [channelSearchValue] : [];
+    
+    // 构建样式类名
+    const optionClassName = [
+      'flex items-center gap-3 px-3 py-2 transition-all duration-200 rounded-lg mx-2 my-1',
+      focused && 'bg-blue-50 shadow-sm',
+      selected && 'bg-blue-100 text-blue-700 shadow-lg ring-2 ring-blue-200 ring-opacity-50',
+      disabled && 'opacity-50 cursor-not-allowed',
+      !disabled && 'hover:bg-gray-50 hover:shadow-md cursor-pointer',
+      className
+    ].filter(Boolean).join(' ');
+    
+    return (
+      <div 
+        style={style} 
+        className={optionClassName}
+        onClick={() => !disabled && onClick()} 
+        onMouseEnter={e => onMouseEnter()}
+      >
+        <div className="flex items-center gap-3 w-full">
+          <div className="flex-shrink-0 w-5 h-5 flex items-center justify-center">
+            {getChannelIcon(value)}
+          </div>
+          <div className="flex-1 min-w-0">
+            <Highlight 
+              sourceString={label} 
+              searchWords={searchWords}
+              className="text-sm font-medium truncate"
+            />
+          </div>
+          {selected && (
+            <div className="flex-shrink-0 text-blue-600">
+              <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
+                <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
+              </svg>
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  };
+
   return (
     <>
       <SideSheet
@@ -749,6 +838,8 @@ const EditChannel = (props) => {
                     style={{ width: '100%' }}
                     filter
                     searchPosition='dropdown'
+                    onSearch={(value) => setChannelSearchValue(value)}
+                    renderOptionItem={renderChannelOption}
                     onChange={(value) => handleInputChange('type', value)}
                   />
 
@@ -797,22 +888,91 @@ const EditChannel = (props) => {
                   ) : (
                     <>
                       {inputs.type === 41 ? (
-                        <Form.Upload
-                          field='vertex_files'
-                          label={t('密钥文件 (.json)')}
-                          accept='.json'
-                          draggable
-                          dragIcon={<IconBolt />}
-                          dragMainText={t('点击上传文件或拖拽文件到这里')}
-                          dragSubText={t('仅支持 JSON 文件')}
-                          style={{ marginTop: 10 }}
-                          uploadTrigger='custom'
-                          beforeUpload={() => false}
-                          onChange={handleVertexUploadChange}
-                          fileList={vertexFileList}
-                          rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]}
-                          extraText={batchExtra}
-                        />
+                        <>
+                          {!batch && (
+                            <div className="flex items-center justify-between mb-3">
+                              <Text className="text-sm font-medium">{t('密钥输入方式')}</Text>
+                              <Space>
+                                <Button
+                                  size="small"
+                                  type={!useManualInput ? 'primary' : 'tertiary'}
+                                  onClick={() => {
+                                    setUseManualInput(false);
+                                    // 切换到文件上传模式时清空手动输入的密钥
+                                    if (formApiRef.current) {
+                                      formApiRef.current.setValue('key', '');
+                                    }
+                                    handleInputChange('key', '');
+                                  }}
+                                >
+                                  {t('文件上传')}
+                                </Button>
+                                <Button
+                                  size="small"
+                                  type={useManualInput ? 'primary' : 'tertiary'}
+                                  onClick={() => {
+                                    setUseManualInput(true);
+                                    // 切换到手动输入模式时清空文件上传相关状态
+                                    setVertexKeys([]);
+                                    setVertexFileList([]);
+                                    if (formApiRef.current) {
+                                      formApiRef.current.setValue('vertex_files', []);
+                                    }
+                                    setInputs((prev) => ({ ...prev, vertex_files: [] }));
+                                  }}
+                                >
+                                  {t('手动输入')}
+                                </Button>
+                              </Space>
+                            </div>
+                          )}
+                          
+                          {batch && (
+                            <Banner
+                              type='info'
+                              description={t('批量创建模式下仅支持文件上传,不支持手动输入')}
+                              className='!rounded-lg mb-3'
+                            />
+                          )}
+                          
+                          {useManualInput && !batch ? (
+                            <Form.TextArea
+                              field='key'
+                              label={isEdit ? t('密钥(编辑模式下,保存的密钥不会显示)') : t('密钥')}
+                              placeholder={t('请输入 JSON 格式的密钥内容,例如:\n{\n  "type": "service_account",\n  "project_id": "your-project-id",\n  "private_key_id": "...",\n  "private_key": "...",\n  "client_email": "...",\n  "client_id": "...",\n  "auth_uri": "...",\n  "token_uri": "...",\n  "auth_provider_x509_cert_url": "...",\n  "client_x509_cert_url": "..."\n}')}
+                              rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
+                              autoComplete='new-password'
+                              onChange={(value) => handleInputChange('key', value)}
+                              extraText={
+                                <div className="flex items-center gap-2">
+                                  <Text type="tertiary" size="small">
+                                    {t('请输入完整的 JSON 格式密钥内容')}
+                                  </Text>
+                                  {batchExtra}
+                                </div>
+                              }
+                              autosize
+                              showClear
+                            />
+                          ) : (
+                            <Form.Upload
+                              field='vertex_files'
+                              label={t('密钥文件 (.json)')}
+                              accept='.json'
+                              draggable
+                              dragIcon={<IconBolt />}
+                              dragMainText={t('点击上传文件或拖拽文件到这里')}
+                              dragSubText={t('仅支持 JSON 文件')}
+                              style={{ marginTop: 10 }}
+                              uploadTrigger='custom'
+                              beforeUpload={() => false}
+                              onChange={handleVertexUploadChange}
+                              fileList={vertexFileList}
+                              rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]}
+                              extraText={batchExtra}
+                            />
+                          )}
+                        </>
                       ) : (
                         <Form.Input
                           field='key'