feat: enhance model search functionality with status and sync filters

This commit is contained in:
CaIon
2026-07-11 22:36:09 +08:00
parent 92d3c9d18f
commit 7a2b9d86e8
4 changed files with 144 additions and 102 deletions
+14 -5
View File
@@ -17,15 +17,15 @@ import (
func GetAllModelsMeta(c *gin.Context) { func GetAllModelsMeta(c *gin.Context) {
pageInfo := common.GetPageQuery(c) 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 { if err != nil {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
// 批量填充附加字段,提升列表接口性能 // 批量填充附加字段,提升列表接口性能
enrichModels(modelsMeta) enrichModels(modelsMeta)
var total int64
model.DB.Model(&model.Model{}).Count(&total)
// 统计供应商计数(全部数据,不受分页影响) // 统计供应商计数(全部数据,不受分页影响)
vendorCounts, _ := model.GetVendorModelCounts() vendorCounts, _ := model.GetVendorModelCounts()
@@ -46,18 +46,27 @@ func SearchModelsMeta(c *gin.Context) {
keyword := c.Query("keyword") keyword := c.Query("keyword")
vendor := c.Query("vendor") vendor := c.Query("vendor")
status := c.Query("status")
syncOfficial := c.Query("sync_official")
pageInfo := common.GetPageQuery(c) 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 { if err != nil {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
// 批量填充附加字段,提升列表接口性能 // 批量填充附加字段,提升列表接口性能
enrichModels(modelsMeta) enrichModels(modelsMeta)
vendorCounts, _ := model.GetVendorModelCounts()
pageInfo.SetTotal(int(total)) pageInfo.SetTotal(int(total))
pageInfo.SetItems(modelsMeta) 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 获取单条模型信息 // GetModelMeta 根据 ID 获取单条模型信息
+46 -3
View File
@@ -105,8 +105,7 @@ func GetVendorModelCounts() (map[int64]int64, error) {
} }
func GetAllModels(offset int, limit int) ([]*Model, error) { func GetAllModels(offset int, limit int) ([]*Model, error) {
var models []*Model models, _, err := SearchModels("", "", "", "", offset, limit)
err := DB.Order("id DESC").Offset(offset).Limit(limit).Find(&models).Error
return models, err return models, err
} }
@@ -192,7 +191,7 @@ func GetPreferredModelOwnerChannelTypes(modelNames []string, groups []string) (m
return result, nil 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 var models []*Model
db := DB.Model(&Model{}) db := DB.Model(&Model{})
if keyword != "" { 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+"%") 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 var total int64
if err := db.Count(&total).Error; err != nil { if err := db.Count(&total).Error; err != nil {
return nil, 0, err return nil, 0, err
@@ -215,3 +220,41 @@ func SearchModels(keyword string, vendor string, offset int, limit int) ([]*Mode
} }
return models, total, nil 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
}
}
+58 -60
View File
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com 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 { useTranslation } from 'react-i18next'
import { BadgeCell, BadgeListCell } from '@/components/data-table' import { BadgeCell, BadgeListCell } from '@/components/data-table'
@@ -99,16 +99,17 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const id = row.getValue('id') as number const id = row.getValue('id') as number
return <TableId value={id} /> return <TableId value={id} />
}, },
size: 80, size: 64,
}, },
// Icon column // Model Name column (with model icon)
{ {
accessorKey: 'icon', accessorKey: 'model_name',
header: t('Icon'), header: t('Model Name'),
meta: { mobileHidden: true }, meta: { mobileTitle: true },
cell: ({ row }) => { cell: ({ row }) => {
const model = row.original const model = row.original
const name = row.getValue('model_name') as string
const iconKey = const iconKey =
model.icon || model.icon ||
vendorMap[model.vendor_id || 0]?.icon || vendorMap[model.vendor_id || 0]?.icon ||
@@ -117,32 +118,21 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const icon = getCompactModelIcon(iconKey) const icon = getCompactModelIcon(iconKey)
return ( return (
<div className='ms-1 flex size-5 items-center justify-center overflow-hidden'> <div className='flex max-w-full min-w-0 items-center gap-2'>
{icon} <div className='flex size-5 shrink-0 items-center justify-center overflow-hidden'>
{icon}
</div>
<StatusBadge
label={name}
variant='neutral'
copyText={name}
size='sm'
className='-ml-1.5 font-mono'
/>
</div> </div>
) )
}, },
size: 70, size: 260,
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 (
<StatusBadge
label={name}
variant='neutral'
copyText={name}
size='sm'
className='-ml-1.5 font-mono'
/>
)
},
minSize: 200, minSize: 200,
}, },
@@ -162,7 +152,6 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const badge = ( const badge = (
<StatusBadge <StatusBadge
label={label}
variant={ variant={
(config.color === 'error' ? 'danger' : config.color) as (config.color === 'error' ? 'danger' : config.color) as
| 'neutral' | 'neutral'
@@ -172,8 +161,10 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
| 'info' | 'info'
} }
size='sm' size='sm'
className='-ml-1.5' className='-ml-1.5 max-w-none shrink-0'
/> >
{label}
</StatusBadge>
) )
// Show tooltip with matched models for non-exact rules // Show tooltip with matched models for non-exact rules
@@ -182,14 +173,16 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
model.matched_models && model.matched_models &&
model.matched_models.length > 0 model.matched_models.length > 0
) { ) {
const matchedBadges = model.matched_models.map((m, idx) => ( const matchedBadges = model.matched_models.map((m) => (
<StatusBadge key={idx} label={m} autoColor={m} size='sm' /> <StatusBadge key={m} label={m} autoColor={m} size='sm' />
)) ))
return ( return (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger render={<div className='-ml-1.5' />}> <TooltipTrigger
render={<div className='inline-flex max-w-full min-w-0' />}
>
{badge} {badge}
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent
@@ -205,7 +198,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
return badge return badge
}, },
size: 140, size: 100,
enableSorting: false, enableSorting: false,
}, },
@@ -221,12 +214,13 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
return ( return (
<StatusBadge <StatusBadge
label={config.label}
variant={config.variant} variant={config.variant}
size='sm' size='sm'
copyable={false} copyable={false}
className='-ml-1.5' className='-ml-1.5 max-w-none shrink-0'
/> >
{config.label}
</StatusBadge>
) )
}, },
filterFn: (row, id, value) => { filterFn: (row, id, value) => {
@@ -236,7 +230,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
if (value.includes('disabled')) return status !== 1 if (value.includes('disabled')) return status !== 1
return false return false
}, },
size: 120, size: 110,
minSize: 110,
enableSorting: false, enableSorting: false,
}, },
@@ -262,7 +257,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
if (!value || value.length === 0 || value.includes('all')) return true if (!value || value.length === 0 || value.includes('all')) return true
return value.includes(String(row.getValue(id))) return value.includes(String(row.getValue(id)))
}, },
size: 150, size: 130,
enableSorting: false, enableSorting: false,
}, },
@@ -293,13 +288,13 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const tagArray = parseModelTags(tags) const tagArray = parseModelTags(tags)
return ( return (
<BadgeListCell <BadgeListCell
items={tagArray.map((tag, idx) => ( items={tagArray.map((tag) => (
<StatusBadge key={idx} label={tag} autoColor={tag} size='sm' /> <StatusBadge key={tag} label={tag} autoColor={tag} size='sm' />
))} ))}
/> />
) )
}, },
size: 150, size: 100,
enableSorting: false, enableSorting: false,
}, },
@@ -313,13 +308,14 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const endpointArray = formatEndpointsDisplay(endpoints) const endpointArray = formatEndpointsDisplay(endpoints)
return ( return (
<BadgeListCell <BadgeListCell
items={endpointArray.map((ep, idx) => ( max={3}
<StatusBadge key={idx} label={ep} autoColor={ep} size='sm' /> items={endpointArray.map((ep) => (
<StatusBadge key={ep} label={ep} autoColor={ep} size='sm' />
))} ))}
/> />
) )
}, },
size: 150, size: 200,
enableSorting: false, enableSorting: false,
}, },
@@ -337,9 +333,9 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
}> }>
return ( return (
<BadgeListCell <BadgeListCell
items={(channels ?? []).map((c, idx) => ( items={(channels ?? []).map((c) => (
<StatusBadge <StatusBadge
key={idx} key={c.id}
label={`${c.name} (${c.type})`} label={`${c.name} (${c.type})`}
autoColor={c.name} autoColor={c.name}
size='sm' size='sm'
@@ -361,13 +357,14 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const groups = row.getValue('enable_groups') as string[] const groups = row.getValue('enable_groups') as string[]
return ( return (
<BadgeListCell <BadgeListCell
max={3}
items={(groups ?? []).map((g) => ( items={(groups ?? []).map((g) => (
<GroupBadge key={g} group={g} size='sm' /> <GroupBadge key={g} group={g} size='sm' />
))} ))}
/> />
) )
}, },
size: 150, size: 200,
enableSorting: false, enableSorting: false,
}, },
@@ -380,11 +377,11 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const quotaTypes = row.getValue('quota_types') as number[] const quotaTypes = row.getValue('quota_types') as number[]
return ( return (
<BadgeListCell <BadgeListCell
items={(quotaTypes ?? []).map((qt, idx) => { items={(quotaTypes ?? []).map((qt) => {
const config = QUOTA_TYPE_CONFIG[qt] const config = QUOTA_TYPE_CONFIG[qt]
return ( return (
<StatusBadge <StatusBadge
key={idx} key={qt}
label={config?.label || String(qt)} label={config?.label || String(qt)}
variant={ variant={
(config?.color === 'error' ? 'danger' : config?.color) as (config?.color === 'error' ? 'danger' : config?.color) as
@@ -414,12 +411,13 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const syncOfficial = row.getValue('sync_official') as number const syncOfficial = row.getValue('sync_official') as number
return ( return (
<StatusBadge <StatusBadge
label={syncOfficial === 1 ? t('Official Sync') : t('No Sync')}
variant={syncOfficial === 1 ? 'success' : 'warning'} variant={syncOfficial === 1 ? 'success' : 'warning'}
size='sm' size='sm'
copyable={false} copyable={false}
className='-ml-1.5' className='-ml-1.5 max-w-none shrink-0'
/> >
{syncOfficial === 1 ? t('Official Sync') : t('No Sync')}
</StatusBadge>
) )
}, },
filterFn: (row, id, value) => { filterFn: (row, id, value) => {
@@ -429,7 +427,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
if (value.includes('no')) return syncOfficial !== 1 if (value.includes('no')) return syncOfficial !== 1
return false return false
}, },
size: 120, size: 100,
enableSorting: false, enableSorting: false,
}, },
@@ -441,12 +439,12 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
cell: ({ row }) => { cell: ({ row }) => {
const timestamp = row.getValue('created_time') as number const timestamp = row.getValue('created_time') as number
return ( return (
<div className='min-w-[140px] font-mono text-sm'> <div className='font-mono text-sm whitespace-nowrap'>
{formatTimestampToDate(timestamp)} {formatTimestampToDate(timestamp)}
</div> </div>
) )
}, },
size: 180, size: 140,
}, },
// Updated Time column // Updated Time column
@@ -457,12 +455,12 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
cell: ({ row }) => { cell: ({ row }) => {
const timestamp = row.getValue('updated_time') as number const timestamp = row.getValue('updated_time') as number
return ( return (
<div className='min-w-[140px] font-mono text-sm'> <div className='font-mono text-sm whitespace-nowrap'>
{formatTimestampToDate(timestamp)} {formatTimestampToDate(timestamp)}
</div> </div>
) )
}, },
size: 180, size: 140,
}, },
// Actions column // Actions column
+26 -34
View File
@@ -94,9 +94,6 @@ export function ModelsTable() {
})) }))
}, [vendors]) }, [vendors])
// Determine whether to use search or regular list API
const shouldSearch = Boolean(globalFilter?.trim())
// Apply selected vendor from context or filter // Apply selected vendor from context or filter
const activeVendorFilter = const activeVendorFilter =
selectedVendor || selectedVendor ||
@@ -104,55 +101,50 @@ export function ModelsTable() {
? vendorFilter[0] ? vendorFilter[0]
: undefined) : 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 // Fetch models data
// eslint-disable-next-line @tanstack/query/exhaustive-deps // eslint-disable-next-line @tanstack/query/exhaustive-deps
const { data, isLoading, isFetching } = useQuery({ const { data, isLoading, isFetching } = useQuery({
queryKey: modelsQueryKeys.list({ queryKey: modelsQueryKeys.list({
keyword: globalFilter, keyword: globalFilter,
vendor: activeVendorFilter, vendor: activeVendorFilter,
status: status: statusFilterValue,
statusFilter.length > 0 && !statusFilter.includes('all') sync_official: syncFilterValue,
? statusFilter[0]
: undefined,
sync_official:
syncFilter.length > 0 && !syncFilter.includes('all')
? syncFilter[0]
: undefined,
p: pagination.pageIndex + 1, p: pagination.pageIndex + 1,
page_size: pagination.pageSize, page_size: pagination.pageSize,
}), }),
queryFn: async () => { queryFn: async () => {
if (shouldSearch || activeVendorFilter) { if (shouldSearch) {
return searchModels({ return searchModels({
keyword: globalFilter, keyword: globalFilter,
vendor: activeVendorFilter, vendor: activeVendorFilter,
status: status: statusFilterValue,
statusFilter.length > 0 && !statusFilter.includes('all') sync_official: syncFilterValue,
? 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,
p: pagination.pageIndex + 1, p: pagination.pageIndex + 1,
page_size: pagination.pageSize, page_size: pagination.pageSize,
}) })
} }
return getModels({
p: pagination.pageIndex + 1,
page_size: pagination.pageSize,
})
}, },
placeholderData: (previousData) => previousData,
}) })
const models = data?.data?.items || [] const models = data?.data?.items || []