🚀 refactor: migrate vendor-count aggregation to model layer & align frontend logic

Summary
• Backend
  – Moved duplicate-name validation and total vendor-count aggregation from controllers (`controller/model_meta.go`, `controller/vendor_meta.go`, `controller/prefill_group.go`) to model layer (`model/model_meta.go`, `model/vendor_meta.go`, `model/prefill_group.go`).
  – Added `GetVendorModelCounts()` and `Is*NameDuplicated()` helpers; controllers now call these instead of duplicating queries.
  – API response for `/api/models` now returns `vendor_counts` with per-vendor totals across all pages, plus `all` summary.
  – Removed redundant checks and unused imports, eliminating `go vet` warnings.

• Frontend
  – `useModelsData.js` updated to consume backend-supplied `vendor_counts`, calculate the `all` total once, and drop legacy client-side counting logic.
  – Simplified initial data flow: first render now triggers only one models request.
  – Deleted obsolete `updateVendorCounts` helper and related comments.
  – Ensured search flow also sets `vendorCounts`, keeping tab badges accurate.

Why
This refactor enforces single-responsibility (aggregation in model layer), delivers consistent totals irrespective of pagination, and removes redundant client queries, leading to cleaner code and better performance.
This commit is contained in:
t0ng7u
2025-08-06 01:40:08 +08:00
parent d61a862fa2
commit 7c814a5fd9
12 changed files with 334 additions and 175 deletions

View File

@@ -23,6 +23,7 @@ import PrefillGroupManagement from './modals/PrefillGroupManagement.jsx';
import { Button, Space, Modal } from '@douyinfe/semi-ui';
import CompactModeToggle from '../../common/ui/CompactModeToggle';
import { showError } from '../../../helpers';
import SelectionNotification from './components/SelectionNotification.jsx';
const ModelsActions = ({
selectedKeys,
@@ -70,14 +71,6 @@ const ModelsActions = ({
{t('添加模型')}
</Button>
<Button
type='danger'
className="flex-1 md:flex-initial"
onClick={handleDeleteSelectedModels}
size="small"
>
{t('删除所选模型')}
</Button>
<Button
type="secondary"
@@ -104,6 +97,12 @@ const ModelsActions = ({
/>
</div>
<SelectionNotification
selectedKeys={selectedKeys}
t={t}
onDelete={handleDeleteSelectedModels}
/>
<Modal
title={t('批量删除模型')}
visible={showDeleteModal}

View File

@@ -0,0 +1,76 @@
/*
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, { useEffect } from 'react';
import { Notification, Button, Space } from '@douyinfe/semi-ui';
// 固定通知 ID保持同一个实例即可避免闪烁
const NOTICE_ID = 'models-batch-actions';
/**
* SelectionNotification 选择通知组件
* 1. 当 selectedKeys.length > 0 时,使用固定 id 创建/更新通知
* 2. 当 selectedKeys 清空时关闭通知
*/
const SelectionNotification = ({ selectedKeys = [], t, onDelete }) => {
// 根据选中数量决定显示/隐藏或更新通知
useEffect(() => {
const selectedCount = selectedKeys.length;
if (selectedCount > 0) {
const content = (
<Space>
<span>{t('已选择 {{count}} 个模型', { count: selectedCount })}</span>
<Button
size="small"
type="danger"
theme="solid"
onClick={onDelete}
>
{t('删除所选模型')}
</Button>
</Space>
);
// 使用相同 id 更新通知(若已存在则就地更新,不存在则创建)
Notification.info({
id: NOTICE_ID,
title: t('批量操作'),
content,
duration: 0, // 不自动关闭
position: 'bottom',
showClose: false,
});
} else {
// 取消全部勾选时关闭通知
Notification.close(NOTICE_ID);
}
}, [selectedKeys, t, onDelete]);
// 卸载时确保关闭通知
useEffect(() => {
return () => {
Notification.close(NOTICE_ID);
};
}, []);
return null; // 该组件不渲染可见内容
};
export default SelectionNotification;

View File

@@ -32,10 +32,12 @@ import {
Row,
} from '@douyinfe/semi-ui';
import {
IconSave,
IconClose,
IconLayers,
} from '@douyinfe/semi-icons';
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';
@@ -258,7 +260,7 @@ const EditModelModal = (props) => {
theme='solid'
className='!rounded-lg'
onClick={() => formApiRef.current?.submitForm()}
icon={<IconSave />}
icon={<Save size={16} />}
loading={loading}
>
{t('提交')}
@@ -268,7 +270,7 @@ const EditModelModal = (props) => {
className='!rounded-lg'
type='primary'
onClick={handleCancel}
icon={<IconClose />}
icon={<X size={16} />}
>
{t('取消')}
</Button>
@@ -291,7 +293,7 @@ const EditModelModal = (props) => {
<Card className='!rounded-2xl shadow-sm border-0'>
<div className='flex items-center mb-2'>
<Avatar size='small' color='green' className='mr-2 shadow-md'>
<IconLayers size={16} />
<FileText size={16} />
</Avatar>
<div>
<Text className='text-lg font-medium'>{t('基本信息')}</Text>
@@ -373,7 +375,7 @@ const EditModelModal = (props) => {
<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'>
<IconLayers size={16} />
<Building size={16} />
</Avatar>
<div>
<Text className='text-lg font-medium'>{t('供应商信息')}</Text>
@@ -405,7 +407,7 @@ const EditModelModal = (props) => {
<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'>
<IconLayers size={16} />
<Settings size={16} />
</Avatar>
<div>
<Text className='text-lg font-medium'>{t('功能配置')}</Text>