Просмотр исходного кода

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

yyhhyyyyyy 3 дней назад
Родитель
Сommit
dc8deb0c24

+ 6 - 9
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,

+ 71 - 19
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
 	}

+ 40 - 2
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<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,

+ 14 - 0
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
 }

+ 1 - 1
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...",

+ 1 - 1
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...": "このベンダーを説明...",

+ 1 - 1
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...": "Опишите этого поставщика...",

+ 1 - 1
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...",

+ 1 - 1
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...": "描述此供应商...",