Просмотр исходного кода

✨ feat(models): Revamp EditModelModal UI and UX

This commit significantly refactors the `EditModelModal` component to streamline the user interface and enhance usability, aligning it with the interaction patterns found elsewhere in the application.

- **Consolidated Layout:** Merged the "Vendor Information" and "Feature Configuration" sections into a single "Basic Information" card. This simplifies the form, reduces clutter, and makes all settings accessible in one view.

- **Improved Prefill Groups:** Replaced the separate `Select` dropdowns for tag and endpoint groups with a more intuitive button-based system within the `extraText` of the `TagInput` components.

- **Additive Button Logic:** The prefill group buttons now operate in an additive mode. Users can click multiple group buttons to incrementally add tags or endpoints, with duplicates being automatically handled.

- **Clear Functionality:** Added "Clear" buttons for both tags and endpoints, allowing users to easily reset the fields.

- **Code Cleanup:** Removed the unused `endpointOptions` constant and unnecessary icon imports (`Building`, `Settings`) to keep the codebase clean.
t0ng7u 7 месяцев назад
Родитель
Сommit
94506bee99

+ 20 - 20
web/src/components/table/channels/modals/EditChannelModal.jsx

@@ -1174,27 +1174,27 @@ const EditChannelModal = (props) => {
                     </>
                   )}
 
