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.
This commit is contained in:
t0ng7u
2025-08-06 03:29:45 +08:00
parent 7c814a5fd9
commit 94506bee99
8 changed files with 171 additions and 116 deletions

View File

@@ -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 && (
<>

View File

@@ -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>

View File

@@ -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)}
/>
</>
);
};

View File

@@ -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(() => {

View File

@@ -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}

View File

@@ -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}>

View File

@@ -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>

View File

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