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 10 tháng trước cách đây
mục cha
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 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;

+ 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
   }, []);