fix: enable channel table server-side sorting (#4600)
This commit is contained in:
@@ -72,6 +72,7 @@ func GetAllChannels(c *gin.Context) {
|
|||||||
pageInfo := common.GetPageQuery(c)
|
pageInfo := common.GetPageQuery(c)
|
||||||
channelData := make([]*model.Channel, 0)
|
channelData := make([]*model.Channel, 0)
|
||||||
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
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"))
|
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
|
||||||
statusParam := c.Query("status")
|
statusParam := c.Query("status")
|
||||||
// statusFilter: -1 all, 1 enabled, 0 disabled (include auto & manual)
|
// statusFilter: -1 all, 1 enabled, 0 disabled (include auto & manual)
|
||||||
@@ -98,7 +99,7 @@ func GetAllChannels(c *gin.Context) {
|
|||||||
if tag == nil || *tag == "" {
|
if tag == nil || *tag == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tagChannels, err := model.GetChannelsByTag(*tag, idSort, false)
|
tagChannels, err := model.GetChannelsByTag(*tag, idSort, false, sortOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -131,12 +132,7 @@ func GetAllChannels(c *gin.Context) {
|
|||||||
|
|
||||||
baseQuery.Count(&total)
|
baseQuery.Count(&total)
|
||||||
|
|
||||||
order := "priority desc"
|
err := sortOptions.Apply(baseQuery).Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Omit("key").Find(&channelData).Error
|
||||||
if idSort {
|
|
||||||
order = "id desc"
|
|
||||||
}
|
|
||||||
|
|
||||||
err := baseQuery.Order(order).Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Omit("key").Find(&channelData).Error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("failed to get channels: " + err.Error())
|
common.SysError("failed to get channels: " + err.Error())
|
||||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "获取渠道列表失败,请稍后重试"})
|
c.JSON(http.StatusOK, gin.H{"success": false, "message": "获取渠道列表失败,请稍后重试"})
|
||||||
@@ -252,6 +248,7 @@ func SearchChannels(c *gin.Context) {
|
|||||||
statusParam := c.Query("status")
|
statusParam := c.Query("status")
|
||||||
statusFilter := parseStatusFilter(statusParam)
|
statusFilter := parseStatusFilter(statusParam)
|
||||||
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
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"))
|
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
|
||||||
channelData := make([]*model.Channel, 0)
|
channelData := make([]*model.Channel, 0)
|
||||||
if enableTagMode {
|
if enableTagMode {
|
||||||
@@ -265,14 +262,14 @@ func SearchChannels(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
if tag != nil && *tag != "" {
|
if tag != nil && *tag != "" {
|
||||||
tagChannel, err := model.GetChannelsByTag(*tag, idSort, false)
|
tagChannel, err := model.GetChannelsByTag(*tag, idSort, false, sortOptions)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
channelData = append(channelData, tagChannel...)
|
channelData = append(channelData, tagChannel...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channels, err := model.SearchChannels(keyword, group, modelKeyword, idSort)
|
channels, err := model.SearchChannels(keyword, group, modelKeyword, idSort, sortOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
|
|||||||
+71
-19
@@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
@@ -67,6 +68,66 @@ type ChannelInfo struct {
|
|||||||
MultiKeyMode constant.MultiKeyMode `json:"multi_key_mode"`
|
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
|
// Value implements driver.Valuer interface
|
||||||
func (c ChannelInfo) Value() (driver.Value, error) {
|
func (c ChannelInfo) Value() (driver.Value, error) {
|
||||||
return common.Marshal(&c)
|
return common.Marshal(&c)
|
||||||
@@ -260,28 +321,22 @@ func (channel *Channel) SaveWithoutKey() error {
|
|||||||
return DB.Omit("key").Save(channel).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 channels []*Channel
|
||||||
var err error
|
var err error
|
||||||
order := "priority desc"
|
order := resolveChannelSortOptions(idSort, sortOptions)
|
||||||
if idSort {
|
|
||||||
order = "id desc"
|
|
||||||
}
|
|
||||||
if selectAll {
|
if selectAll {
|
||||||
err = DB.Order(order).Find(&channels).Error
|
err = order.Apply(DB).Find(&channels).Error
|
||||||
} else {
|
} 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
|
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
|
var channels []*Channel
|
||||||
order := "priority desc"
|
order := resolveChannelSortOptions(idSort, sortOptions)
|
||||||
if idSort {
|
query := order.Apply(DB.Where("tag = ?", tag))
|
||||||
order = "id desc"
|
|
||||||
}
|
|
||||||
query := DB.Where("tag = ?", tag).Order(order)
|
|
||||||
if !selectAll {
|
if !selectAll {
|
||||||
query = query.Omit("key")
|
query = query.Omit("key")
|
||||||
}
|
}
|
||||||
@@ -289,7 +344,7 @@ func GetChannelsByTag(tag string, idSort bool, selectAll bool) ([]*Channel, erro
|
|||||||
return channels, err
|
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
|
var channels []*Channel
|
||||||
modelsCol := "`models`"
|
modelsCol := "`models`"
|
||||||
|
|
||||||
@@ -304,10 +359,7 @@ func SearchChannels(keyword string, group string, model string, idSort bool) ([]
|
|||||||
baseURLCol = `"base_url"`
|
baseURLCol = `"base_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
order := "priority desc"
|
order := resolveChannelSortOptions(idSort, sortOptions)
|
||||||
if idSort {
|
|
||||||
order = "id desc"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构造基础查询
|
// 构造基础查询
|
||||||
baseQuery := DB.Model(&Channel{}).Omit("key")
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
getExpandedRowModel,
|
getExpandedRowModel,
|
||||||
|
type OnChangeFn,
|
||||||
type SortingState,
|
type SortingState,
|
||||||
type VisibilityState,
|
type VisibilityState,
|
||||||
type ExpandedState,
|
type ExpandedState,
|
||||||
@@ -33,13 +34,22 @@ import {
|
|||||||
getChannelTypeIcon,
|
getChannelTypeIcon,
|
||||||
getChannelTypeLabel,
|
getChannelTypeLabel,
|
||||||
} from '../lib'
|
} from '../lib'
|
||||||
import type { Channel } from '../types'
|
import type { Channel, ChannelSortBy } from '../types'
|
||||||
import { useChannelsColumns } from './channels-columns'
|
import { useChannelsColumns } from './channels-columns'
|
||||||
import { useChannels } from './channels-provider'
|
import { useChannels } from './channels-provider'
|
||||||
import { DataTableBulkActions } from './data-table-bulk-actions'
|
import { DataTableBulkActions } from './data-table-bulk-actions'
|
||||||
|
|
||||||
const route = getRouteApi('/_authenticated/channels/')
|
const route = getRouteApi('/_authenticated/channels/')
|
||||||
|
|
||||||
|
const CHANNEL_SORTABLE_COLUMNS = new Set<ChannelSortBy>([
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'priority',
|
||||||
|
'balance',
|
||||||
|
'response_time',
|
||||||
|
'test_time',
|
||||||
|
])
|
||||||
|
|
||||||
function isDisabledChannelRow(channel: Channel) {
|
function isDisabledChannelRow(channel: Channel) {
|
||||||
return (
|
return (
|
||||||
!isTagAggregateRow(channel) && channel.status !== CHANNEL_STATUS.ENABLED
|
!isTagAggregateRow(channel) && channel.status !== CHANNEL_STATUS.ENABLED
|
||||||
@@ -121,6 +131,31 @@ export function ChannelsTable() {
|
|||||||
// Determine whether to use search or regular list API
|
// Determine whether to use search or regular list API
|
||||||
const shouldSearch = Boolean(globalFilter?.trim() || modelFilter.trim())
|
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
|
// Fetch groups for filter
|
||||||
const { data: groupsData } = useQuery({
|
const { data: groupsData } = useQuery({
|
||||||
queryKey: ['groups'],
|
queryKey: ['groups'],
|
||||||
@@ -156,6 +191,7 @@ export function ChannelsTable() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
tag_mode: enableTagMode,
|
tag_mode: enableTagMode,
|
||||||
id_sort: idSort,
|
id_sort: idSort,
|
||||||
|
...sortParams,
|
||||||
p: pagination.pageIndex + 1,
|
p: pagination.pageIndex + 1,
|
||||||
page_size: pagination.pageSize,
|
page_size: pagination.pageSize,
|
||||||
}),
|
}),
|
||||||
@@ -178,6 +214,7 @@ export function ChannelsTable() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
tag_mode: enableTagMode,
|
tag_mode: enableTagMode,
|
||||||
id_sort: idSort,
|
id_sort: idSort,
|
||||||
|
...sortParams,
|
||||||
p: pagination.pageIndex + 1,
|
p: pagination.pageIndex + 1,
|
||||||
page_size: pagination.pageSize,
|
page_size: pagination.pageSize,
|
||||||
})
|
})
|
||||||
@@ -197,6 +234,7 @@ export function ChannelsTable() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
tag_mode: enableTagMode,
|
tag_mode: enableTagMode,
|
||||||
id_sort: idSort,
|
id_sort: idSort,
|
||||||
|
...sortParams,
|
||||||
p: pagination.pageIndex + 1,
|
p: pagination.pageIndex + 1,
|
||||||
page_size: pagination.pageSize,
|
page_size: pagination.pageSize,
|
||||||
})
|
})
|
||||||
@@ -238,7 +276,7 @@ export function ChannelsTable() {
|
|||||||
},
|
},
|
||||||
enableRowSelection: (row: Row<Channel>) => !isTagAggregateRow(row.original),
|
enableRowSelection: (row: Row<Channel>) => !isTagAggregateRow(row.original),
|
||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
onSortingChange: setSorting,
|
onSortingChange: handleSortingChange,
|
||||||
onColumnFiltersChange,
|
onColumnFiltersChange,
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
onPaginationChange,
|
onPaginationChange,
|
||||||
|
|||||||
+14
@@ -194,6 +194,16 @@ export interface MultiKeyStatusResponse {
|
|||||||
// API Request Parameters
|
// API Request Parameters
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
export type ChannelSortBy =
|
||||||
|
| 'id'
|
||||||
|
| 'name'
|
||||||
|
| 'priority'
|
||||||
|
| 'balance'
|
||||||
|
| 'response_time'
|
||||||
|
| 'test_time'
|
||||||
|
|
||||||
|
export type ChannelSortOrder = 'asc' | 'desc'
|
||||||
|
|
||||||
export interface GetChannelsParams {
|
export interface GetChannelsParams {
|
||||||
p?: number
|
p?: number
|
||||||
page_size?: number
|
page_size?: number
|
||||||
@@ -202,6 +212,8 @@ export interface GetChannelsParams {
|
|||||||
group?: string
|
group?: string
|
||||||
id_sort?: boolean
|
id_sort?: boolean
|
||||||
tag_mode?: boolean
|
tag_mode?: boolean
|
||||||
|
sort_by?: ChannelSortBy
|
||||||
|
sort_order?: ChannelSortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchChannelsParams {
|
export interface SearchChannelsParams {
|
||||||
@@ -212,6 +224,8 @@ export interface SearchChannelsParams {
|
|||||||
type?: number
|
type?: number
|
||||||
id_sort?: boolean
|
id_sort?: boolean
|
||||||
tag_mode?: boolean
|
tag_mode?: boolean
|
||||||
|
sort_by?: ChannelSortBy
|
||||||
|
sort_order?: ChannelSortOrder
|
||||||
p?: number
|
p?: number
|
||||||
page_size?: number
|
page_size?: number
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1108,7 +1108,7 @@
|
|||||||
"Deployment Region *": "Région de déploiement *",
|
"Deployment Region *": "Région de déploiement *",
|
||||||
"Deployment requested": "Déploiement demandé",
|
"Deployment requested": "Déploiement demandé",
|
||||||
"Deployments": "Déploiements",
|
"Deployments": "Déploiements",
|
||||||
"Desc": "Description",
|
"Desc": "Desc.",
|
||||||
"Describe": "Décrire",
|
"Describe": "Décrire",
|
||||||
"Describe this model...": "Décrire ce modèle...",
|
"Describe this model...": "Décrire ce modèle...",
|
||||||
"Describe this vendor...": "Décrire ce fournisseur...",
|
"Describe this vendor...": "Décrire ce fournisseur...",
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1108,7 +1108,7 @@
|
|||||||
"Deployment Region *": "デプロイリージョン *",
|
"Deployment Region *": "デプロイリージョン *",
|
||||||
"Deployment requested": "デプロイメントが要求されました",
|
"Deployment requested": "デプロイメントが要求されました",
|
||||||
"Deployments": "デプロイ",
|
"Deployments": "デプロイ",
|
||||||
"Desc": "説明",
|
"Desc": "降順",
|
||||||
"Describe": "説明",
|
"Describe": "説明",
|
||||||
"Describe this model...": "このモデルを説明...",
|
"Describe this model...": "このモデルを説明...",
|
||||||
"Describe this vendor...": "このベンダーを説明...",
|
"Describe this vendor...": "このベンダーを説明...",
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1108,7 +1108,7 @@
|
|||||||
"Deployment Region *": "Регион развертывания *",
|
"Deployment Region *": "Регион развертывания *",
|
||||||
"Deployment requested": "Развертывание запрошено",
|
"Deployment requested": "Развертывание запрошено",
|
||||||
"Deployments": "Развертывания",
|
"Deployments": "Развертывания",
|
||||||
"Desc": "Описание",
|
"Desc": "По убыванию",
|
||||||
"Describe": "Описание",
|
"Describe": "Описание",
|
||||||
"Describe this model...": "Опишите эту модель...",
|
"Describe this model...": "Опишите эту модель...",
|
||||||
"Describe this vendor...": "Опишите этого поставщика...",
|
"Describe this vendor...": "Опишите этого поставщика...",
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1108,7 +1108,7 @@
|
|||||||
"Deployment Region *": "Khu vực triển khai *",
|
"Deployment Region *": "Khu vực triển khai *",
|
||||||
"Deployment requested": "Yêu cầu triển khai",
|
"Deployment requested": "Yêu cầu triển khai",
|
||||||
"Deployments": "Triển khai",
|
"Deployments": "Triển khai",
|
||||||
"Desc": "Mô tả",
|
"Desc": "Giảm dần",
|
||||||
"Describe": "Mô tả",
|
"Describe": "Mô tả",
|
||||||
"Describe this model...": "Mô tả mô hình này...",
|
"Describe this model...": "Mô tả mô hình này...",
|
||||||
"Describe this vendor...": "Mô tả nhà cung cấp này...",
|
"Describe this vendor...": "Mô tả nhà cung cấp này...",
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1108,7 +1108,7 @@
|
|||||||
"Deployment Region *": "部署区域 *",
|
"Deployment Region *": "部署区域 *",
|
||||||
"Deployment requested": "部署已请求",
|
"Deployment requested": "部署已请求",
|
||||||
"Deployments": "部署",
|
"Deployments": "部署",
|
||||||
"Desc": "描述",
|
"Desc": "降序",
|
||||||
"Describe": "图生文",
|
"Describe": "图生文",
|
||||||
"Describe this model...": "描述此模型...",
|
"Describe this model...": "描述此模型...",
|
||||||
"Describe this vendor...": "描述此供应商...",
|
"Describe this vendor...": "描述此供应商...",
|
||||||
|
|||||||
Reference in New Issue
Block a user