-                {isEdit && isMultiKeyChannel && (
-                        <Form.Select
-                          field='key_mode'
-                          label={t('密钥更新模式')}
-                          placeholder={t('请选择密钥更新模式')}
-                          optionList={[
-                            { label: t('追加到现有密钥'), value: 'append' },
-                            { label: t('覆盖现有密钥'), value: 'replace' },
-                          ]}
-                          style={{ width: '100%' }}
-                          value={keyMode}
-                          onChange={(value) => setKeyMode(value)}
-                          extraText={
-                            <Text type="tertiary" size="small">
-                              {keyMode === 'replace' 
-                                ? t('覆盖模式:将完全替换现有的所有密钥') 
-                                : t('追加模式:将新密钥添加到现有密钥列表末尾')
-                              }
-                            </Text>
+                  {isEdit && isMultiKeyChannel && (
+                    <Form.Select
+                      field='key_mode'
+                      label={t('密钥更新模式')}
+                      placeholder={t('请选择密钥更新模式')}
+                      optionList={[
+                        { label: t('追加到现有密钥'), value: 'append' },
+                        { label: t('覆盖现有密钥'), value: 'replace' },
+                      ]}
+                      style={{ width: '100%' }}
+                      value={keyMode}
+                      onChange={(value) => setKeyMode(value)}
+                      extraText={
+                        <Text type="tertiary" size="small">
+                          {keyMode === 'replace'
+                            ? t('覆盖模式:将完全替换现有的所有密钥')
+                            : t('追加模式:将新密钥添加到现有密钥列表末尾')
                           }
-                        />
+                        </Text>
+                      }
+                    />
                   )}
                   {batch && multiToSingle && (
                     <>

+ 1 - 1
web/src/components/table/channels/modals/ModelTestModal.jsx

@@ -175,7 +175,7 @@ const ModelTestModal = ({
             <Typography.Text strong className="!text-[var(--semi-color-text-0)] !text-base">
               {currentTestChannel.name} {t('渠道的模型测试')}
             </Typography.Text>
-            <Typography.Text type="tertiary" className="!text-xs flex items-center">
+            <Typography.Text type="tertiary" size="small">
               {t('共')} {currentTestChannel.models.split(',').length} {t('个模型')}
             </Typography.Text>
           </div>

+ 40 - 7
web/src/components/table/models/ModelsActions.jsx

@@ -20,13 +20,15 @@ For commercial licensing, please contact support@quantumnous.com
 import React, { useState } from 'react';
 import MissingModelsModal from './modals/MissingModelsModal.jsx';
 import PrefillGroupManagement from './modals/PrefillGroupManagement.jsx';
-import { Button, Space, Modal } from '@douyinfe/semi-ui';
+import EditPrefillGroupModal from './modals/EditPrefillGroupModal.jsx';
+import { Button, Modal } from '@douyinfe/semi-ui';
+import { showSuccess, showError, copy } from '../../../helpers';
 import CompactModeToggle from '../../common/ui/CompactModeToggle';
-import { showError } from '../../../helpers';
 import SelectionNotification from './components/SelectionNotification.jsx';
 
 const ModelsActions = ({
   selectedKeys,
+  setSelectedKeys,
   setEditingModel,
   setShowEdit,
   batchDeleteModels,
@@ -38,13 +40,11 @@ const ModelsActions = ({
   const [showDeleteModal, setShowDeleteModal] = useState(false);
   const [showMissingModal, setShowMissingModal] = useState(false);
   const [showGroupManagement, setShowGroupManagement] = useState(false);
+  const [showAddPrefill, setShowAddPrefill] = useState(false);
+  const [prefillInit, setPrefillInit] = useState({ id: undefined });
 
   // Handle delete selected models with confirmation
   const handleDeleteSelectedModels = () => {
-    if (selectedKeys.length === 0) {
-      showError(t('请至少选择一个模型!'));
-      return;
-    }
     setShowDeleteModal(true);
   };
 
@@ -54,6 +54,30 @@ const ModelsActions = ({
     setShowDeleteModal(false);
   };
 
+  // Handle clear selection
+  const handleClearSelected = () => {
+    setSelectedKeys([]);
+  };
+
+  // Handle add selected models to prefill group
+  const handleCopyNames = async () => {
+    const text = selectedKeys.map(m => m.model_name).join(',');
+    if (!text) return;
+    const ok = await copy(text);
+    if (ok) {
+      showSuccess(t('已复制模型名称'));
+    } else {
+      showError(t('复制失败'));
+    }
+  };
+
+  const handleAddToPrefill = () => {
+    // Prepare initial data
+    const items = selectedKeys.map((m) => m.model_name);
+    setPrefillInit({ id: undefined, type: 'model', items });
+    setShowAddPrefill(true);
+  };
+
   return (
     <>
       <div className="flex flex-wrap gap-2 w-full md:w-auto order-2 md:order-1">
@@ -71,7 +95,6 @@ const ModelsActions = ({
           {t('添加模型')}
         </Button>
 
-
         <Button
           type="secondary"
           className="flex-1 md:flex-initial"
@@ -101,6 +124,9 @@ const ModelsActions = ({
         selectedKeys={selectedKeys}
         t={t}
         onDelete={handleDeleteSelectedModels}
+        onAddPrefill={handleAddToPrefill}
+        onClear={handleClearSelected}
+        onCopy={handleCopyNames}
       />
 
       <Modal
@@ -130,6 +156,13 @@ const ModelsActions = ({
         visible={showGroupManagement}
         onClose={() => setShowGroupManagement(false)}
       />
+
+      <EditPrefillGroupModal
+        visible={showAddPrefill}
+        onClose={() => setShowAddPrefill(false)}
+        editingGroup={prefillInit}
+        onSuccess={() => setShowAddPrefill(false)}
+      />
     </>
   );
 };

+ 37 - 7
web/src/components/table/models/components/SelectionNotification.jsx

@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React, { useEffect } from 'react';
-import { Notification, Button, Space } from '@douyinfe/semi-ui';
+import { Notification, Button, Space, Typography } from '@douyinfe/semi-ui';
 
 // 固定通知 ID,保持同一个实例即可避免闪烁
 const NOTICE_ID = 'models-batch-actions';
@@ -28,22 +28,52 @@ const NOTICE_ID = 'models-batch-actions';
  * 1. 当 selectedKeys.length > 0 时,使用固定 id 创建/更新通知
  * 2. 当 selectedKeys 清空时关闭通知
  */
-const SelectionNotification = ({ selectedKeys = [], t, onDelete }) => {
+const SelectionNotification = ({ selectedKeys = [], t, onDelete, onAddPrefill, onClear, onCopy }) => {
   // 根据选中数量决定显示/隐藏或更新通知
   useEffect(() => {
     const selectedCount = selectedKeys.length;
 
     if (selectedCount > 0) {
+      const titleNode = (
+        <Space wrap>
+          <span>{t('批量操作')}</span>
+          <Typography.Text type="tertiary" size="small">{t('已选择 {{count}} 个模型', { count: selectedCount })}</Typography.Text>
+        </Space>
+      );
+
       const content = (
-        <Space>
-          <span>{t('已选择 {{count}} 个模型', { count: selectedCount })}</span>
+        <Space wrap>
+          <Button
+            size="small"
+            type="secondary"
+            theme="solid"
+            onClick={onClear}
+          >
+            {t('取消全选')}
+          </Button>
+          <Button
+            size="small"
+            type="primary"
+            theme="solid"
+            onClick={onAddPrefill}
+          >
+            {t('加入预填组')}
+          </Button>
+          <Button
+            size="small"
+            type="tertiary"
+            theme="solid"
+            onClick={onCopy}
+          >
+            {t('复制名称')}
+          </Button>
           <Button
             size="small"
             type="danger"
             theme="solid"
             onClick={onDelete}
           >
-            {t('删除所选模型')}
+            {t('删除所选')}
           </Button>
         </Space>
       );
@@ -51,7 +81,7 @@ const SelectionNotification = ({ selectedKeys = [], t, onDelete }) => {
       // 使用相同 id 更新通知(若已存在则就地更新,不存在则创建)
       Notification.info({
         id: NOTICE_ID,
-        title: t('批量操作'),
+        title: titleNode,
         content,
         duration: 0, // 不自动关闭
         position: 'bottom',
@@ -61,7 +91,7 @@ const SelectionNotification = ({ selectedKeys = [], t, onDelete }) => {
       // 取消全部勾选时关闭通知
       Notification.close(NOTICE_ID);
     }
-  }, [selectedKeys, t, onDelete]);
+  }, [selectedKeys, t, onDelete, onAddPrefill, onClear, onCopy]);
 
   // 卸载时确保关闭通知
   useEffect(() => {

+ 2 - 0
web/src/components/table/models/index.jsx

@@ -42,6 +42,7 @@ const ModelsPage = () => {
 
     // Actions state
     selectedKeys,
+    setSelectedKeys,
     setEditingModel,
     setShowEdit,
     batchDeleteModels,
@@ -100,6 +101,7 @@ const ModelsPage = () => {
           <div className="flex flex-col md:flex-row justify-between items-center gap-2 w-full">
             <ModelsActions
               selectedKeys={selectedKeys}
+              setSelectedKeys={setSelectedKeys}
               setEditingModel={setEditingModel}
               setShowEdit={setShowEdit}
               batchDeleteModels={batchDeleteModels}

+ 69 - 80
web/src/components/table/models/modals/EditModelModal.jsx

@@ -35,13 +35,13 @@ import {
   Save,
   X,
   FileText,
-  Building,
-  Settings,
 } from 'lucide-react';
 import { API, showError, showSuccess } from '../../../../helpers';
 import { useTranslation } from 'react-i18next';
 import { useIsMobile } from '../../../../hooks/common/useIsMobile';
 
+const { Text, Title } = Typography;
+
 const nameRuleOptions = [
   { label: '精确名称匹配', value: 0 },
   { label: '前缀名称匹配', value: 1 },
@@ -49,16 +49,6 @@ const nameRuleOptions = [
   { label: '后缀名称匹配', value: 3 },
 ];
 
-const endpointOptions = [
-  { label: 'OpenAI', value: 'openai' },
-  { label: 'Anthropic', value: 'anthropic' },
-  { label: 'Gemini', value: 'gemini' },
-  { label: 'Image Generation', value: 'image-generation' },
-  { label: 'Jina Rerank', value: 'jina-rerank' },
-];
-
-const { Text, Title } = Typography;
-
 const EditModelModal = (props) => {
   const { t } = useTranslation();
   const [loading, setLoading] = useState(false);
@@ -332,23 +322,6 @@ const EditModelModal = (props) => {
                       showClear
                     />
                   </Col>
-                  <Col span={24}>
-                    <Form.Select
-                      field='tag_group'
-                      label={t('标签组')}
-                      placeholder={t('选择标签组后将自动填充标签')}
-                      optionList={tagGroups.map(g => ({ label: g.name, value: g.id }))}
-                      showClear
-                      onChange={(value) => {
-                        const g = tagGroups.find(item => item.id === value);
-                        if (g && formApiRef.current) {
-                          formApiRef.current.setValue('tags', g.items || []);
-                        }
-                      }}
-                      style={{ width: '100%' }}
-                    />
-                  </Col>
-
                   <Col span={24}>
                     <Form.TagInput
                       field='tags'
@@ -366,23 +339,40 @@ const EditModelModal = (props) => {
                         formApiRef.current.setValue('tags', normalized);
                       }}
                       style={{ width: '100%' }}
+                      extraText={(
+                        <Space wrap>
+                          {tagGroups.map(group => (
+                            <Button
+                              key={group.id}
+                              size='small'
+                              type='primary'
+                              onClick={() => {
+                                if (formApiRef.current) {
+                                  const currentTags = formApiRef.current.getValue('tags') || [];
+                                  const newTags = [...currentTags, ...(group.items || [])];
+                                  const uniqueTags = [...new Set(newTags)];
+                                  formApiRef.current.setValue('tags', uniqueTags);
+                                }
+                              }}
+                            >
+                              {group.name}
+                            </Button>
+                          ))}
+                          <Button
+                            size='small'
+                            type='warning'
+                            onClick={() => {
+                              if (formApiRef.current) {
+                                formApiRef.current.setValue('tags', []);
+                              }
+                            }}
+                          >
+                            {t('清除标签')}
+                          </Button>
+                        </Space>
+                      )}
                     />
                   </Col>
-                </Row>
-              </Card>
-
-              {/* 供应商信息 */}
-              <Card className='!rounded-2xl shadow-sm border-0'>
-                <div className='flex items-center mb-2'>
-                  <Avatar size='small' color='blue' className='mr-2 shadow-md'>
-                    <Building size={16} />
-                  </Avatar>
-                  <div>
-                    <Text className='text-lg font-medium'>{t('供应商信息')}</Text>
-                    <div className='text-xs text-gray-600'>{t('设置模型的供应商相关信息')}</div>
-                  </div>
-                </div>
-                <Row gutter={12}>
                   <Col span={24}>
                     <Form.Select
                       field='vendor_id'
@@ -400,47 +390,46 @@ const EditModelModal = (props) => {
                       style={{ width: '100%' }}
                     />
                   </Col>
-                </Row>
-              </Card>
-
-              {/* 功能配置 */}
-              <Card className='!rounded-2xl shadow-sm border-0'>
-                <div className='flex items-center mb-2'>
-                  <Avatar size='small' color='purple' className='mr-2 shadow-md'>
-                    <Settings size={16} />
-                  </Avatar>
-                  <div>
-                    <Text className='text-lg font-medium'>{t('功能配置')}</Text>
-                    <div className='text-xs text-gray-600'>{t('设置模型的功能和状态')}</div>
-                  </div>
-                </div>
-                <Row gutter={12}>
-                  <Col span={24}>
-                    <Form.Select
-                      field='endpoint_group'
-                      label={t('端点组')}
-                      placeholder={t('选择端点组后将自动填充端点')}
-                      optionList={endpointGroups.map(g => ({ label: g.name, value: g.id }))}
-                      showClear
-                      style={{ width: '100%' }}
-                      onChange={(value) => {
-                        const g = endpointGroups.find(item => item.id === value);
-                        if (g && formApiRef.current) {
-                          formApiRef.current.setValue('endpoints', g.items || []);
-                        }
-                      }}
-                    />
-                  </Col>
-
                   <Col span={24}>
-                    <Form.Select
+                    <Form.TagInput
                       field='endpoints'
                       label={t('支持端点')}
-                      placeholder={t('选择模型支持的端点类型')}
-                      optionList={endpointOptions}
-                      multiple
+                      placeholder={t('输入端点名称,按回车添加')}
+                      addOnBlur
                       showClear
                       style={{ width: '100%' }}
+                      extraText={(
+                        <Space wrap>
+                          {endpointGroups.map(group => (
+                            <Button
+                              key={group.id}
+                              size='small'
+                              type='primary'
+                              onClick={() => {
+                                if (formApiRef.current) {
+                                  const currentEndpoints = formApiRef.current.getValue('endpoints') || [];
+                                  const newEndpoints = [...currentEndpoints, ...(group.items || [])];
+                                  const uniqueEndpoints = [...new Set(newEndpoints)];
+                                  formApiRef.current.setValue('endpoints', uniqueEndpoints);
+                                }
+                              }}
+                            >
+                              {group.name}
+                            </Button>
+                          ))}
+                          <Button
+                            size='small'
+                            type='warning'
+                            onClick={() => {
+                              if (formApiRef.current) {
+                                formApiRef.current.setValue('endpoints', []);
+                              }
+                            }}
+                          >
+                            {t('清除端点')}
+                          </Button>
+                        </Space>
+                      )}
                     />
                   </Col>
                   <Col span={24}>

+ 1 - 1
web/src/components/table/models/modals/MissingModelsModal.jsx

@@ -111,7 +111,7 @@ const MissingModelsModal = ({
             <Typography.Text strong className="!text-[var(--semi-color-text-0)] !text-base">
               {t('未配置的模型列表')}
             </Typography.Text>
-            <Typography.Text type="tertiary" className="!text-xs flex items-center">
+            <Typography.Text type="tertiary" size="small">
               {t('共')} {missingModels.length} {t('个未配置模型')}
             </Typography.Text>
           </div>

+ 1 - 0
web/src/hooks/models/useModelsData.js

@@ -328,6 +328,7 @@ export const useModelsData = () => {
     selectedKeys,
     rowSelection,
     handleRow,
+    setSelectedKeys,
 
     // Modal state
     showEdit,