Browse Source

🚀 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.
t0ng7u 5 months ago
parent
commit
7c814a5fd9

+ 28 - 1
controller/model_meta.go

@@ -25,9 +25,19 @@ func GetAllModelsMeta(c *gin.Context) {
     }
     var total int64
     model.DB.Model(&model.Model{}).Count(&total)
+
+    // 统计供应商计数(全部数据,不受分页影响)
+    vendorCounts, _ := model.GetVendorModelCounts()
+
     pageInfo.SetTotal(int(total))
     pageInfo.SetItems(modelsMeta)
-    common.ApiSuccess(c, pageInfo)
+    common.ApiSuccess(c, gin.H{
+        "items":         modelsMeta,
+        "total":         total,
+        "page":          pageInfo.GetPage(),
+        "page_size":     pageInfo.GetPageSize(),
+        "vendor_counts": vendorCounts,
+    })
 }
 
 // SearchModelsMeta 搜索模型列表
@@ -78,6 +88,14 @@ func CreateModelMeta(c *gin.Context) {
         common.ApiErrorMsg(c, "模型名称不能为空")
         return
     }
+    // 名称冲突检查
+    if dup, err := model.IsModelNameDuplicated(0, m.ModelName); err != nil {
+        common.ApiError(c, err)
+        return
+    } else if dup {
+        common.ApiErrorMsg(c, "模型名称已存在")
+        return
+    }
 
     if err := m.Insert(); err != nil {
         common.ApiError(c, err)
@@ -108,6 +126,15 @@ func UpdateModelMeta(c *gin.Context) {
             return
         }
     } else {
+        // 名称冲突检查
+        if dup, err := model.IsModelNameDuplicated(m.Id, m.ModelName); err != nil {
+            common.ApiError(c, err)
+            return
+        } else if dup {
+            common.ApiErrorMsg(c, "模型名称已存在")
+            return
+        }
+
         if err := m.Update(); err != nil {
             common.ApiError(c, err)
             return

+ 18 - 0
controller/prefill_group.go

@@ -31,6 +31,15 @@ func CreatePrefillGroup(c *gin.Context) {
         common.ApiErrorMsg(c, "组名称和类型不能为空")
         return
     }
+    // 创建前检查名称
+    if dup, err := model.IsPrefillGroupNameDuplicated(0, g.Name); err != nil {
+        common.ApiError(c, err)
+        return
+    } else if dup {
+        common.ApiErrorMsg(c, "组名称已存在")
+        return
+    }
+
     if err := g.Insert(); err != nil {
         common.ApiError(c, err)
         return
@@ -49,6 +58,15 @@ func UpdatePrefillGroup(c *gin.Context) {
         common.ApiErrorMsg(c, "缺少组 ID")
         return
     }
+    // 名称冲突检查
+    if dup, err := model.IsPrefillGroupNameDuplicated(g.Id, g.Name); err != nil {
+        common.ApiError(c, err)
+        return
+    } else if dup {
+        common.ApiErrorMsg(c, "组名称已存在")
+        return
+    }
+
     if err := g.Update(); err != nil {
         common.ApiError(c, err)
         return

+ 14 - 4
controller/vendor_meta.go

@@ -65,6 +65,15 @@ func CreateVendorMeta(c *gin.Context) {
         common.ApiErrorMsg(c, "供应商名称不能为空")
         return
     }
+    // 创建前先检查名称
+    if dup, err := model.IsVendorNameDuplicated(0, v.Name); err != nil {
+        common.ApiError(c, err)
+        return
+    } else if dup {
+        common.ApiErrorMsg(c, "供应商名称已存在")
+        return
+    }
+
     if err := v.Insert(); err != nil {
         common.ApiError(c, err)
         return
@@ -83,10 +92,11 @@ func UpdateVendorMeta(c *gin.Context) {
         common.ApiErrorMsg(c, "缺少供应商 ID")
         return
     }
-    // 检查名称冲突
-    var dup int64
-    _ = model.DB.Model(&model.Vendor{}).Where("name = ? AND id <> ?", v.Name, v.Id).Count(&dup).Error
-    if dup > 0 {
+    // 名称冲突检查
+    if dup, err := model.IsVendorNameDuplicated(v.Id, v.Name); err != nil {
+        common.ApiError(c, err)
+        return
+    } else if dup {
         common.ApiErrorMsg(c, "供应商名称已存在")
         return
     }

+ 29 - 0
model/model_meta.go

@@ -60,6 +60,16 @@ func (mi *Model) Insert() error {
     return DB.Create(mi).Error
 }
 
+// IsModelNameDuplicated 检查模型名称是否重复(排除自身 ID)
+func IsModelNameDuplicated(id int, name string) (bool, error) {
+    if name == "" {
+        return false, nil
+    }
+    var cnt int64
+    err := DB.Model(&Model{}).Where("model_name = ? AND id <> ?", name, id).Count(&cnt).Error
+    return cnt > 0, err
+}
+
 // Update 更新现有模型记录
 func (mi *Model) Update() error {
     // 仅更新需要变更的字段,避免覆盖 CreatedTime
@@ -84,6 +94,25 @@ func GetModelByName(name string) (*Model, error) {
     return &mi, nil
 }
 
+// GetVendorModelCounts 统计每个供应商下模型数量(不受分页影响)
+func GetVendorModelCounts() (map[int64]int64, error) {
+    var stats []struct {
+        VendorID int64
+        Count    int64
+    }
+    if err := DB.Model(&Model{}).
+        Select("vendor_id as vendor_id, count(*) as count").
+        Group("vendor_id").
+        Scan(&stats).Error; err != nil {
+        return nil, err
+    }
+    m := make(map[int64]int64, len(stats))
+    for _, s := range stats {
+        m[s.VendorID] = s.Count
+    }
+    return m, nil
+}
+
 // GetAllModels 分页获取所有模型元数据
 func GetAllModels(offset int, limit int) ([]*Model, error) {
     var models []*Model

+ 10 - 0
model/prefill_group.go

@@ -33,6 +33,16 @@ func (g *PrefillGroup) Insert() error {
     return DB.Create(g).Error
 }
 
+// IsPrefillGroupNameDuplicated 检查组名称是否重复(排除自身 ID)
+func IsPrefillGroupNameDuplicated(id int, name string) (bool, error) {
+    if name == "" {
+        return false, nil
+    }
+    var cnt int64
+    err := DB.Model(&PrefillGroup{}).Where("name = ? AND id <> ?", name, id).Count(&cnt).Error
+    return cnt > 0, err
+}
+
 // Update 更新组
 func (g *PrefillGroup) Update() error {
     g.UpdatedTime = common.GetTimestamp()

+ 10 - 0
model/vendor_meta.go

@@ -31,6 +31,16 @@ func (v *Vendor) Insert() error {
     return DB.Create(v).Error
 }
 
+// IsVendorNameDuplicated 检查供应商名称是否重复(排除自身 ID)
+func IsVendorNameDuplicated(id int, name string) (bool, error) {
+    if name == "" {
+        return false, nil
+    }
+    var cnt int64
+    err := DB.Model(&Vendor{}).Where("name = ? AND id <> ?", name, id).Count(&cnt).Error
+    return cnt > 0, err
+}
+
 // Update 更新供应商记录
 func (v *Vendor) Update() error {
     v.UpdatedTime = common.GetTimestamp()

+ 12 - 15
web/src/components/table/model-pricing/modal/components/ModelBasicInfo.jsx

@@ -71,21 +71,18 @@ const ModelBasicInfo = ({ modelData, vendorsMap = {}, t }) => {
       <div className="text-gray-600">
         <p className="mb-4">{getModelDescription()}</p>
         {getModelTags().length > 0 && (
-          <div>
-            <Text className="text-sm font-medium text-gray-700 mb-2 block">{t('模型标签')}</Text>
-            <Space wrap>
-              {getModelTags().map((tag, index) => (
-                <Tag
-                  key={index}
-                  color={tag.color}
-                  shape="circle"
-                  size="small"
-                >
-                  {tag.text}
-                </Tag>
-              ))}
-            </Space>
-          </div>
+          <Space wrap>
+            {getModelTags().map((tag, index) => (
+              <Tag
+                key={index}
+                color={tag.color}
+                shape="circle"
+                size="small"
+              >
+                {tag.text}
+              </Tag>
+            ))}
+          </Space>
         )}
       </div>
     </Card>

+ 107 - 101
web/src/components/table/model-pricing/view/card/PricingCardView.jsx

@@ -131,41 +131,42 @@ const PricingCardView = ({
 
   // 渲染标签
   const renderTags = (record) => {
-    const allTags = [];
-
-    // 计费类型标签  
+    // 计费类型标签(左边)
     const billingType = record.quota_type === 1 ? 'teal' : 'violet';
     const billingText = record.quota_type === 1 ? t('按次计费') : t('按量计费');
-    allTags.push({
-      key: "billing",
-      element: (
-        <Tag shape='circle' color={billingType} size='small'>
-          {billingText}
-        </Tag>
-      )
-    });
+    const billingTag = (
+      <Tag key="billing" shape='circle' color={billingType} size='small'>
+        {billingText}
+      </Tag>
+    );
 
-    // 自定义标签
+    // 自定义标签(右边)
+    const customTags = [];
     if (record.tags) {
       const tagArr = record.tags.split(',').filter(Boolean);
       tagArr.forEach((tg, idx) => {
-        allTags.push({
-          key: `custom-${idx}`,
-          element: (
-            <Tag shape='circle' color={stringToColor(tg)} size='small'>
-              {tg}
-            </Tag>
-          )
-        });
+        customTags.push(
+          <Tag key={`custom-${idx}`} shape='circle' color={stringToColor(tg)} size='small'>
+            {tg}
+          </Tag>
+        );
       });
     }
 
-    // 使用 renderLimitedItems 渲染标签
-    return renderLimitedItems({
-      items: allTags,
-      renderItem: (item, idx) => React.cloneElement(item.element, { key: item.key }),
-      maxDisplay: 3
-    });
+    return (
+      <div className="flex items-center justify-between">
+        <div className="flex items-center gap-2">
+          {billingTag}
+        </div>
+        <div className="flex items-center gap-1">
+          {renderLimitedItems({
+            items: customTags.map((tag, idx) => ({ key: `custom-${idx}`, element: tag })),
+            renderItem: (item, idx) => item.element,
+            maxDisplay: 3
+          })}
+        </div>
+      </div>
+    );
   };
 
   // 显示骨架屏
@@ -201,96 +202,101 @@ const PricingCardView = ({
             <Card
               key={modelKey || index}
               className={`!rounded-2xl transition-all duration-200 hover:shadow-lg border cursor-pointer ${isSelected ? CARD_STYLES.selected : CARD_STYLES.default}`}
-              bodyStyle={{ padding: '24px' }}
+              bodyStyle={{ height: '100%' }}
               onClick={() => openModelDetail && openModelDetail(model)}
             >
-              {/* 头部:图标 + 模型名称 + 操作按钮 */}
-              <div className="flex items-start justify-between mb-3">
-                <div className="flex items-start space-x-3 flex-1 min-w-0">
-                  {getModelIcon(model)}
-                  <div className="flex-1 min-w-0">
-                    <h3 className="text-lg font-bold text-gray-900 truncate">
-                      {model.model_name}
-                    </h3>
-                    <div className="flex items-center gap-3 text-xs mt-1">
-                      {renderPriceInfo(model)}
+              <div className="flex flex-col h-full">
+                {/* 头部:图标 + 模型名称 + 操作按钮 */}
+                <div className="flex items-start justify-between mb-3">
+                  <div className="flex items-start space-x-3 flex-1 min-w-0">
+                    {getModelIcon(model)}
+                    <div className="flex-1 min-w-0">
+                      <h3 className="text-lg font-bold text-gray-900 truncate">
+                        {model.model_name}
+                      </h3>
+                      <div className="flex items-center gap-3 text-xs mt-1">
+                        {renderPriceInfo(model)}
+                      </div>
                     </div>
                   </div>
-                </div>
 
-                <div className="flex items-center space-x-2 ml-3">
-                  {/* 复制按钮 */}
-                  <Button
-                    size="small"
-                    type="tertiary"
-                    icon={<IconCopy />}
-                    onClick={(e) => {
-                      e.stopPropagation();
-                      copyText(model.model_name);
-                    }}
-                  />
-
-                  {/* 选择框 */}
-                  {rowSelection && (
-                    <Checkbox
-                      checked={isSelected}
-                      onChange={(e) => {
+                  <div className="flex items-center space-x-2 ml-3">
+                    {/* 复制按钮 */}
+                    <Button
+                      size="small"
+                      type="tertiary"
+                      icon={<IconCopy />}
+                      onClick={(e) => {
                         e.stopPropagation();
-                        handleCheckboxChange(model, e.target.checked);
+                        copyText(model.model_name);
                       }}
                     />
-                  )}
-                </div>
-              </div>
 
-              {/* 模型描述 */}
-              <div className="mb-4">
-                <p
-                  className="text-xs line-clamp-2 leading-relaxed"
-                  style={{ color: 'var(--semi-color-text-2)' }}
-                >
-                  {getModelDescription(model)}
-                </p>
-              </div>
-
-              {/* 标签区域 */}
-              <div>
-                {renderTags(model)}
-              </div>
-
-              {/* 倍率信息(可选) */}
-              {showRatio && (
-                <div
-                  className="mt-4 pt-3 border-t border-dashed"
-                  style={{ borderColor: 'var(--semi-color-border)' }}
-                >
-                  <div className="flex items-center space-x-1 mb-2">
-                    <span className="text-xs font-medium text-gray-700">{t('倍率信息')}</span>
-                    <Tooltip content={t('倍率是为了方便换算不同价格的模型')}>
-                      <IconHelpCircle
-                        className="text-blue-500 cursor-pointer"
-                        size="small"
-                        onClick={(e) => {
+                    {/* 选择框 */}
+                    {rowSelection && (
+                      <Checkbox
+                        checked={isSelected}
+                        onChange={(e) => {
                           e.stopPropagation();
-                          setModalImageUrl('/ratio.png');
-                          setIsModalOpenurl(true);
+                          handleCheckboxChange(model, e.target.checked);
                         }}
                       />
-                    </Tooltip>
+                    )}
                   </div>
-                  <div className="grid grid-cols-3 gap-2 text-xs text-gray-600">
-                    <div>
-                      {t('模型')}: {model.quota_type === 0 ? model.model_ratio : t('无')}
-                    </div>
-                    <div>
-                      {t('补全')}: {model.quota_type === 0 ? parseFloat(model.completion_ratio.toFixed(3)) : t('无')}
-                    </div>
-                    <div>
-                      {t('分组')}: {groupRatio[selectedGroup]}
-                    </div>
+                </div>
+
+                {/* 模型描述 - 占据剩余空间 */}
+                <div className="flex-1 mb-4">
+                  <p
+                    className="text-xs line-clamp-2 leading-relaxed"
+                    style={{ color: 'var(--semi-color-text-2)' }}
+                  >
+                    {getModelDescription(model)}
+                  </p>
+                </div>
+
+                {/* 底部区域 */}
+                <div className="mt-auto">
+                  {/* 标签区域 */}
+                  <div className="mb-3">
+                    {renderTags(model)}
                   </div>
+
+                  {/* 倍率信息(可选) */}
+                  {showRatio && (
+                    <div
+                      className="pt-3 border-t border-dashed"
+                      style={{ borderColor: 'var(--semi-color-border)' }}
+                    >
+                      <div className="flex items-center space-x-1 mb-2">
+                        <span className="text-xs font-medium text-gray-700">{t('倍率信息')}</span>
+                        <Tooltip content={t('倍率是为了方便换算不同价格的模型')}>
+                          <IconHelpCircle
+                            className="text-blue-500 cursor-pointer"
+                            size="small"
+                            onClick={(e) => {
+                              e.stopPropagation();
+                              setModalImageUrl('/ratio.png');
+                              setIsModalOpenurl(true);
+                            }}
+                          />
+                        </Tooltip>
+                      </div>
+                      <div className="grid grid-cols-3 gap-2 text-xs text-gray-600">
+                        <div>
+                          {t('模型')}: {model.quota_type === 0 ? model.model_ratio : t('无')}
+                        </div>
+                        <div>
+                          {t('补全')}: {model.quota_type === 0 ? parseFloat(model.completion_ratio.toFixed(3)) : t('无')}
+                        </div>
+                        <div>
+                          {t('分组')}: {groupRatio[selectedGroup]}
+                        </div>
+                      </div>
+                    </div>
+                  )}
                 </div>
-              )}
+              </div>
             </Card>
           );
         })}

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

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

+ 76 - 0
web/src/components/table/models/components/SelectionNotification.jsx

@@ -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 [email protected]
+*/
+
+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;

+ 11 - 9
web/src/components/table/models/modals/EditModelModal.jsx

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

+ 7 - 32
web/src/hooks/models/useModelsData.js

@@ -135,9 +135,9 @@ export const useModelsData = () => {
         setModelCount(data.total || newPageData.length);
         setModelFormat(newPageData);
 
-        // Refresh vendor counts only when viewing 'all' to preserve other counts
-        if (vendorKey === 'all') {
-          updateVendorCounts(newPageData);
+        if (data.vendor_counts) {
+          const sumAll = Object.values(data.vendor_counts).reduce((acc, v) => acc + v, 0);
+          setVendorCounts({ ...data.vendor_counts, all: sumAll });
         }
       } else {
         showError(message);
@@ -151,27 +151,9 @@ export const useModelsData = () => {
     setLoading(false);
   };
 
-  // Fetch vendor counts separately to keep tab numbers accurate
-  const refreshVendorCounts = async () => {
-    try {
-      // Load all models (large page_size) to compute counts for every vendor
-      const res = await API.get('/api/models/?p=1&page_size=100000');
-      if (res.data.success) {
-        const newItems = extractItems(res.data.data);
-        updateVendorCounts(newItems);
-      }
-    } catch (_) {
-      // ignore count refresh errors
-    }
-  };
-
   // Refresh data
   const refresh = async (page = activePage) => {
     await loadModels(page, pageSize);
-    // When not viewing 'all', tab counts need a separate refresh
-    if (activeVendorKey !== 'all') {
-      await refreshVendorCounts();
-    }
   };
 
   // Search models with keyword and vendor
@@ -195,6 +177,10 @@ export const useModelsData = () => {
         setActivePage(data.page || 1);
         setModelCount(data.total || newPageData.length);
         setModelFormat(newPageData);
+        if (data.vendor_counts) {
+          const sumAll = Object.values(data.vendor_counts).reduce((acc, v) => acc + v, 0);
+          setVendorCounts({ ...data.vendor_counts, all: sumAll });
+        }
       } else {
         showError(message);
         setModels([]);
@@ -242,16 +228,6 @@ export const useModelsData = () => {
     }
   };
 
-  // Update vendor counts
-  const updateVendorCounts = (models) => {
-    const counts = { all: models.length };
-    models.forEach(model => {
-      if (model.vendor_id) {
-        counts[model.vendor_id] = (counts[model.vendor_id] || 0) + 1;
-      }
-    });
-    setVendorCounts(counts);
-  };
 
   // Handle page change
   const handlePageChange = (page) => {
@@ -335,7 +311,6 @@ export const useModelsData = () => {
   useEffect(() => {
     (async () => {
       await loadVendors();
-      await loadModels();
     })();
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);