From 7a2b9d86e8d923a6ed61a2e3ec1ec72ce74ea115 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sat, 11 Jul 2026 22:36:09 +0800 Subject: [PATCH] feat: enhance model search functionality with status and sync filters --- controller/model_meta.go | 19 ++- model/model_meta.go | 49 +++++++- .../models/components/models-columns.tsx | 118 +++++++++--------- .../models/components/models-table.tsx | 60 ++++----- 4 files changed, 144 insertions(+), 102 deletions(-) diff --git a/controller/model_meta.go b/controller/model_meta.go index fd362644..c3d99546 100644 --- a/controller/model_meta.go +++ b/controller/model_meta.go @@ -17,15 +17,15 @@ import ( func GetAllModelsMeta(c *gin.Context) { pageInfo := common.GetPageQuery(c) - modelsMeta, err := model.GetAllModels(pageInfo.GetStartIdx(), pageInfo.GetPageSize()) + status := c.Query("status") + syncOfficial := c.Query("sync_official") + modelsMeta, total, err := model.SearchModels("", "", status, syncOfficial, pageInfo.GetStartIdx(), pageInfo.GetPageSize()) if err != nil { common.ApiError(c, err) return } // 批量填充附加字段,提升列表接口性能 enrichModels(modelsMeta) - var total int64 - model.DB.Model(&model.Model{}).Count(&total) // 统计供应商计数(全部数据,不受分页影响) vendorCounts, _ := model.GetVendorModelCounts() @@ -46,18 +46,27 @@ func SearchModelsMeta(c *gin.Context) { keyword := c.Query("keyword") vendor := c.Query("vendor") + status := c.Query("status") + syncOfficial := c.Query("sync_official") pageInfo := common.GetPageQuery(c) - modelsMeta, total, err := model.SearchModels(keyword, vendor, pageInfo.GetStartIdx(), pageInfo.GetPageSize()) + modelsMeta, total, err := model.SearchModels(keyword, vendor, status, syncOfficial, pageInfo.GetStartIdx(), pageInfo.GetPageSize()) if err != nil { common.ApiError(c, err) return } // 批量填充附加字段,提升列表接口性能 enrichModels(modelsMeta) + 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, + }) } // GetModelMeta 根据 ID 获取单条模型信息 diff --git a/model/model_meta.go b/model/model_meta.go index 86421277..bd701e2b 100644 --- a/model/model_meta.go +++ b/model/model_meta.go @@ -105,8 +105,7 @@ func GetVendorModelCounts() (map[int64]int64, error) { } func GetAllModels(offset int, limit int) ([]*Model, error) { - var models []*Model - err := DB.Order("id DESC").Offset(offset).Limit(limit).Find(&models).Error + models, _, err := SearchModels("", "", "", "", offset, limit) return models, err } @@ -192,7 +191,7 @@ func GetPreferredModelOwnerChannelTypes(modelNames []string, groups []string) (m return result, nil } -func SearchModels(keyword string, vendor string, offset int, limit int) ([]*Model, int64, error) { +func SearchModels(keyword string, vendor string, status string, syncOfficial string, offset int, limit int) ([]*Model, int64, error) { var models []*Model db := DB.Model(&Model{}) if keyword != "" { @@ -206,6 +205,12 @@ func SearchModels(keyword string, vendor string, offset int, limit int) ([]*Mode db = db.Joins("JOIN vendors ON vendors.id = models.vendor_id").Where("vendors.name LIKE ?", "%"+vendor+"%") } } + if statusValue, ok := parseModelStatusFilter(status); ok { + db = db.Where("models.status = ?", statusValue) + } + if syncValue, ok := parseModelSyncFilter(syncOfficial); ok { + db = db.Where("models.sync_official = ?", syncValue) + } var total int64 if err := db.Count(&total).Error; err != nil { return nil, 0, err @@ -215,3 +220,41 @@ func SearchModels(keyword string, vendor string, offset int, limit int) ([]*Mode } return models, total, nil } + +// parseModelStatusFilter maps UI/API status values to the models.status column. +// Returns ok=false when no status filter should be applied. +func parseModelStatusFilter(status string) (value int, ok bool) { + switch strings.ToLower(strings.TrimSpace(status)) { + case "", "all": + return 0, false + case "enabled", "1": + return 1, true + case "disabled", "0": + return 0, true + default: + n, err := strconv.Atoi(status) + if err != nil { + return 0, false + } + return n, true + } +} + +// parseModelSyncFilter maps UI/API sync values to the models.sync_official column. +// Returns ok=false when no sync filter should be applied. +func parseModelSyncFilter(syncOfficial string) (value int, ok bool) { + switch strings.ToLower(strings.TrimSpace(syncOfficial)) { + case "", "all": + return 0, false + case "yes", "1": + return 1, true + case "no", "0": + return 0, true + default: + n, err := strconv.Atoi(syncOfficial) + if err != nil { + return 0, false + } + return n, true + } +} diff --git a/web/default/src/features/models/components/models-columns.tsx b/web/default/src/features/models/components/models-columns.tsx index 61afe555..9a4041e7 100644 --- a/web/default/src/features/models/components/models-columns.tsx +++ b/web/default/src/features/models/components/models-columns.tsx @@ -16,7 +16,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { type ColumnDef } from '@tanstack/react-table' +import type { ColumnDef } from '@tanstack/react-table' import { useTranslation } from 'react-i18next' import { BadgeCell, BadgeListCell } from '@/components/data-table' @@ -99,16 +99,17 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { const id = row.getValue('id') as number return }, - size: 80, + size: 64, }, - // Icon column + // Model Name column (with model icon) { - accessorKey: 'icon', - header: t('Icon'), - meta: { mobileHidden: true }, + accessorKey: 'model_name', + header: t('Model Name'), + meta: { mobileTitle: true }, cell: ({ row }) => { const model = row.original + const name = row.getValue('model_name') as string const iconKey = model.icon || vendorMap[model.vendor_id || 0]?.icon || @@ -117,32 +118,21 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { const icon = getCompactModelIcon(iconKey) return ( -
- {icon} +
+
+ {icon} +
+
) }, - size: 70, - enableSorting: false, - }, - - // Model Name column - { - accessorKey: 'model_name', - header: t('Model Name'), - meta: { mobileTitle: true }, - cell: ({ row }) => { - const name = row.getValue('model_name') as string - return ( - - ) - }, + size: 260, minSize: 200, }, @@ -162,7 +152,6 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { const badge = ( [] { | 'info' } size='sm' - className='-ml-1.5' - /> + className='-ml-1.5 max-w-none shrink-0' + > + {label} + ) // Show tooltip with matched models for non-exact rules @@ -182,14 +173,16 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { model.matched_models && model.matched_models.length > 0 ) { - const matchedBadges = model.matched_models.map((m, idx) => ( - + const matchedBadges = model.matched_models.map((m) => ( + )) return ( - }> + } + > {badge} [] { return badge }, - size: 140, + size: 100, enableSorting: false, }, @@ -221,12 +214,13 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { return ( + className='-ml-1.5 max-w-none shrink-0' + > + {config.label} + ) }, filterFn: (row, id, value) => { @@ -236,7 +230,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { if (value.includes('disabled')) return status !== 1 return false }, - size: 120, + size: 110, + minSize: 110, enableSorting: false, }, @@ -262,7 +257,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { if (!value || value.length === 0 || value.includes('all')) return true return value.includes(String(row.getValue(id))) }, - size: 150, + size: 130, enableSorting: false, }, @@ -293,13 +288,13 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { const tagArray = parseModelTags(tags) return ( ( - + items={tagArray.map((tag) => ( + ))} /> ) }, - size: 150, + size: 100, enableSorting: false, }, @@ -313,13 +308,14 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { const endpointArray = formatEndpointsDisplay(endpoints) return ( ( - + max={3} + items={endpointArray.map((ep) => ( + ))} /> ) }, - size: 150, + size: 200, enableSorting: false, }, @@ -337,9 +333,9 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { }> return ( ( + items={(channels ?? []).map((c) => ( [] { const groups = row.getValue('enable_groups') as string[] return ( ( ))} /> ) }, - size: 150, + size: 200, enableSorting: false, }, @@ -380,11 +377,11 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { const quotaTypes = row.getValue('quota_types') as number[] return ( { + items={(quotaTypes ?? []).map((qt) => { const config = QUOTA_TYPE_CONFIG[qt] return ( [] { const syncOfficial = row.getValue('sync_official') as number return ( + className='-ml-1.5 max-w-none shrink-0' + > + {syncOfficial === 1 ? t('Official Sync') : t('No Sync')} + ) }, filterFn: (row, id, value) => { @@ -429,7 +427,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { if (value.includes('no')) return syncOfficial !== 1 return false }, - size: 120, + size: 100, enableSorting: false, }, @@ -441,12 +439,12 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { cell: ({ row }) => { const timestamp = row.getValue('created_time') as number return ( -
+
{formatTimestampToDate(timestamp)}
) }, - size: 180, + size: 140, }, // Updated Time column @@ -457,12 +455,12 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { cell: ({ row }) => { const timestamp = row.getValue('updated_time') as number return ( -
+
{formatTimestampToDate(timestamp)}
) }, - size: 180, + size: 140, }, // Actions column diff --git a/web/default/src/features/models/components/models-table.tsx b/web/default/src/features/models/components/models-table.tsx index ceff4039..9a2a192b 100644 --- a/web/default/src/features/models/components/models-table.tsx +++ b/web/default/src/features/models/components/models-table.tsx @@ -94,9 +94,6 @@ export function ModelsTable() { })) }, [vendors]) - // Determine whether to use search or regular list API - const shouldSearch = Boolean(globalFilter?.trim()) - // Apply selected vendor from context or filter const activeVendorFilter = selectedVendor || @@ -104,55 +101,50 @@ export function ModelsTable() { ? vendorFilter[0] : undefined) + const statusFilterValue = + statusFilter.length > 0 && !statusFilter.includes('all') + ? statusFilter[0] + : undefined + const syncFilterValue = + syncFilter.length > 0 && !syncFilter.includes('all') + ? syncFilter[0] + : undefined + + // Use search API whenever any filter is active so status/sync are applied server-side + const shouldSearch = Boolean( + globalFilter?.trim() || + activeVendorFilter || + statusFilterValue || + syncFilterValue + ) + // Fetch models data // eslint-disable-next-line @tanstack/query/exhaustive-deps const { data, isLoading, isFetching } = useQuery({ queryKey: modelsQueryKeys.list({ keyword: globalFilter, vendor: activeVendorFilter, - status: - statusFilter.length > 0 && !statusFilter.includes('all') - ? statusFilter[0] - : undefined, - sync_official: - syncFilter.length > 0 && !syncFilter.includes('all') - ? syncFilter[0] - : undefined, + status: statusFilterValue, + sync_official: syncFilterValue, p: pagination.pageIndex + 1, page_size: pagination.pageSize, }), queryFn: async () => { - if (shouldSearch || activeVendorFilter) { + if (shouldSearch) { return searchModels({ keyword: globalFilter, vendor: activeVendorFilter, - status: - statusFilter.length > 0 && !statusFilter.includes('all') - ? statusFilter[0] - : undefined, - sync_official: - syncFilter.length > 0 && !syncFilter.includes('all') - ? syncFilter[0] - : undefined, - p: pagination.pageIndex + 1, - page_size: pagination.pageSize, - }) - } else { - return getModels({ - status: - statusFilter.length > 0 && !statusFilter.includes('all') - ? statusFilter[0] - : undefined, - sync_official: - syncFilter.length > 0 && !syncFilter.includes('all') - ? syncFilter[0] - : undefined, + status: statusFilterValue, + sync_official: syncFilterValue, p: pagination.pageIndex + 1, page_size: pagination.pageSize, }) } + return getModels({ + p: pagination.pageIndex + 1, + page_size: pagination.pageSize, + }) }, - placeholderData: (previousData) => previousData, }) const models = data?.data?.items || []