fix: enable channel table server-side sorting (#4600)

This commit is contained in:
yyhhyyyyyy 2026-05-06 18:27:36 +08:00 committed by GitHub
parent f8cf9c57c4
commit dc8deb0c24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 136 additions and 35 deletions

View File

@ -72,6 +72,7 @@ func GetAllChannels(c *gin.Context) {
pageInfo := common.GetPageQuery(c)
channelData := make([]*model.Channel, 0)
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
sortOptions := model.NewChannelSortOptions(c.Query("sort_by"), c.Query("sort_order"), idSort)
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
statusParam := c.Query("status")
// statusFilter: -1 all, 1 enabled, 0 disabled (include auto & manual)
@ -98,7 +99,7 @@ func GetAllChannels(c *gin.Context) {
if tag == nil || *tag == "" {
continue
}
tagChannels, err := model.GetChannelsByTag(*tag, idSort, false)
tagChannels, err := model.GetChannelsByTag(*tag, idSort, false, sortOptions)
if err != nil {
continue
}
@ -131,12 +132,7 @@ func GetAllChannels(c *gin.Context) {
baseQuery.Count(&total)
order := "priority desc"
if idSort {
order = "id desc"
}
err := baseQuery.Order(order).Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Omit("key").Find(&channelData).Error
err := sortOptions.Apply(baseQuery).Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Omit("key").Find(&channelData).Error
if err != nil {
common.SysError("failed to get channels: " + err.Error())
c.JSON(http.StatusOK, gin.H{"success": false, "message": "获取渠道列表失败,请稍后重试"})
@ -252,6 +248,7 @@ func SearchChannels(c *gin.Context) {
statusParam := c.Query("status")
statusFilter := parseStatusFilter(statusParam)
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
sortOptions := model.NewChannelSortOptions(c.Query("sort_by"), c.Query("sort_order"), idSort)
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
channelData := make([]*model.Channel, 0)
if enableTagMode {
@ -265,14 +262,14 @@ func SearchChannels(c *gin.Context) {
}
for _, tag := range tags {
if tag != nil && *tag != "" {
tagChannel, err := model.GetChannelsByTag(*tag, idSort, false)
tagChannel, err := model.GetChannelsByTag(*tag, idSort, false, sortOptions)
if err == nil {
channelData = append(channelData, tagChannel...)
}
}
}
} else {
channels, err := model.SearchChannels(keyword, group, modelKeyword, idSort)
channels, err := model.SearchChannels(keyword, group, modelKeyword, idSort, sortOptions)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,

View File

@ -16,6 +16,7 @@ import (
"github.com/samber/lo"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Channel struct {
@ -67,6 +68,66 @@ type ChannelInfo struct {
MultiKeyMode constant.MultiKeyMode `json:"multi_key_mode"`
}
type ChannelSortOptions struct {
SortBy string
SortOrder string
IDSort bool
}
var channelSortColumns = map[string]string{
"id": "id",
"name": "name",
"priority": "priority",
"balance": "balance",
"response_time": "response_time",
"test_time": "test_time",
}
func NewChannelSortOptions(sortBy string, sortOrder string, idSort bool) ChannelSortOptions {
normalizedSortBy := strings.ToLower(strings.TrimSpace(sortBy))
normalizedSortOrder := strings.ToLower(strings.TrimSpace(sortOrder))
if _, ok := channelSortColumns[normalizedSortBy]; !ok {
normalizedSortBy = ""
normalizedSortOrder = ""
} else if normalizedSortOrder != "asc" {
normalizedSortOrder = "desc"
}
return ChannelSortOptions{
SortBy: normalizedSortBy,
SortOrder: normalizedSortOrder,
IDSort: idSort,
}
}
func (options ChannelSortOptions) Apply(query *gorm.DB) *gorm.DB {
if columnName, ok := channelSortColumns[options.SortBy]; ok {
return query.Order(clause.OrderByColumn{
Column: clause.Column{Name: columnName},
Desc: options.SortOrder != "asc",
})
}
if options.IDSort {
return query.Order(clause.OrderByColumn{
Column: clause.Column{Name: "id"},
Desc: true,
})
}
return query.Order(clause.OrderByColumn{
Column: clause.Column{Name: "priority"},
Desc: true,
})
}
func resolveChannelSortOptions(idSort bool, sortOptions []ChannelSortOptions) ChannelSortOptions {
if len(sortOptions) == 0 {
return NewChannelSortOptions("", "", idSort)
}
options := sortOptions[0]
options.IDSort = options.IDSort || idSort
return options
}
// Value implements driver.Valuer interface
func (c ChannelInfo) Value() (driver.Value, error) {
return common.Marshal(&c)
@ -260,28 +321,22 @@ func (channel *Channel) SaveWithoutKey() error {
return DB.Omit("key").Save(channel).Error
}
func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Channel, error) {
func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool, sortOptions ...ChannelSortOptions) ([]*Channel, error) {
var channels []*Channel
var err error
order := "priority desc"
if idSort {
order = "id desc"
}
order := resolveChannelSortOptions(idSort, sortOptions)
if selectAll {
err = DB.Order(order).Find(&channels).Error
err = order.Apply(DB).Find(&channels).Error
} else {
err = DB.Order(order).Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error
err = order.Apply(DB).Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error
}
return channels, err
}
func GetChannelsByTag(tag string, idSort bool, selectAll bool) ([]*Channel, error) {
func GetChannelsByTag(tag string, idSort bool, selectAll bool, sortOptions ...ChannelSortOptions) ([]*Channel, error) {
var channels []*Channel
order := "priority desc"
if idSort {
order = "id desc"
}
query := DB.Where("tag = ?", tag).Order(order)
order := resolveChannelSortOptions(idSort, sortOptions)
query := order.Apply(DB.Where("tag = ?", tag))
if !selectAll {
query = query.Omit("key")
}
@ -289,7 +344,7 @@ func GetChannelsByTag(tag string, idSort bool, selectAll bool) ([]*Channel, erro
return channels, err
}
func SearchChannels(keyword string, group string, model string, idSort bool) ([]*Channel, error) {
func SearchChannels(keyword string, group string, model string, idSort bool, sortOptions ...ChannelSortOptions) ([]*Channel, error) {
var channels []*Channel
modelsCol := "`models`"
@ -304,10 +359,7 @@ func SearchChannels(keyword string, group string, model string, idSort bool) ([]
baseURLCol = `"base_url"`
}
order := "priority desc"
if idSort {
order = "id desc"
}
order := resolveChannelSortOptions(idSort, sortOptions)
// 构造基础查询
baseQuery := DB.Model(&Channel{}).Omit("key")
@ -331,7 +383,7 @@ func SearchChannels(keyword string, group string, model string, idSort bool) ([]
}
// 执行查询
err := baseQuery.Where(whereClause, args...).Order(order).Find(&channels).Error
err := order.Apply(baseQuery.Where(whereClause, args...)).Find(&channels).Error
if err != nil {
return nil, err
}

View File

@ -5,6 +5,7 @@ import {
getCoreRowModel,
useReactTable,
getExpandedRowModel,
type OnChangeFn,
type SortingState,
type VisibilityState,
type ExpandedState,
@ -33,13 +34,22 @@ import {
getChannelTypeIcon,
getChannelTypeLabel,
} from '../lib'
import type { Channel } from '../types'
import type { Channel, ChannelSortBy } from '../types'
import { useChannelsColumns } from './channels-columns'
import { useChannels } from './channels-provider'
import { DataTableBulkActions } from './data-table-bulk-actions'
const route = getRouteApi('/_authenticated/channels/')
const CHANNEL_SORTABLE_COLUMNS = new Set<ChannelSortBy>([
'id',
'name',
'priority',
'balance',
'response_time',
'test_time',
])
function isDisabledChannelRow(channel: Channel) {
return (
!isTagAggregateRow(channel) && channel.status !== CHANNEL_STATUS.ENABLED
@ -121,6 +131,31 @@ export function ChannelsTable() {
// Determine whether to use search or regular list API
const shouldSearch = Boolean(globalFilter?.trim() || modelFilter.trim())
const sortParams = useMemo(() => {
const activeSort = sorting[0]
if (
!activeSort ||
!CHANNEL_SORTABLE_COLUMNS.has(activeSort.id as ChannelSortBy)
) {
return {}
}
return {
sort_by: activeSort.id as ChannelSortBy,
sort_order: activeSort.desc ? 'desc' : 'asc',
} as const
}, [sorting])
const handleSortingChange: OnChangeFn<SortingState> = (updater) => {
setSorting((previous) => {
const next = typeof updater === 'function' ? updater(previous) : updater
if (pagination.pageIndex > 0) {
onPaginationChange({ ...pagination, pageIndex: 0 })
}
return next
})
}
// Fetch groups for filter
const { data: groupsData } = useQuery({
queryKey: ['groups'],
@ -156,6 +191,7 @@ export function ChannelsTable() {
: undefined,
tag_mode: enableTagMode,
id_sort: idSort,
...sortParams,
p: pagination.pageIndex + 1,
page_size: pagination.pageSize,
}),
@ -178,6 +214,7 @@ export function ChannelsTable() {
: undefined,
tag_mode: enableTagMode,
id_sort: idSort,
...sortParams,
p: pagination.pageIndex + 1,
page_size: pagination.pageSize,
})
@ -197,6 +234,7 @@ export function ChannelsTable() {
: undefined,
tag_mode: enableTagMode,
id_sort: idSort,
...sortParams,
p: pagination.pageIndex + 1,
page_size: pagination.pageSize,
})
@ -238,7 +276,7 @@ export function ChannelsTable() {
},
enableRowSelection: (row: Row<Channel>) => !isTagAggregateRow(row.original),
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onSortingChange: handleSortingChange,
onColumnFiltersChange,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange,

View File

@ -194,6 +194,16 @@ export interface MultiKeyStatusResponse {
// API Request Parameters
// ============================================================================
export type ChannelSortBy =
| 'id'
| 'name'
| 'priority'
| 'balance'
| 'response_time'
| 'test_time'
export type ChannelSortOrder = 'asc' | 'desc'
export interface GetChannelsParams {
p?: number
page_size?: number
@ -202,6 +212,8 @@ export interface GetChannelsParams {
group?: string
id_sort?: boolean
tag_mode?: boolean
sort_by?: ChannelSortBy
sort_order?: ChannelSortOrder
}
export interface SearchChannelsParams {
@ -212,6 +224,8 @@ export interface SearchChannelsParams {
type?: number
id_sort?: boolean
tag_mode?: boolean
sort_by?: ChannelSortBy
sort_order?: ChannelSortOrder
p?: number
page_size?: number
}

View File

@ -1108,7 +1108,7 @@
"Deployment Region *": "Région de déploiement *",
"Deployment requested": "Déploiement demandé",
"Deployments": "Déploiements",
"Desc": "Description",
"Desc": "Desc.",
"Describe": "Décrire",
"Describe this model...": "Décrire ce modèle...",
"Describe this vendor...": "Décrire ce fournisseur...",

View File

@ -1108,7 +1108,7 @@
"Deployment Region *": "デプロイリージョン *",
"Deployment requested": "デプロイメントが要求されました",
"Deployments": "デプロイ",
"Desc": "説明",
"Desc": "降順",
"Describe": "説明",
"Describe this model...": "このモデルを説明...",
"Describe this vendor...": "このベンダーを説明...",

View File

@ -1108,7 +1108,7 @@
"Deployment Region *": "Регион развертывания *",
"Deployment requested": "Развертывание запрошено",
"Deployments": "Развертывания",
"Desc": "Описание",
"Desc": "По убыванию",
"Describe": "Описание",
"Describe this model...": "Опишите эту модель...",
"Describe this vendor...": "Опишите этого поставщика...",

View File

@ -1108,7 +1108,7 @@
"Deployment Region *": "Khu vực triển khai *",
"Deployment requested": "Yêu cầu triển khai",
"Deployments": "Triển khai",
"Desc": "Mô tả",
"Desc": "Giảm dần",
"Describe": "Mô tả",
"Describe this model...": "Mô tả mô hình này...",
"Describe this vendor...": "Mô tả nhà cung cấp này...",

View File

@ -1108,7 +1108,7 @@
"Deployment Region *": "部署区域 *",
"Deployment requested": "部署已请求",
"Deployments": "部署",
"Desc": "描述",
"Desc": "降序",
"Describe": "图生文",
"Describe this model...": "描述此模型...",
"Describe this vendor...": "描述此供应商...",