From 57cd68e7e30a475c0cad2cbec65ce24728db3f68 Mon Sep 17 00:00:00 2001 From: Buer <42402987+MartialBE@users.noreply.github.com> Date: Thu, 8 Feb 2024 03:41:51 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20support=20paging=20/=20sort?= =?UTF-8?q?ing=20(#64)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: support paging / sorting * 🔥 del: delete unused files --- common/gin.go | 7 + controller/channel-billing.go | 2 +- controller/channel-test.go | 2 +- controller/channel.go | 30 +-- controller/log.go | 49 ++-- controller/redemption.go | 39 +-- controller/token.go | 31 +-- controller/user.go | 30 +-- model/channel.go | 40 +-- model/common.go | 83 +++++++ model/log.go | 105 ++++---- model/redemption.go | 23 +- model/token.go | 25 +- model/user.go | 19 +- router/api-router.go | 20 +- web/src/ui-component/TableHead.js | 47 ++++ web/src/ui-component/TableToolBar.js | 5 +- web/src/views/Channel/component/TableHead.js | 21 -- web/src/views/Channel/index.js | 156 ++++++------ web/src/views/Log/component/TableHead.js | 28 --- web/src/views/Log/index.js | 229 ++++++++++++------ .../views/Redemption/component/TableHead.js | 19 -- web/src/views/Redemption/index.js | 145 +++++------ web/src/views/Token/component/TableHead.js | 19 -- web/src/views/Token/index.js | 140 ++++++----- web/src/views/User/component/TableHead.js | 20 -- web/src/views/User/component/TableRow.js | 4 +- web/src/views/User/index.js | 149 ++++++------ 28 files changed, 796 insertions(+), 691 deletions(-) create mode 100644 web/src/ui-component/TableHead.js delete mode 100644 web/src/views/Channel/component/TableHead.js delete mode 100644 web/src/views/Log/component/TableHead.js delete mode 100644 web/src/views/Redemption/component/TableHead.js delete mode 100644 web/src/views/Token/component/TableHead.js delete mode 100644 web/src/views/User/component/TableHead.js diff --git a/common/gin.go b/common/gin.go index 7e865f67..56f3a341 100644 --- a/common/gin.go +++ b/common/gin.go @@ -67,3 +67,10 @@ func AbortWithMessage(c *gin.Context, statusCode int, message string) { c.Abort() LogError(c.Request.Context(), message) } + +func APIRespondWithError(c *gin.Context, status int, err error) { + c.JSON(status, gin.H{ + "success": false, + "message": err.Error(), + }) +} diff --git a/controller/channel-billing.go b/controller/channel-billing.go index f6bb523b..83bfce88 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -104,7 +104,7 @@ func UpdateChannelBalance(c *gin.Context) { } func updateAllChannelsBalance() error { - channels, err := model.GetAllChannels(0, 0, true) + channels, err := model.GetAllChannels() if err != nil { return err } diff --git a/controller/channel-test.go b/controller/channel-test.go index 3f675184..37d544c3 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -167,7 +167,7 @@ func testAllChannels(notify bool) error { } testAllChannelsRunning = true testAllChannelsLock.Unlock() - channels, err := model.GetAllChannels(0, 0, true) + channels, err := model.GetAllChannels() if err != nil { return err } diff --git a/controller/channel.go b/controller/channel.go index 7b2a45f7..c05909eb 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -10,34 +10,16 @@ import ( "github.com/gin-gonic/gin" ) -func GetAllChannels(c *gin.Context) { - p, _ := strconv.Atoi(c.Query("p")) - if p < 0 { - p = 0 - } - channels, err := model.GetAllChannels(p*common.ItemsPerPage, common.ItemsPerPage, false) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) +func GetChannelsList(c *gin.Context) { + var params model.GenericParams + if err := c.ShouldBindQuery(¶ms); err != nil { + common.APIRespondWithError(c, http.StatusOK, err) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": channels, - }) -} -func SearchChannels(c *gin.Context) { - keyword := c.Query("keyword") - channels, err := model.SearchChannels(keyword) + channels, err := model.GetChannelsList(¶ms) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + common.APIRespondWithError(c, http.StatusOK, err) return } c.JSON(http.StatusOK, gin.H{ diff --git a/controller/log.go b/controller/log.go index 6327a6a8..bc30fea7 100644 --- a/controller/log.go +++ b/controller/log.go @@ -9,24 +9,16 @@ import ( "github.com/gin-gonic/gin" ) -func GetAllLogs(c *gin.Context) { - p, _ := strconv.Atoi(c.Query("p")) - if p < 0 { - p = 0 +func GetLogsList(c *gin.Context) { + var params model.LogsListParams + if err := c.ShouldBindQuery(¶ms); err != nil { + common.APIRespondWithError(c, http.StatusOK, err) + return } - logType, _ := strconv.Atoi(c.Query("type")) - startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) - endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) - username := c.Query("username") - tokenName := c.Query("token_name") - modelName := c.Query("model_name") - channel, _ := strconv.Atoi(c.Query("channel")) - logs, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, p*common.ItemsPerPage, common.ItemsPerPage, channel) + + logs, err := model.GetLogsList(¶ms) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + common.APIRespondWithError(c, http.StatusOK, err) return } c.JSON(http.StatusOK, gin.H{ @@ -36,23 +28,18 @@ func GetAllLogs(c *gin.Context) { }) } -func GetUserLogs(c *gin.Context) { - p, _ := strconv.Atoi(c.Query("p")) - if p < 0 { - p = 0 - } +func GetUserLogsList(c *gin.Context) { userId := c.GetInt("id") - logType, _ := strconv.Atoi(c.Query("type")) - startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) - endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) - tokenName := c.Query("token_name") - modelName := c.Query("model_name") - logs, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, p*common.ItemsPerPage, common.ItemsPerPage) + + var params model.LogsListParams + if err := c.ShouldBindQuery(¶ms); err != nil { + common.APIRespondWithError(c, http.StatusOK, err) + return + } + + logs, err := model.GetUserLogsList(userId, ¶ms) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + common.APIRespondWithError(c, http.StatusOK, err) return } c.JSON(http.StatusOK, gin.H{ diff --git a/controller/redemption.go b/controller/redemption.go index 0f656be0..ee111f88 100644 --- a/controller/redemption.go +++ b/controller/redemption.go @@ -1,42 +1,24 @@ package controller import ( - "github.com/gin-gonic/gin" "net/http" "one-api/common" "one-api/model" "strconv" + + "github.com/gin-gonic/gin" ) -func GetAllRedemptions(c *gin.Context) { - p, _ := strconv.Atoi(c.Query("p")) - if p < 0 { - p = 0 - } - redemptions, err := model.GetAllRedemptions(p*common.ItemsPerPage, common.ItemsPerPage) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) +func GetRedemptionsList(c *gin.Context) { + var params model.GenericParams + if err := c.ShouldBindQuery(¶ms); err != nil { + common.APIRespondWithError(c, http.StatusOK, err) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": redemptions, - }) - return -} -func SearchRedemptions(c *gin.Context) { - keyword := c.Query("keyword") - redemptions, err := model.SearchRedemptions(keyword) + redemptions, err := model.GetRedemptionsList(¶ms) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + common.APIRespondWithError(c, http.StatusOK, err) return } c.JSON(http.StatusOK, gin.H{ @@ -44,7 +26,6 @@ func SearchRedemptions(c *gin.Context) { "message": "", "data": redemptions, }) - return } func GetRedemption(c *gin.Context) { @@ -69,7 +50,6 @@ func GetRedemption(c *gin.Context) { "message": "", "data": redemption, }) - return } func AddRedemption(c *gin.Context) { @@ -129,7 +109,6 @@ func AddRedemption(c *gin.Context) { "message": "", "data": keys, }) - return } func DeleteRedemption(c *gin.Context) { @@ -146,7 +125,6 @@ func DeleteRedemption(c *gin.Context) { "success": true, "message": "", }) - return } func UpdateRedemption(c *gin.Context) { @@ -188,5 +166,4 @@ func UpdateRedemption(c *gin.Context) { "message": "", "data": cleanRedemption, }) - return } diff --git a/controller/token.go b/controller/token.go index a4e3a235..608d630b 100644 --- a/controller/token.go +++ b/controller/token.go @@ -9,36 +9,17 @@ import ( "github.com/gin-gonic/gin" ) -func GetAllTokens(c *gin.Context) { +func GetUserTokensList(c *gin.Context) { userId := c.GetInt("id") - p, _ := strconv.Atoi(c.Query("p")) - if p < 0 { - p = 0 - } - tokens, err := model.GetAllUserTokens(userId, p*common.ItemsPerPage, common.ItemsPerPage) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + var params model.GenericParams + if err := c.ShouldBindQuery(¶ms); err != nil { + common.APIRespondWithError(c, http.StatusOK, err) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": tokens, - }) -} -func SearchTokens(c *gin.Context) { - userId := c.GetInt("id") - keyword := c.Query("keyword") - tokens, err := model.SearchUserTokens(userId, keyword) + tokens, err := model.GetUserTokensList(userId, ¶ms) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + common.APIRespondWithError(c, http.StatusOK, err) return } c.JSON(http.StatusOK, gin.H{ diff --git a/controller/user.go b/controller/user.go index e854093f..42faad3e 100644 --- a/controller/user.go +++ b/controller/user.go @@ -176,34 +176,16 @@ func Register(c *gin.Context) { }) } -func GetAllUsers(c *gin.Context) { - p, _ := strconv.Atoi(c.Query("p")) - if p < 0 { - p = 0 - } - users, err := model.GetAllUsers(p*common.ItemsPerPage, common.ItemsPerPage) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) +func GetUsersList(c *gin.Context) { + var params model.GenericParams + if err := c.ShouldBindQuery(¶ms); err != nil { + common.APIRespondWithError(c, http.StatusOK, err) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": users, - }) -} -func SearchUsers(c *gin.Context) { - keyword := c.Query("keyword") - users, err := model.SearchUsers(keyword) + users, err := model.GetUsersList(¶ms) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + common.APIRespondWithError(c, http.StatusOK, err) return } c.JSON(http.StatusOK, gin.H{ diff --git a/model/channel.go b/model/channel.go index 98e22c10..29c5b038 100644 --- a/model/channel.go +++ b/model/channel.go @@ -29,23 +29,35 @@ type Channel struct { TestModel string `json:"test_model" gorm:"type:varchar(50);default:''"` } -func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) { - var channels []*Channel - var err error - if selectAll { - err = DB.Order("id desc").Find(&channels).Error - } else { - err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error - } - return channels, err +var allowedChannelOrderFields = map[string]bool{ + "id": true, + "name": true, + "group": true, + "type": true, + "status": true, + "response_time": true, + "balance": true, + "priority": true, } -func SearchChannels(keyword string) (channels []*Channel, err error) { - keyCol := "`key`" - if common.UsingPostgreSQL { - keyCol = `"key"` +func GetChannelsList(params *GenericParams) (*DataResult, error) { + var channels []*Channel + + db := DB.Omit("key") + if params.Keyword != "" { + keyCol := "`key`" + if common.UsingPostgreSQL { + keyCol = `"key"` + } + db = db.Where("id = ? or name LIKE ? or "+keyCol+" = ?", common.String2Int(params.Keyword), params.Keyword+"%", params.Keyword) } - err = DB.Omit("key").Where("id = ? or name LIKE ? or "+keyCol+" = ?", common.String2Int(keyword), keyword+"%", keyword).Find(&channels).Error + + return PaginateAndOrder(db, ¶ms.PaginationParams, &channels, allowedChannelOrderFields) +} + +func GetAllChannels() ([]*Channel, error) { + var channels []*Channel + err := DB.Order("id desc").Find(&channels).Error return channels, err } diff --git a/model/common.go b/model/common.go index 4257b097..37fcd739 100644 --- a/model/common.go +++ b/model/common.go @@ -3,8 +3,91 @@ package model import ( "fmt" "one-api/common" + "strings" + + "gorm.io/gorm" ) +type GenericParams struct { + PaginationParams + Keyword string `form:"keyword"` +} + +type PaginationParams struct { + Page int `form:"page"` + Size int `form:"size"` + Order string `form:"order"` +} + +type DataResult struct { + Data interface{} `json:"data"` + Page int `json:"page"` + Size int `json:"size"` + TotalCount int64 `json:"total_count"` +} + +func PaginateAndOrder(db *gorm.DB, params *PaginationParams, result interface{}, allowedOrderFields map[string]bool) (*DataResult, error) { + // 获取总数 + var totalCount int64 + err := db.Model(result).Count(&totalCount).Error + if err != nil { + return nil, err + } + + fmt.Println("totalCount", totalCount) + + // 分页 + if params.Page < 1 { + params.Page = 1 + } + if params.Size < 1 { + params.Size = common.ItemsPerPage + } + + if params.Size > common.MaxRecentItems { + return nil, fmt.Errorf("size 参数不能超过 %d", common.MaxRecentItems) + } + + offset := (params.Page - 1) * params.Size + db = db.Offset(offset).Limit(params.Size) + + // 排序 + if params.Order != "" { + orderFields := strings.Split(params.Order, ",") + for _, field := range orderFields { + field = strings.TrimSpace(field) + desc := strings.HasPrefix(field, "-") + if desc { + field = field[1:] + } + if !allowedOrderFields[field] { + return nil, fmt.Errorf("不允许对字段 '%s' 进行排序", field) + } + if desc { + field = field + " DESC" + } + db = db.Order(field) + } + } else { + // 默认排序 + db = db.Order("id DESC") + } + + // 查询 + err = db.Find(result).Error + if err != nil { + return nil, err + } + + // 返回结果 + return &DataResult{ + Data: result, + Page: params.Page, + Size: params.Size, + TotalCount: totalCount, + }, nil +} + func getDateFormat(groupType string) string { var dateFormat string if groupType == "day" { diff --git a/model/log.go b/model/log.go index ca5eb5e9..a4141952 100644 --- a/model/log.go +++ b/model/log.go @@ -74,56 +74,79 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke } } -func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int) (logs []*Log, err error) { - var tx *gorm.DB - if logType == LogTypeUnknown { - tx = DB - } else { - tx = DB.Where("type = ?", logType) - } - if modelName != "" { - tx = tx.Where("model_name = ?", modelName) - } - if username != "" { - tx = tx.Where("username = ?", username) - } - if tokenName != "" { - tx = tx.Where("token_name = ?", tokenName) - } - if startTimestamp != 0 { - tx = tx.Where("created_at >= ?", startTimestamp) - } - if endTimestamp != 0 { - tx = tx.Where("created_at <= ?", endTimestamp) - } - if channel != 0 { - tx = tx.Where("channel_id = ?", channel) - } - err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error - return logs, err +type LogsListParams struct { + PaginationParams + LogType int `form:"log_type"` + StartTimestamp int64 `form:"start_timestamp"` + EndTimestamp int64 `form:"end_timestamp"` + ModelName string `form:"model_name"` + Username string `form:"username"` + TokenName string `form:"token_name"` + Channel int `form:"channel"` } -func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int) (logs []*Log, err error) { +var allowedLogsOrderFields = map[string]bool{ + "created_at": true, + "channel_id": true, + "user_id": true, + "token_name": true, + "model_name": true, + "type": true, +} + +func GetLogsList(params *LogsListParams) (*DataResult, error) { var tx *gorm.DB - if logType == LogTypeUnknown { - tx = DB.Where("user_id = ?", userId) + var logs []*Log + + if params.LogType == LogTypeUnknown { + tx = DB } else { - tx = DB.Where("user_id = ? and type = ?", userId, logType) + tx = DB.Where("type = ?", params.LogType) } - if modelName != "" { - tx = tx.Where("model_name = ?", modelName) + if params.ModelName != "" { + tx = tx.Where("model_name = ?", params.ModelName) } - if tokenName != "" { - tx = tx.Where("token_name = ?", tokenName) + if params.Username != "" { + tx = tx.Where("username = ?", params.Username) } - if startTimestamp != 0 { - tx = tx.Where("created_at >= ?", startTimestamp) + if params.TokenName != "" { + tx = tx.Where("token_name = ?", params.TokenName) } - if endTimestamp != 0 { - tx = tx.Where("created_at <= ?", endTimestamp) + if params.StartTimestamp != 0 { + tx = tx.Where("created_at >= ?", params.StartTimestamp) } - err = tx.Order("id desc").Limit(num).Offset(startIdx).Omit("id").Find(&logs).Error - return logs, err + if params.EndTimestamp != 0 { + tx = tx.Where("created_at <= ?", params.EndTimestamp) + } + if params.Channel != 0 { + tx = tx.Where("channel_id = ?", params.Channel) + } + + return PaginateAndOrder(tx, ¶ms.PaginationParams, &logs, allowedLogsOrderFields) +} + +func GetUserLogsList(userId int, params *LogsListParams) (*DataResult, error) { + var logs []*Log + + tx := DB.Where("user_id = ?", userId).Omit("id") + + if params.LogType != LogTypeUnknown { + tx = DB.Where("type = ?", params.LogType) + } + if params.ModelName != "" { + tx = tx.Where("model_name = ?", params.ModelName) + } + if params.TokenName != "" { + tx = tx.Where("token_name = ?", params.TokenName) + } + if params.StartTimestamp != 0 { + tx = tx.Where("created_at >= ?", params.StartTimestamp) + } + if params.EndTimestamp != 0 { + tx = tx.Where("created_at <= ?", params.EndTimestamp) + } + + return PaginateAndOrder(tx, ¶ms.PaginationParams, &logs, allowedLogsOrderFields) } func SearchAllLogs(keyword string) (logs []*Log, err error) { diff --git a/model/redemption.go b/model/redemption.go index 6cf6be43..a5389102 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -20,16 +20,23 @@ type Redemption struct { Count int `json:"count" gorm:"-:all"` // only for api request } -func GetAllRedemptions(startIdx int, num int) ([]*Redemption, error) { - var redemptions []*Redemption - var err error - err = DB.Order("id desc").Limit(num).Offset(startIdx).Find(&redemptions).Error - return redemptions, err +var allowedRedemptionslOrderFields = map[string]bool{ + "id": true, + "name": true, + "status": true, + "quota": true, + "created_time": true, + "redeemed_time": true, } -func SearchRedemptions(keyword string) (redemptions []*Redemption, err error) { - err = DB.Where("id = ? or name LIKE ?", common.String2Int(keyword), keyword+"%").Find(&redemptions).Error - return redemptions, err +func GetRedemptionsList(params *GenericParams) (*DataResult, error) { + var redemptions []*Redemption + db := DB + if params.Keyword != "" { + db = db.Where("id = ? or name LIKE ?", common.String2Int(params.Keyword), params.Keyword+"%") + } + + return PaginateAndOrder(db, ¶ms.PaginationParams, &redemptions, allowedRedemptionslOrderFields) } func GetRedemptionById(id int) (*Redemption, error) { diff --git a/model/token.go b/model/token.go index f1699f49..74eae34b 100644 --- a/model/token.go +++ b/model/token.go @@ -22,16 +22,25 @@ type Token struct { UsedQuota int `json:"used_quota" gorm:"default:0"` // used quota } -func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) { - var tokens []*Token - var err error - err = DB.Where("user_id = ?", userId).Order("id desc").Limit(num).Offset(startIdx).Find(&tokens).Error - return tokens, err +var allowedTokenOrderFields = map[string]bool{ + "id": true, + "name": true, + "status": true, + "expired_time": true, + "created_time": true, + "remain_quota": true, + "used_quota": true, } -func SearchUserTokens(userId int, keyword string) (tokens []*Token, err error) { - err = DB.Where("user_id = ?", userId).Where("name LIKE ?", keyword+"%").Find(&tokens).Error - return tokens, err +func GetUserTokensList(userId int, params *GenericParams) (*DataResult, error) { + var tokens []*Token + db := DB.Where("user_id = ?", userId) + + if params.Keyword != "" { + db = db.Where("name LIKE ?", params.Keyword+"%") + } + + return PaginateAndOrder(db, ¶ms.PaginationParams, &tokens, allowedTokenOrderFields) } func ValidateUserToken(key string) (token *Token, err error) { diff --git a/model/user.go b/model/user.go index d908ab93..50a6d5a8 100644 --- a/model/user.go +++ b/model/user.go @@ -38,15 +38,22 @@ func GetMaxUserId() int { return user.Id } -func GetAllUsers(startIdx int, num int) (users []*User, err error) { - err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("password").Find(&users).Error - return users, err +var allowedUserOrderFields = map[string]bool{ + "id": true, + "username": true, + "role": true, + "status": true, + "created_time": true, } -func SearchUsers(keyword string) (users []*User, err error) { - err = DB.Omit("password").Where("id = ? or username LIKE ? or email LIKE ? or display_name LIKE ?", common.String2Int(keyword), keyword+"%", keyword+"%", keyword+"%").Find(&users).Error +func GetUsersList(params *GenericParams) (*DataResult, error) { + var users []*User + db := DB.Omit("password") + if params.Keyword != "" { + db = db.Where("id = ? or username LIKE ? or email LIKE ? or display_name LIKE ?", common.String2Int(params.Keyword), params.Keyword+"%", params.Keyword+"%", params.Keyword+"%") + } - return users, err + return PaginateAndOrder(db, ¶ms.PaginationParams, &users, allowedUserOrderFields) } func GetUserById(id int, selectAll bool) (*User, error) { diff --git a/router/api-router.go b/router/api-router.go index 9a1f9b6c..f602a68a 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -48,8 +48,7 @@ func SetApiRouter(router *gin.Engine) { adminRoute := userRoute.Group("/") adminRoute.Use(middleware.AdminAuth()) { - adminRoute.GET("/", controller.GetAllUsers) - adminRoute.GET("/search", controller.SearchUsers) + adminRoute.GET("/", controller.GetUsersList) adminRoute.GET("/:id", controller.GetUser) adminRoute.POST("/", controller.CreateUser) adminRoute.POST("/manage", controller.ManageUser) @@ -66,8 +65,7 @@ func SetApiRouter(router *gin.Engine) { channelRoute := apiRouter.Group("/channel") channelRoute.Use(middleware.AdminAuth()) { - channelRoute.GET("/", controller.GetAllChannels) - channelRoute.GET("/search", controller.SearchChannels) + channelRoute.GET("/", controller.GetChannelsList) channelRoute.GET("/models", controller.ListModelsForAdmin) channelRoute.GET("/:id", controller.GetChannel) channelRoute.GET("/test", controller.TestAllChannels) @@ -82,8 +80,7 @@ func SetApiRouter(router *gin.Engine) { tokenRoute := apiRouter.Group("/token") tokenRoute.Use(middleware.UserAuth()) { - tokenRoute.GET("/", controller.GetAllTokens) - tokenRoute.GET("/search", controller.SearchTokens) + tokenRoute.GET("/", controller.GetUserTokensList) tokenRoute.GET("/:id", controller.GetToken) tokenRoute.POST("/", controller.AddToken) tokenRoute.PUT("/", controller.UpdateToken) @@ -92,21 +89,20 @@ func SetApiRouter(router *gin.Engine) { redemptionRoute := apiRouter.Group("/redemption") redemptionRoute.Use(middleware.AdminAuth()) { - redemptionRoute.GET("/", controller.GetAllRedemptions) - redemptionRoute.GET("/search", controller.SearchRedemptions) + redemptionRoute.GET("/", controller.GetRedemptionsList) redemptionRoute.GET("/:id", controller.GetRedemption) redemptionRoute.POST("/", controller.AddRedemption) redemptionRoute.PUT("/", controller.UpdateRedemption) redemptionRoute.DELETE("/:id", controller.DeleteRedemption) } logRoute := apiRouter.Group("/log") - logRoute.GET("/", middleware.AdminAuth(), controller.GetAllLogs) + logRoute.GET("/", middleware.AdminAuth(), controller.GetLogsList) logRoute.DELETE("/", middleware.AdminAuth(), controller.DeleteHistoryLogs) logRoute.GET("/stat", middleware.AdminAuth(), controller.GetLogsStat) logRoute.GET("/self/stat", middleware.UserAuth(), controller.GetLogsSelfStat) - logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs) - logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogs) - logRoute.GET("/self/search", middleware.UserAuth(), controller.SearchUserLogs) + // logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs) + logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogsList) + // logRoute.GET("/self/search", middleware.UserAuth(), controller.SearchUserLogs) groupRoute := apiRouter.Group("/group") groupRoute.Use(middleware.AdminAuth()) { diff --git a/web/src/ui-component/TableHead.js b/web/src/ui-component/TableHead.js new file mode 100644 index 00000000..400efc50 --- /dev/null +++ b/web/src/ui-component/TableHead.js @@ -0,0 +1,47 @@ +import PropTypes from 'prop-types'; +import { TableCell, TableHead, TableRow, TableSortLabel } from '@mui/material'; + +const KeywordTableHead = ({ order, orderBy, headLabel, onRequestSort }) => { + const onSort = (property) => (event) => { + onRequestSort(event, property); + }; + + return ( + + + {headLabel.map((headCell) => + headCell.hide && headCell.hide === true ? null : ( + + {headCell.disableSort ? ( + headCell.label + ) : ( + + {headCell.label} + + )} + + ) + )} + + + ); +}; + +export default KeywordTableHead; + +KeywordTableHead.propTypes = { + order: PropTypes.oneOf(['asc', 'desc']), + orderBy: PropTypes.string, + onRequestSort: PropTypes.func, + headLabel: PropTypes.array +}; diff --git a/web/src/ui-component/TableToolBar.js b/web/src/ui-component/TableToolBar.js index b31d0ef4..e1f90a78 100644 --- a/web/src/ui-component/TableToolBar.js +++ b/web/src/ui-component/TableToolBar.js @@ -9,7 +9,7 @@ import { IconSearch } from '@tabler/icons-react'; // ---------------------------------------------------------------------- -export default function TableToolBar({ filterName, handleFilterName, placeholder }) { +export default function TableToolBar({ placeholder }) { const theme = useTheme(); const grey500 = theme.palette.grey[500]; @@ -24,11 +24,10 @@ export default function TableToolBar({ filterName, handleFilterName, placeholder > diff --git a/web/src/views/Channel/component/TableHead.js b/web/src/views/Channel/component/TableHead.js deleted file mode 100644 index 736dd8aa..00000000 --- a/web/src/views/Channel/component/TableHead.js +++ /dev/null @@ -1,21 +0,0 @@ -import { TableCell, TableHead, TableRow } from '@mui/material'; - -const ChannelTableHead = () => { - return ( - - - ID - 名称 - 分组 - 类型 - 状态 - 响应时间 - 余额 - 优先级 - 操作 - - - ); -}; - -export default ChannelTableHead; diff --git a/web/src/views/Channel/index.js b/web/src/views/Channel/index.js index c397f2c9..631595fd 100644 --- a/web/src/views/Channel/index.js +++ b/web/src/views/Channel/index.js @@ -15,83 +15,53 @@ import useMediaQuery from '@mui/material/useMediaQuery'; import { Button, IconButton, Card, Box, Stack, Container, Typography, Divider } from '@mui/material'; import ChannelTableRow from './component/TableRow'; -import ChannelTableHead from './component/TableHead'; +import KeywordTableHead from 'ui-component/TableHead'; import TableToolBar from 'ui-component/TableToolBar'; import { API } from 'utils/api'; -import { ITEMS_PER_PAGE } from 'constants'; import { IconRefresh, IconHttpDelete, IconPlus, IconBrandSpeedtest, IconCoinYuan } from '@tabler/icons-react'; import EditeModal from './component/EditModal'; +import { ITEMS_PER_PAGE } from 'constants'; // ---------------------------------------------------------------------- // CHANNEL_OPTIONS, export default function ChannelPage() { - const [channels, setChannels] = useState([]); - const [activePage, setActivePage] = useState(0); - const [searching, setSearching] = useState(false); + const [page, setPage] = useState(0); + const [order, setOrder] = useState('desc'); + const [orderBy, setOrderBy] = useState('id'); + const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE); + const [listCount, setListCount] = useState(0); const [searchKeyword, setSearchKeyword] = useState(''); + const [searching, setSearching] = useState(false); + const [channels, setChannels] = useState([]); + const [refreshFlag, setRefreshFlag] = useState(false); + const theme = useTheme(); const matchUpMd = useMediaQuery(theme.breakpoints.up('sm')); const [openModal, setOpenModal] = useState(false); const [editChannelId, setEditChannelId] = useState(0); - const loadChannels = async (startIdx) => { - setSearching(true); - try { - const res = await API.get(`/api/channel/?p=${startIdx}`); - const { success, message, data } = res.data; - if (success) { - if (startIdx === 0) { - setChannels(data); - } else { - let newChannels = [...channels]; - newChannels.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data); - setChannels(newChannels); - } - } else { - showError(message); - } - } catch (error) { - console.error(error); + const handleSort = (event, id) => { + const isAsc = orderBy === id && order === 'asc'; + if (id !== '') { + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(id); } - - setSearching(false); }; - const onPaginationChange = (event, activePage) => { - (async () => { - if (activePage === Math.ceil(channels.length / ITEMS_PER_PAGE)) { - // In this case we have to load more data and then append them. - await loadChannels(activePage); - } - setActivePage(activePage); - })(); + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setPage(0); + setRowsPerPage(parseInt(event.target.value, 10)); }; const searchChannels = async (event) => { event.preventDefault(); - if (searchKeyword === '') { - await loadChannels(0); - setActivePage(0); - return; - } - setSearching(true); - try { - const res = await API.get(`/api/channel/search?keyword=${searchKeyword}`); - const { success, message, data } = res.data; - if (success) { - setChannels(data); - setActivePage(0); - } else { - showError(message); - } - } catch (error) { - console.error(error); - } - setSearching(false); - }; - - const handleSearchKeyword = (event) => { - setSearchKeyword(event.target.value); + const formData = new FormData(event.target); + setPage(0); + setSearchKeyword(formData.get('keyword')); }; const manageChannel = async (id, action, value) => { @@ -141,7 +111,9 @@ export default function ChannelPage() { // 处理刷新 const handleRefresh = async () => { - await loadChannels(activePage); + setOrderBy('id'); + setOrder('desc'); + setRefreshFlag(!refreshFlag); }; // 处理测试所有启用渠道 @@ -210,14 +182,36 @@ export default function ChannelPage() { } }; - useEffect(() => { - loadChannels(0) - .then() - .catch((reason) => { - showError(reason); + const fetchData = async (page, rowsPerPage, keyword, order, orderBy) => { + setSearching(true); + try { + if (orderBy) { + orderBy = order === 'desc' ? '-' + orderBy : orderBy; + } + const res = await API.get(`/api/channel/`, { + params: { + page: page + 1, + size: rowsPerPage, + keyword: keyword, + order: orderBy + } }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const { success, message, data } = res.data; + if (success) { + setListCount(data.total_count); + setChannels(data.data); + } else { + showError(message); + } + } catch (error) { + console.error(error); + } + setSearching(false); + }; + + useEffect(() => { + fetchData(page, rowsPerPage, searchKeyword, order, orderBy); + }, [page, rowsPerPage, searchKeyword, order, orderBy, refreshFlag]); return ( <> @@ -237,7 +231,7 @@ export default function ChannelPage() { - + - + - {channels.slice(activePage * ITEMS_PER_PAGE, (activePage + 1) * ITEMS_PER_PAGE).map((row) => ( + {channels.map((row) => ( diff --git a/web/src/views/Log/component/TableHead.js b/web/src/views/Log/component/TableHead.js deleted file mode 100644 index 102986ef..00000000 --- a/web/src/views/Log/component/TableHead.js +++ /dev/null @@ -1,28 +0,0 @@ -import PropTypes from 'prop-types'; -import { TableCell, TableHead, TableRow } from '@mui/material'; - -const LogTableHead = ({ userIsAdmin }) => { - return ( - - - 时间 - {userIsAdmin && 渠道} - {userIsAdmin && 用户} - 令牌 - 类型 - 模型 - 耗时 - 提示 - 补全 - 额度 - 详情 - - - ); -}; - -export default LogTableHead; - -LogTableHead.propTypes = { - userIsAdmin: PropTypes.bool -}; diff --git a/web/src/views/Log/index.js b/web/src/views/Log/index.js index 43567531..b4b6e973 100644 --- a/web/src/views/Log/index.js +++ b/web/src/views/Log/index.js @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { showError } from 'utils/common'; import Table from '@mui/material/Table'; @@ -12,12 +12,13 @@ import Toolbar from '@mui/material/Toolbar'; import { Button, Card, Stack, Container, Typography, Box } from '@mui/material'; import LogTableRow from './component/TableRow'; -import LogTableHead from './component/TableHead'; +import KeywordTableHead from 'ui-component/TableHead'; import TableToolBar from './component/TableToolBar'; import { API } from 'utils/api'; import { isAdmin } from 'utils/common'; import { ITEMS_PER_PAGE } from 'constants'; import { IconRefresh, IconSearch } from '@tabler/icons-react'; +import dayjs from 'dayjs'; export default function Log() { const originalKeyword = { @@ -26,86 +27,98 @@ export default function Log() { token_name: '', model_name: '', start_timestamp: 0, - end_timestamp: new Date().getTime() / 1000 + 3600, + end_timestamp: dayjs().unix() + 3600, type: 0, channel: '' }; - const [logs, setLogs] = useState([]); - const [activePage, setActivePage] = useState(0); + + const [page, setPage] = useState(0); + const [order, setOrder] = useState('desc'); + const [orderBy, setOrderBy] = useState('created_at'); + const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE); + const [listCount, setListCount] = useState(0); const [searching, setSearching] = useState(false); + const [toolBarValue, setToolBarValue] = useState(originalKeyword); const [searchKeyword, setSearchKeyword] = useState(originalKeyword); - const [initPage, setInitPage] = useState(true); + const [refreshFlag, setRefreshFlag] = useState(false); + + const [logs, setLogs] = useState([]); const userIsAdmin = isAdmin(); - const loadLogs = async (startIdx) => { - setSearching(true); - const url = userIsAdmin ? '/api/log/' : '/api/log/self/'; - const query = searchKeyword; - - query.p = startIdx; - if (!userIsAdmin) { - delete query.username; - delete query.channel; + const handleSort = (event, id) => { + const isAsc = orderBy === id && order === 'asc'; + if (id !== '') { + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(id); } + }; - try { - const res = await API.get(url, { params: query }); - const { success, message, data } = res.data; - if (success) { - if (startIdx === 0) { - setLogs(data); - } else { - let newLogs = [...logs]; - newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data); - setLogs(newLogs); + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setPage(0); + setRowsPerPage(parseInt(event.target.value, 10)); + }; + + const searchLogs = async () => { + setPage(0); + setSearchKeyword(toolBarValue); + }; + + const handleToolBarValue = (event) => { + setToolBarValue({ ...toolBarValue, [event.target.name]: event.target.value }); + }; + + const fetchData = useCallback( + async (page, rowsPerPage, keyword, order, orderBy) => { + setSearching(true); + try { + if (orderBy) { + orderBy = order === 'desc' ? '-' + orderBy : orderBy; } - } else { - showError(message); + const url = userIsAdmin ? '/api/log/' : '/api/log/self/'; + if (!userIsAdmin) { + delete keyword.username; + delete keyword.channel; + } + + const res = await API.get(url, { + params: { + page: page + 1, + size: rowsPerPage, + order: orderBy, + ...keyword + } + }); + const { success, message, data } = res.data; + if (success) { + setListCount(data.total_count); + setLogs(data.data); + } else { + showError(message); + } + } catch (error) { + console.error(error); } - } catch (error) { - console.log(error); - } - - setSearching(false); - }; - - const onPaginationChange = (event, activePage) => { - (async () => { - if (activePage === Math.ceil(logs.length / ITEMS_PER_PAGE)) { - // In this case we have to load more data and then append them. - await loadLogs(activePage); - } - setActivePage(activePage); - })(); - }; - - const searchLogs = async (event) => { - event.preventDefault(); - await loadLogs(0); - setActivePage(0); - return; - }; - - const handleSearchKeyword = (event) => { - setSearchKeyword({ ...searchKeyword, [event.target.name]: event.target.value }); - }; + setSearching(false); + }, + [userIsAdmin] + ); // 处理刷新 - const handleRefresh = () => { - setInitPage(true); + const handleRefresh = async () => { + setOrderBy('created_at'); + setOrder('desc'); + setToolBarValue(originalKeyword); + setSearchKeyword(originalKeyword); + setRefreshFlag(!refreshFlag); }; useEffect(() => { - setSearchKeyword(originalKeyword); - setActivePage(0); - loadLogs(0) - .then() - .catch((reason) => { - showError(reason); - }); - setInitPage(false); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initPage]); + fetchData(page, rowsPerPage, searchKeyword, order, orderBy); + }, [page, rowsPerPage, searchKeyword, order, orderBy, fetchData, refreshFlag]); return ( <> @@ -113,8 +126,8 @@ export default function Log() { 日志 - - + +
- + - {logs.slice(activePage * ITEMS_PER_PAGE, (activePage + 1) * ITEMS_PER_PAGE).map((row, index) => ( + {logs.map((row, index) => ( ))} @@ -151,12 +227,15 @@ export default function Log() { diff --git a/web/src/views/Redemption/component/TableHead.js b/web/src/views/Redemption/component/TableHead.js deleted file mode 100644 index f5606937..00000000 --- a/web/src/views/Redemption/component/TableHead.js +++ /dev/null @@ -1,19 +0,0 @@ -import { TableCell, TableHead, TableRow } from '@mui/material'; - -const RedemptionTableHead = () => { - return ( - - - ID - 名称 - 状态 - 额度 - 创建时间 - 兑换时间 - 操作 - - - ); -}; - -export default RedemptionTableHead; diff --git a/web/src/views/Redemption/index.js b/web/src/views/Redemption/index.js index 9842c079..245c0fea 100644 --- a/web/src/views/Redemption/index.js +++ b/web/src/views/Redemption/index.js @@ -12,7 +12,7 @@ import Toolbar from '@mui/material/Toolbar'; import { Button, Card, Box, Stack, Container, Typography } from '@mui/material'; import RedemptionTableRow from './component/TableRow'; -import RedemptionTableHead from './component/TableHead'; +import KeywordTableHead from 'ui-component/TableHead'; import TableToolBar from 'ui-component/TableToolBar'; import { API } from 'utils/api'; import { ITEMS_PER_PAGE } from 'constants'; @@ -21,74 +21,81 @@ import EditeModal from './component/EditModal'; // ---------------------------------------------------------------------- export default function Redemption() { - const [redemptions, setRedemptions] = useState([]); - const [activePage, setActivePage] = useState(0); - const [searching, setSearching] = useState(false); + const [page, setPage] = useState(0); + const [order, setOrder] = useState('desc'); + const [orderBy, setOrderBy] = useState('id'); + const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE); + const [listCount, setListCount] = useState(0); const [searchKeyword, setSearchKeyword] = useState(''); + const [searching, setSearching] = useState(false); + const [redemptions, setRedemptions] = useState([]); + const [refreshFlag, setRefreshFlag] = useState(false); + const [openModal, setOpenModal] = useState(false); const [editRedemptionId, setEditRedemptionId] = useState(0); - const loadRedemptions = async (startIdx) => { - setSearching(true); - try { - const res = await API.get(`/api/redemption/?p=${startIdx}`); - const { success, message, data } = res.data; - if (success) { - if (startIdx === 0) { - setRedemptions(data); - } else { - let newRedemptions = [...redemptions]; - newRedemptions.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data); - setRedemptions(newRedemptions); - } - } else { - showError(message); - } - } catch (error) { - console.log(error); + const handleSort = (event, id) => { + const isAsc = orderBy === id && order === 'asc'; + if (id !== '') { + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(id); } - setSearching(false); }; - const onPaginationChange = (event, activePage) => { - (async () => { - if (activePage === Math.ceil(redemptions.length / ITEMS_PER_PAGE)) { - // In this case we have to load more data and then append them. - await loadRedemptions(activePage); - } - setActivePage(activePage); - })(); + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setPage(0); + setRowsPerPage(parseInt(event.target.value, 10)); }; const searchRedemptions = async (event) => { event.preventDefault(); - if (searchKeyword === '') { - await loadRedemptions(0); - setActivePage(0); - return; - } - setSearching(true); + const formData = new FormData(event.target); + setPage(0); + setSearchKeyword(formData.get('keyword')); + }; + const fetchData = async (page, rowsPerPage, keyword, order, orderBy) => { + setSearching(true); try { - const res = await API.get(`/api/redemption/search?keyword=${searchKeyword}`); + if (orderBy) { + orderBy = order === 'desc' ? '-' + orderBy : orderBy; + } + const res = await API.get(`/api/redemption/`, { + params: { + page: page + 1, + size: rowsPerPage, + keyword: keyword, + order: orderBy + } + }); const { success, message, data } = res.data; if (success) { - setRedemptions(data); - setActivePage(0); + setListCount(data.total_count); + setRedemptions(data.data); } else { showError(message); } } catch (error) { - console.log(error); + console.error(error); } - setSearching(false); }; - const handleSearchKeyword = (event) => { - setSearchKeyword(event.target.value); + // 处理刷新 + const handleRefresh = async () => { + setOrderBy('id'); + setOrder('desc'); + setRefreshFlag(!refreshFlag); }; + useEffect(() => { + fetchData(page, rowsPerPage, searchKeyword, order, orderBy); + }, [page, rowsPerPage, searchKeyword, order, orderBy, refreshFlag]); + const manageRedemptions = async (id, action, value) => { const url = '/api/redemption/'; let data = { id }; @@ -110,7 +117,7 @@ export default function Redemption() { if (success) { showSuccess('操作成功完成!'); if (action === 'delete') { - await loadRedemptions(0); + await handleRefresh(); } } else { showError(message); @@ -122,13 +129,6 @@ export default function Redemption() { } }; - // 处理刷新 - const handleRefresh = async () => { - await loadRedemptions(0); - setActivePage(0); - setSearchKeyword(''); - }; - const handleOpenModal = (redemptionId) => { setEditRedemptionId(redemptionId); setOpenModal(true); @@ -146,15 +146,6 @@ export default function Redemption() { } }; - useEffect(() => { - loadRedemptions(0) - .then() - .catch((reason) => { - showError(reason); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return ( <> @@ -166,7 +157,7 @@ export default function Redemption() { - +
- + - {redemptions.slice(activePage * ITEMS_PER_PAGE, (activePage + 1) * ITEMS_PER_PAGE).map((row) => ( + {redemptions.map((row) => ( diff --git a/web/src/views/Token/component/TableHead.js b/web/src/views/Token/component/TableHead.js deleted file mode 100644 index aadf1f6e..00000000 --- a/web/src/views/Token/component/TableHead.js +++ /dev/null @@ -1,19 +0,0 @@ -import { TableCell, TableHead, TableRow } from '@mui/material'; - -const TokenTableHead = () => { - return ( - - - 名称 - 状态 - 已用额度 - 剩余额度 - 创建时间 - 过期时间 - 操作 - - - ); -}; - -export default TokenTableHead; diff --git a/web/src/views/Token/index.js b/web/src/views/Token/index.js index 215c5bc0..aeb6d599 100644 --- a/web/src/views/Token/index.js +++ b/web/src/views/Token/index.js @@ -13,94 +13,91 @@ import Toolbar from '@mui/material/Toolbar'; import { Button, Card, Box, Stack, Container, Typography } from '@mui/material'; import TokensTableRow from './component/TableRow'; -import TokenTableHead from './component/TableHead'; +import KeywordTableHead from 'ui-component/TableHead'; import TableToolBar from 'ui-component/TableToolBar'; import { API } from 'utils/api'; -import { ITEMS_PER_PAGE } from 'constants'; import { IconRefresh, IconPlus } from '@tabler/icons-react'; import EditeModal from './component/EditModal'; import { useSelector } from 'react-redux'; +import { ITEMS_PER_PAGE } from 'constants'; export default function Token() { - const [tokens, setTokens] = useState([]); - const [activePage, setActivePage] = useState(0); + const [page, setPage] = useState(0); + const [order, setOrder] = useState('desc'); + const [orderBy, setOrderBy] = useState('id'); + const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE); + const [listCount, setListCount] = useState(0); const [searching, setSearching] = useState(false); const [searchKeyword, setSearchKeyword] = useState(''); + const [tokens, setTokens] = useState([]); + const [refreshFlag, setRefreshFlag] = useState(false); + const [openModal, setOpenModal] = useState(false); const [editTokenId, setEditTokenId] = useState(0); const siteInfo = useSelector((state) => state.siteInfo); - const loadTokens = async (startIdx) => { - setSearching(true); - try { - const res = await API.get(`/api/token/?p=${startIdx}`); - const { success, message, data } = res.data; - if (success) { - if (startIdx === 0) { - setTokens(data); - } else { - let newTokens = [...tokens]; - newTokens.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data); - setTokens(newTokens); - } - } else { - showError(message); - } - } catch (error) { - console.log(error); + const handleSort = (event, id) => { + const isAsc = orderBy === id && order === 'asc'; + if (id !== '') { + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(id); } - - setSearching(false); }; - useEffect(() => { - loadTokens(0) - .then() - .catch((reason) => { - showError(reason); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; - const onPaginationChange = (event, activePage) => { - (async () => { - if (activePage === Math.ceil(tokens.length / ITEMS_PER_PAGE)) { - // In this case we have to load more data and then append them. - await loadTokens(activePage); - } - setActivePage(activePage); - })(); + const handleChangeRowsPerPage = (event) => { + setPage(0); + setRowsPerPage(parseInt(event.target.value, 10)); }; const searchTokens = async (event) => { event.preventDefault(); - if (searchKeyword === '') { - await loadTokens(0); - setActivePage(0); - return; - } - setSearching(true); + const formData = new FormData(event.target); + setPage(0); + setSearchKeyword(formData.get('keyword')); + }; + const fetchData = async (page, rowsPerPage, keyword, order, orderBy) => { + setSearching(true); try { - const res = await API.get(`/api/token/search?keyword=${searchKeyword}`); + if (orderBy) { + orderBy = order === 'desc' ? '-' + orderBy : orderBy; + } + const res = await API.get(`/api/token/`, { + params: { + page: page + 1, + size: rowsPerPage, + keyword: keyword, + order: orderBy + } + }); const { success, message, data } = res.data; if (success) { - setTokens(data); - setActivePage(0); + setListCount(data.total_count); + setTokens(data.data); } else { showError(message); } } catch (error) { - return; + console.error(error); } - setSearching(false); }; - const handleSearchKeyword = (event) => { - setSearchKeyword(event.target.value); + // 处理刷新 + const handleRefresh = async () => { + setOrderBy('id'); + setOrder('desc'); + setRefreshFlag(!refreshFlag); }; + useEffect(() => { + fetchData(page, rowsPerPage, searchKeyword, order, orderBy); + }, [page, rowsPerPage, searchKeyword, order, orderBy, refreshFlag]); + const manageToken = async (id, action, value) => { const url = '/api/token/'; let data = { id }; @@ -133,11 +130,6 @@ export default function Token() { } }; - // 处理刷新 - const handleRefresh = async () => { - await loadTokens(activePage); - }; - const handleOpenModal = (tokenId) => { setEditTokenId(tokenId); setOpenModal(true); @@ -178,7 +170,7 @@ export default function Token() { - +
- + - {tokens.slice(activePage * ITEMS_PER_PAGE, (activePage + 1) * ITEMS_PER_PAGE).map((row) => ( + {tokens.map((row) => ( diff --git a/web/src/views/User/component/TableHead.js b/web/src/views/User/component/TableHead.js deleted file mode 100644 index b7a04165..00000000 --- a/web/src/views/User/component/TableHead.js +++ /dev/null @@ -1,20 +0,0 @@ -import { TableCell, TableHead, TableRow } from '@mui/material'; - -const UsersTableHead = () => { - return ( - - - ID - 用户名 - 分组 - 统计信息 - 用户角色 - 绑定 - 状态 - 操作 - - - ); -}; - -export default UsersTableHead; diff --git a/web/src/views/User/component/TableRow.js b/web/src/views/User/component/TableRow.js index 5722a751..ae3bf734 100644 --- a/web/src/views/User/component/TableRow.js +++ b/web/src/views/User/component/TableRow.js @@ -19,7 +19,7 @@ import { import Label from 'ui-component/Label'; import TableSwitch from 'ui-component/Switch'; -import { renderQuota, renderNumber } from 'utils/common'; +import { renderQuota, renderNumber, timestamp2string } from 'utils/common'; import { IconDotsVertical, IconEdit, IconTrash, IconUser, IconBrandWechat, IconBrandGithub, IconMail } from '@tabler/icons-react'; import { useTheme } from '@mui/material/styles'; @@ -119,7 +119,7 @@ export default function UsersTableRow({ item, manageUser, handleOpenModal, setMo - + {item.created_time === 0 ? '未知' : timestamp2string(item.created_time)} {' '} diff --git a/web/src/views/User/index.js b/web/src/views/User/index.js index 1983992f..4863d4cb 100644 --- a/web/src/views/User/index.js +++ b/web/src/views/User/index.js @@ -12,7 +12,7 @@ import Toolbar from '@mui/material/Toolbar'; import { Button, Card, Box, Stack, Container, Typography } from '@mui/material'; import UsersTableRow from './component/TableRow'; -import UsersTableHead from './component/TableHead'; +import KeywordTableHead from 'ui-component/TableHead'; import TableToolBar from 'ui-component/TableToolBar'; import { API } from 'utils/api'; import { ITEMS_PER_PAGE } from 'constants'; @@ -21,74 +21,81 @@ import EditeModal from './component/EditModal'; // ---------------------------------------------------------------------- export default function Users() { - const [users, setUsers] = useState([]); - const [activePage, setActivePage] = useState(0); + const [page, setPage] = useState(0); + const [order, setOrder] = useState('desc'); + const [orderBy, setOrderBy] = useState('id'); + const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE); + const [listCount, setListCount] = useState(0); const [searching, setSearching] = useState(false); const [searchKeyword, setSearchKeyword] = useState(''); + const [users, setUsers] = useState([]); + const [refreshFlag, setRefreshFlag] = useState(false); + const [openModal, setOpenModal] = useState(false); const [editUserId, setEditUserId] = useState(0); - const loadUsers = async (startIdx) => { - setSearching(true); - try { - const res = await API.get(`/api/user/?p=${startIdx}`); - const { success, message, data } = res.data; - if (success) { - if (startIdx === 0) { - setUsers(data); - } else { - let newUsers = [...users]; - newUsers.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data); - setUsers(newUsers); - } - } else { - showError(message); - } - setSearching(false); - } catch (error) { - setSearching(false); - return; + const handleSort = (event, id) => { + const isAsc = orderBy === id && order === 'asc'; + if (id !== '') { + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(id); } }; - const onPaginationChange = (event, activePage) => { - (async () => { - if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE)) { - // In this case we have to load more data and then append them. - await loadUsers(activePage); - } - setActivePage(activePage); - })(); + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setPage(0); + setRowsPerPage(parseInt(event.target.value, 10)); }; const searchUsers = async (event) => { event.preventDefault(); - if (searchKeyword === '') { - await loadUsers(0); - setActivePage(0); - return; - } + const formData = new FormData(event.target); + setPage(0); + setSearchKeyword(formData.get('keyword')); + }; + + const fetchData = async (page, rowsPerPage, keyword, order, orderBy) => { setSearching(true); try { - const res = await API.get(`/api/user/search?keyword=${searchKeyword}`); + if (orderBy) { + orderBy = order === 'desc' ? '-' + orderBy : orderBy; + } + const res = await API.get(`/api/user/`, { + params: { + page: page + 1, + size: rowsPerPage, + keyword: keyword, + order: orderBy + } + }); const { success, message, data } = res.data; if (success) { - setUsers(data); - setActivePage(0); + setListCount(data.total_count); + setUsers(data.data); } else { showError(message); } - setSearching(false); } catch (error) { - setSearching(false); - return; + console.error(error); } + setSearching(false); }; - const handleSearchKeyword = (event) => { - setSearchKeyword(event.target.value); + // 处理刷新 + const handleRefresh = async () => { + setOrderBy('id'); + setOrder('desc'); + setRefreshFlag(!refreshFlag); }; + useEffect(() => { + fetchData(page, rowsPerPage, searchKeyword, order, orderBy); + }, [page, rowsPerPage, searchKeyword, order, orderBy, refreshFlag]); + const manageUser = async (username, action, value) => { const url = '/api/user/manage'; let data = { username: username, action: '' }; @@ -110,7 +117,7 @@ export default function Users() { const { success, message } = res.data; if (success) { showSuccess('操作成功完成!'); - await loadUsers(activePage); + await handleRefresh(); } else { showError(message); } @@ -121,11 +128,6 @@ export default function Users() { } }; - // 处理刷新 - const handleRefresh = async () => { - await loadUsers(activePage); - }; - const handleOpenModal = (userId) => { setEditUserId(userId); setOpenModal(true); @@ -143,15 +145,6 @@ export default function Users() { } }; - useEffect(() => { - loadUsers(0) - .then() - .catch((reason) => { - showError(reason); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return ( <> @@ -163,11 +156,7 @@ export default function Users() { - +
- + - {users.slice(activePage * ITEMS_PER_PAGE, (activePage + 1) * ITEMS_PER_PAGE).map((row) => ( + {users.map((row) => (