Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
7a2b9d86
authored
Jul 11, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: enhance model search functionality with status and sync filters
parent
92d3c9d1
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
133 additions
and
91 deletions
+133
-91
controller/model_meta.go
+14
-5
model/model_meta.go
+46
-3
web/default/src/features/models/components/models-columns.tsx
+50
-52
web/default/src/features/models/components/models-table.tsx
+23
-31
No files found.
controller/model_meta.go
View file @
7a2b9d86
...
...
@@ -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 获取单条模型信息
...
...
model/model_meta.go
View file @
7a2b9d86
...
...
@@ -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
}
}
web/default/src/features/models/components/models-columns.tsx
View file @
7a2b9d86
...
...
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
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<Model>[] {
const
id
=
row
.
getValue
(
'id'
)
as
number
return
<
TableId
value=
{
id
}
/>
},
size
:
80
,
size
:
64
,
},
//
Icon column
//
Model Name column (with model icon)
{
accessorKey
:
'
icon
'
,
header
:
t
(
'
Icon
'
),
meta
:
{
mobile
Hidden
:
true
},
accessorKey
:
'
model_name
'
,
header
:
t
(
'
Model Name
'
),
meta
:
{
mobile
Title
:
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,23 +118,10 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const
icon
=
getCompactModelIcon
(
iconKey
)
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'
>
<
div
className=
'flex size-5 shrink-0 items-center justify-center overflow-hidden'
>
{
icon
}
</
div
>
)
},
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
(
<
StatusBadge
label=
{
name
}
variant=
'neutral'
...
...
@@ -141,8 +129,10 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
size=
'sm'
className=
'-ml-1.5 font-mono'
/>
</
div
>
)
},
size
:
260
,
minSize
:
200
,
},
...
...
@@ -162,7 +152,6 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const
badge
=
(
<
StatusBadge
label=
{
label
}
variant=
{
(
config
.
color
===
'error'
?
'danger'
:
config
.
color
)
as
|
'neutral'
...
...
@@ -172,8 +161,10 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
'info'
}
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
...
...
@@ -182,14 +173,16 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
model
.
matched_models
&&
model
.
matched_models
.
length
>
0
)
{
const
matchedBadges
=
model
.
matched_models
.
map
((
m
,
idx
)
=>
(
<
StatusBadge
key=
{
idx
}
label=
{
m
}
autoColor=
{
m
}
size=
'sm'
/>
const
matchedBadges
=
model
.
matched_models
.
map
((
m
)
=>
(
<
StatusBadge
key=
{
m
}
label=
{
m
}
autoColor=
{
m
}
size=
'sm'
/>
))
return
(
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
render=
{
<
div
className=
'-ml-1.5'
/>
}
>
<
TooltipTrigger
render=
{
<
div
className=
'inline-flex max-w-full min-w-0'
/>
}
>
{
badge
}
</
TooltipTrigger
>
<
TooltipContent
...
...
@@ -205,7 +198,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
return
badge
},
size
:
1
4
0
,
size
:
1
0
0
,
enableSorting
:
false
,
},
...
...
@@ -221,12 +214,13 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
return
(
<
StatusBadge
label=
{
config
.
label
}
variant=
{
config
.
variant
}
size=
'sm'
copyable=
{
false
}
className=
'-ml-1.5'
/>
className=
'-ml-1.5 max-w-none shrink-0'
>
{
config
.
label
}
</
StatusBadge
>
)
},
filterFn
:
(
row
,
id
,
value
)
=>
{
...
...
@@ -236,7 +230,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
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<Model>[] {
if
(
!
value
||
value
.
length
===
0
||
value
.
includes
(
'all'
))
return
true
return
value
.
includes
(
String
(
row
.
getValue
(
id
)))
},
size
:
1
5
0
,
size
:
1
3
0
,
enableSorting
:
false
,
},
...
...
@@ -293,13 +288,13 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const
tagArray
=
parseModelTags
(
tags
)
return
(
<
BadgeListCell
items=
{
tagArray
.
map
((
tag
,
idx
)
=>
(
<
StatusBadge
key=
{
idx
}
label=
{
tag
}
autoColor=
{
tag
}
size=
'sm'
/>
items=
{
tagArray
.
map
((
tag
)
=>
(
<
StatusBadge
key=
{
tag
}
label=
{
tag
}
autoColor=
{
tag
}
size=
'sm'
/>
))
}
/>
)
},
size
:
1
5
0
,
size
:
1
0
0
,
enableSorting
:
false
,
},
...
...
@@ -313,13 +308,14 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const
endpointArray
=
formatEndpointsDisplay
(
endpoints
)
return
(
<
BadgeListCell
items=
{
endpointArray
.
map
((
ep
,
idx
)
=>
(
<
StatusBadge
key=
{
idx
}
label=
{
ep
}
autoColor=
{
ep
}
size=
'sm'
/>
max=
{
3
}
items=
{
endpointArray
.
map
((
ep
)
=>
(
<
StatusBadge
key=
{
ep
}
label=
{
ep
}
autoColor=
{
ep
}
size=
'sm'
/>
))
}
/>
)
},
size
:
15
0
,
size
:
20
0
,
enableSorting
:
false
,
},
...
...
@@ -337,9 +333,9 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
}
>
return
(
<
BadgeListCell
items=
{
(
channels
??
[]).
map
((
c
,
idx
)
=>
(
items=
{
(
channels
??
[]).
map
((
c
)
=>
(
<
StatusBadge
key=
{
idx
}
key=
{
c
.
id
}
label=
{
`${c.name} (${c.type})`
}
autoColor=
{
c
.
name
}
size=
'sm'
...
...
@@ -361,13 +357,14 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const
groups
=
row
.
getValue
(
'enable_groups'
)
as
string
[]
return
(
<
BadgeListCell
max=
{
3
}
items=
{
(
groups
??
[]).
map
((
g
)
=>
(
<
GroupBadge
key=
{
g
}
group=
{
g
}
size=
'sm'
/>
))
}
/>
)
},
size
:
15
0
,
size
:
20
0
,
enableSorting
:
false
,
},
...
...
@@ -380,11 +377,11 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const
quotaTypes
=
row
.
getValue
(
'quota_types'
)
as
number
[]
return
(
<
BadgeListCell
items=
{
(
quotaTypes
??
[]).
map
((
qt
,
idx
)
=>
{
items=
{
(
quotaTypes
??
[]).
map
((
qt
)
=>
{
const
config
=
QUOTA_TYPE_CONFIG
[
qt
]
return
(
<
StatusBadge
key=
{
idx
}
key=
{
qt
}
label=
{
config
?.
label
||
String
(
qt
)
}
variant=
{
(
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
return
(
<
StatusBadge
label=
{
syncOfficial
===
1
?
t
(
'Official Sync'
)
:
t
(
'No Sync'
)
}
variant=
{
syncOfficial
===
1
?
'success'
:
'warning'
}
size=
'sm'
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
)
=>
{
...
...
@@ -429,7 +427,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
if
(
value
.
includes
(
'no'
))
return
syncOfficial
!==
1
return
false
},
size
:
1
2
0
,
size
:
1
0
0
,
enableSorting
:
false
,
},
...
...
@@ -441,12 +439,12 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
cell
:
({
row
})
=>
{
const
timestamp
=
row
.
getValue
(
'created_time'
)
as
number
return
(
<
div
className=
'
min-w-[140px] font-mono text-sm
'
>
<
div
className=
'
font-mono text-sm whitespace-nowrap
'
>
{
formatTimestampToDate
(
timestamp
)
}
</
div
>
)
},
size
:
1
8
0
,
size
:
1
4
0
,
},
// Updated Time column
...
...
@@ -457,12 +455,12 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
cell
:
({
row
})
=>
{
const
timestamp
=
row
.
getValue
(
'updated_time'
)
as
number
return
(
<
div
className=
'
min-w-[140px] font-mono text-sm
'
>
<
div
className=
'
font-mono text-sm whitespace-nowrap
'
>
{
formatTimestampToDate
(
timestamp
)
}
</
div
>
)
},
size
:
1
8
0
,
size
:
1
4
0
,
},
// Actions column
...
...
web/default/src/features/models/components/models-table.tsx
View file @
7a2b9d86
...
...
@@ -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
,
status
:
statusFilterValue
,
sync_official
:
syncFilterValue
,
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
,
page_size
:
pagination
.
pageSize
,
})
}
},
placeholderData
:
(
previousData
)
=>
previousData
,
})
const
models
=
data
?.
data
?.
items
||
[]
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment