From dc8deb0c24919a78e129781a610617e93a18c9a2 Mon Sep 17 00:00:00 2001 From: yyhhyyyyyy Date: Wed, 6 May 2026 18:27:36 +0800 Subject: [PATCH] fix: enable channel table server-side sorting (#4600) --- controller/channel.go | 15 ++-- model/channel.go | 90 +++++++++++++++---- .../channels/components/channels-table.tsx | 42 ++++++++- web/default/src/features/channels/types.ts | 14 +++ web/default/src/i18n/locales/fr.json | 2 +- web/default/src/i18n/locales/ja.json | 2 +- web/default/src/i18n/locales/ru.json | 2 +- web/default/src/i18n/locales/vi.json | 2 +- web/default/src/i18n/locales/zh.json | 2 +- 9 files changed, 136 insertions(+), 35 deletions(-) diff --git a/controller/channel.go b/controller/channel.go index b0dd2286..eb95ccc3 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -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, diff --git a/model/channel.go b/model/channel.go index f256b54c..dce78c51 100644 --- a/model/channel.go +++ b/model/channel.go @@ -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 } diff --git a/web/default/src/features/channels/components/channels-table.tsx b/web/default/src/features/channels/components/channels-table.tsx index 24a4bc63..cbc36599 100644 --- a/web/default/src/features/channels/components/channels-table.tsx +++ b/web/default/src/features/channels/components/channels-table.tsx @@ -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([ + '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 = (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) => !isTagAggregateRow(row.original), onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, + onSortingChange: handleSortingChange, onColumnFiltersChange, onColumnVisibilityChange: setColumnVisibility, onPaginationChange, diff --git a/web/default/src/features/channels/types.ts b/web/default/src/features/channels/types.ts index 163d39c4..fddf26ad 100644 --- a/web/default/src/features/channels/types.ts +++ b/web/default/src/features/channels/types.ts @@ -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 } diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index 2bfe53fe..fcef6a7c 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -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...", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index 2decc831..742478a0 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -1108,7 +1108,7 @@ "Deployment Region *": "デプロイリージョン *", "Deployment requested": "デプロイメントが要求されました", "Deployments": "デプロイ", - "Desc": "説明", + "Desc": "降順", "Describe": "説明", "Describe this model...": "このモデルを説明...", "Describe this vendor...": "このベンダーを説明...", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index 24b80199..d2640d71 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -1108,7 +1108,7 @@ "Deployment Region *": "Регион развертывания *", "Deployment requested": "Развертывание запрошено", "Deployments": "Развертывания", - "Desc": "Описание", + "Desc": "По убыванию", "Describe": "Описание", "Describe this model...": "Опишите эту модель...", "Describe this vendor...": "Опишите этого поставщика...", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index dfb1732c..d04623d3 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -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...", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 4306ef5e..b26ceec8 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -1108,7 +1108,7 @@ "Deployment Region *": "部署区域 *", "Deployment requested": "部署已请求", "Deployments": "部署", - "Desc": "描述", + "Desc": "降序", "Describe": "图生文", "Describe this model...": "描述此模型...", "Describe this vendor...": "描述此供应商...",