Browse Source

🚀 feat(pagination): unify backend-driven pagination & improve channel tag aggregation

SUMMARY
• Migrated Token, Task, Midjourney, Channel, Redemption tables to true server-side pagination.
• Added total / page / page_size metadata in API responses; switched all affected React tables to consume new structure.
• Implemented counting helpers:
  – model/token.go CountUserTokens
  – model/task.go TaskCountAllTasks / TaskCountAllUserTask
  – model/midjourney.go CountAllTasks / CountAllUserTask
  – model/channel.go CountAllChannels / CountAllTags
• Refactored controllers (token, task, midjourney, channel) for 1-based paging & aggregated returns.
• Redesigned `ChannelsTable.js`:
  – `loadChannels`, `syncPageData`, `enrichChannels` for tag-mode grouping without recursion.
  – Fixed runtime white-screen (maximum call-stack) by removing child duplication.
  – Pagination, search, tag-mode, idSort all hot-reload correctly.
• Removed unused `log` import in controller/midjourney.go.

BREAKING CHANGES
Front-end consumers must now expect data.items / total / page / page_size from list endpoints (`/api/channel`, `/api/task`, `/api/mj`, `/api/token`, etc.).
Apple\Apple 6 months ago
parent
commit
dcefd7dfb4

+ 21 - 14
controller/channel.go

@@ -43,22 +43,23 @@ type OpenAIModelsResponse struct {
 func GetAllChannels(c *gin.Context) {
 	p, _ := strconv.Atoi(c.Query("p"))
 	pageSize, _ := strconv.Atoi(c.Query("page_size"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
 	}
-	if pageSize < 0 {
+	if pageSize < 1 {
 		pageSize = common.ItemsPerPage
 	}
 	channelData := make([]*model.Channel, 0)
 	idSort, _ := strconv.ParseBool(c.Query("id_sort"))
 	enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
+
+	var total int64
+
 	if enableTagMode {
-		tags, err := model.GetPaginatedTags(p*pageSize, pageSize)
+		// tag 分页:先分页 tag,再取各 tag 下 channels
+		tags, err := model.GetPaginatedTags((p-1)*pageSize, pageSize)
 		if err != nil {
-			c.JSON(http.StatusOK, gin.H{
-				"success": false,
-				"message": err.Error(),
-			})
+			c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
 			return
 		}
 		for _, tag := range tags {
@@ -69,21 +70,27 @@ func GetAllChannels(c *gin.Context) {
 				}
 			}
 		}
+		// 计算 tag 总数用于分页
+		total, _ = model.CountAllTags()
 	} else {
-		channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
+		channels, err := model.GetAllChannels((p-1)*pageSize, pageSize, false, idSort)
 		if err != nil {
-			c.JSON(http.StatusOK, gin.H{
-				"success": false,
-				"message": err.Error(),
-			})
+			c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
 			return
 		}
 		channelData = channels
+		total, _ = model.CountAllChannels()
 	}
+
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",
-		"data":    channelData,
+		"data": gin.H{
+			"items":     channelData,
+			"total":     total,
+			"page":      p,
+			"page_size": pageSize,
+		},
 	})
 	return
 }

+ 34 - 20
controller/midjourney.go

@@ -7,7 +7,6 @@ import (
 	"fmt"
 	"github.com/gin-gonic/gin"
 	"io"
-	"log"
 	"net/http"
 	"one-api/common"
 	"one-api/dto"
@@ -215,8 +214,12 @@ func checkMjTaskNeedUpdate(oldTask *model.Midjourney, newTask dto.MidjourneyDto)
 
 func GetAllMidjourney(c *gin.Context) {
 	p, _ := strconv.Atoi(c.Query("p"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
+	}
+	pageSize, _ := strconv.Atoi(c.Query("page_size"))
+	if pageSize <= 0 {
+		pageSize = common.ItemsPerPage
 	}
 
 	// 解析其他查询参数
@@ -227,31 +230,38 @@ func GetAllMidjourney(c *gin.Context) {
 		EndTimestamp:   c.Query("end_timestamp"),
 	}
 
-	logs := model.GetAllTasks(p*common.ItemsPerPage, common.ItemsPerPage, queryParams)
-	if logs == nil {
-		logs = make([]*model.Midjourney, 0)
-	}
+	items := model.GetAllTasks((p-1)*pageSize, pageSize, queryParams)
+	total := model.CountAllTasks(queryParams)
+
 	if setting.MjForwardUrlEnabled {
-		for i, midjourney := range logs {
+		for i, midjourney := range items {
 			midjourney.ImageUrl = setting.ServerAddress + "/mj/image/" + midjourney.MjId
-			logs[i] = midjourney
+			items[i] = midjourney
 		}
 	}
 	c.JSON(200, gin.H{
 		"success": true,
 		"message": "",
-		"data":    logs,
+		"data": gin.H{
+			"items":     items,
+			"total":     total,
+			"page":      p,
+			"page_size": pageSize,
+		},
 	})
 }
 
 func GetUserMidjourney(c *gin.Context) {
 	p, _ := strconv.Atoi(c.Query("p"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
+	}
+	pageSize, _ := strconv.Atoi(c.Query("page_size"))
+	if pageSize <= 0 {
+		pageSize = common.ItemsPerPage
 	}
 
 	userId := c.GetInt("id")
-	log.Printf("userId = %d \n", userId)
 
 	queryParams := model.TaskQueryParams{
 		MjID:           c.Query("mj_id"),
@@ -259,19 +269,23 @@ func GetUserMidjourney(c *gin.Context) {
 		EndTimestamp:   c.Query("end_timestamp"),
 	}
 
-	logs := model.GetAllUserTask(userId, p*common.ItemsPerPage, common.ItemsPerPage, queryParams)
-	if logs == nil {
-		logs = make([]*model.Midjourney, 0)
-	}
+	items := model.GetAllUserTask(userId, (p-1)*pageSize, pageSize, queryParams)
+	total := model.CountAllUserTask(userId, queryParams)
+
 	if setting.MjForwardUrlEnabled {
-		for i, midjourney := range logs {
+		for i, midjourney := range items {
 			midjourney.ImageUrl = setting.ServerAddress + "/mj/image/" + midjourney.MjId
-			logs[i] = midjourney
+			items[i] = midjourney
 		}
 	}
 	c.JSON(200, gin.H{
 		"success": true,
 		"message": "",
-		"data":    logs,
+		"data": gin.H{
+			"items":     items,
+			"total":     total,
+			"page":      p,
+			"page_size": pageSize,
+		},
 	})
 }

+ 30 - 14
controller/task.go

@@ -224,9 +224,14 @@ func checkTaskNeedUpdate(oldTask *model.Task, newTask dto.SunoDataResponse) bool
 
 func GetAllTask(c *gin.Context) {
 	p, _ := strconv.Atoi(c.Query("p"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
 	}
+	pageSize, _ := strconv.Atoi(c.Query("page_size"))
+	if pageSize <= 0 {
+		pageSize = common.ItemsPerPage
+	}
+
 	startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
 	endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
 	// 解析其他查询参数
@@ -237,24 +242,32 @@ func GetAllTask(c *gin.Context) {
 		Action:         c.Query("action"),
 		StartTimestamp: startTimestamp,
 		EndTimestamp:   endTimestamp,
+		ChannelID:      c.Query("channel_id"),
 	}
 
-	logs := model.TaskGetAllTasks(p*common.ItemsPerPage, common.ItemsPerPage, queryParams)
-	if logs == nil {
-		logs = make([]*model.Task, 0)
-	}
+	items := model.TaskGetAllTasks((p-1)*pageSize, pageSize, queryParams)
+	total := model.TaskCountAllTasks(queryParams)
 
 	c.JSON(200, gin.H{
 		"success": true,
 		"message": "",
-		"data":    logs,
+		"data": gin.H{
+			"items":     items,
+			"total":     total,
+			"page":      p,
+			"page_size": pageSize,
+		},
 	})
 }
 
 func GetUserTask(c *gin.Context) {
 	p, _ := strconv.Atoi(c.Query("p"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
+	}
+	pageSize, _ := strconv.Atoi(c.Query("page_size"))
+	if pageSize <= 0 {
+		pageSize = common.ItemsPerPage
 	}
 
 	userId := c.GetInt("id")
@@ -271,14 +284,17 @@ func GetUserTask(c *gin.Context) {
 		EndTimestamp:   endTimestamp,
 	}
 
-	logs := model.TaskGetAllUserTask(userId, p*common.ItemsPerPage, common.ItemsPerPage, queryParams)
-	if logs == nil {
-		logs = make([]*model.Task, 0)
-	}
+	items := model.TaskGetAllUserTask(userId, (p-1)*pageSize, pageSize, queryParams)
+	total := model.TaskCountAllUserTask(userId, queryParams)
 
 	c.JSON(200, gin.H{
 		"success": true,
 		"message": "",
-		"data":    logs,
+		"data": gin.H{
+			"items":     items,
+			"total":     total,
+			"page":      p,
+			"page_size": pageSize,
+		},
 	})
 }

+ 12 - 4
controller/token.go

@@ -12,15 +12,15 @@ func GetAllTokens(c *gin.Context) {
 	userId := c.GetInt("id")
 	p, _ := strconv.Atoi(c.Query("p"))
 	size, _ := strconv.Atoi(c.Query("size"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
 	}
 	if size <= 0 {
 		size = common.ItemsPerPage
 	} else if size > 100 {
 		size = 100
 	}
-	tokens, err := model.GetAllUserTokens(userId, p*size, size)
+	tokens, err := model.GetAllUserTokens(userId, (p-1)*size, size)
 	if err != nil {
 		c.JSON(http.StatusOK, gin.H{
 			"success": false,
@@ -28,10 +28,18 @@ func GetAllTokens(c *gin.Context) {
 		})
 		return
 	}
+	// Get total count for pagination
+	total, _ := model.CountUserTokens(userId)
+
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",
-		"data":    tokens,
+		"data": gin.H{
+			"items":     tokens,
+			"total":     total,
+			"page":      p,
+			"page_size": size,
+		},
 	})
 	return
 }

+ 14 - 0
model/channel.go

@@ -583,3 +583,17 @@ func BatchSetChannelTag(ids []int, tag *string) error {
 	// 提交事务
 	return tx.Commit().Error
 }
+
+// CountAllChannels returns total channels in DB
+func CountAllChannels() (int64, error) {
+	var total int64
+	err := DB.Model(&Channel{}).Count(&total).Error
+	return total, err
+}
+
+// CountAllTags returns number of non-empty distinct tags
+func CountAllTags() (int64, error) {
+	var total int64
+	err := DB.Model(&Channel{}).Where("tag is not null AND tag != ''").Distinct("tag").Count(&total).Error
+	return total, err
+}

+ 37 - 0
model/midjourney.go

@@ -166,3 +166,40 @@ func MjBulkUpdateByTaskIds(taskIDs []int, params map[string]any) error {
 		Where("id in (?)", taskIDs).
 		Updates(params).Error
 }
+
+// CountAllTasks returns total midjourney tasks for admin query
+func CountAllTasks(queryParams TaskQueryParams) int64 {
+	var total int64
+	query := DB.Model(&Midjourney{})
+	if queryParams.ChannelID != "" {
+		query = query.Where("channel_id = ?", queryParams.ChannelID)
+	}
+	if queryParams.MjID != "" {
+		query = query.Where("mj_id = ?", queryParams.MjID)
+	}
+	if queryParams.StartTimestamp != "" {
+		query = query.Where("submit_time >= ?", queryParams.StartTimestamp)
+	}
+	if queryParams.EndTimestamp != "" {
+		query = query.Where("submit_time <= ?", queryParams.EndTimestamp)
+	}
+	_ = query.Count(&total).Error
+	return total
+}
+
+// CountAllUserTask returns total midjourney tasks for user
+func CountAllUserTask(userId int, queryParams TaskQueryParams) int64 {
+	var total int64
+	query := DB.Model(&Midjourney{}).Where("user_id = ?", userId)
+	if queryParams.MjID != "" {
+		query = query.Where("mj_id = ?", queryParams.MjID)
+	}
+	if queryParams.StartTimestamp != "" {
+		query = query.Where("submit_time >= ?", queryParams.StartTimestamp)
+	}
+	if queryParams.EndTimestamp != "" {
+		query = query.Where("submit_time <= ?", queryParams.EndTimestamp)
+	}
+	_ = query.Count(&total).Error
+	return total
+}

+ 61 - 0
model/task.go

@@ -302,3 +302,64 @@ func SumUsedTaskQuota(queryParams SyncTaskQueryParams) (stat []TaskQuotaUsage, e
 	err = query.Select("mode, sum(quota) as count").Group("mode").Find(&stat).Error
 	return stat, err
 }
+
+// TaskCountAllTasks returns total tasks that match the given query params (admin usage)
+func TaskCountAllTasks(queryParams SyncTaskQueryParams) int64 {
+	var total int64
+	query := DB.Model(&Task{})
+	if queryParams.ChannelID != "" {
+		query = query.Where("channel_id = ?", queryParams.ChannelID)
+	}
+	if queryParams.Platform != "" {
+		query = query.Where("platform = ?", queryParams.Platform)
+	}
+	if queryParams.UserID != "" {
+		query = query.Where("user_id = ?", queryParams.UserID)
+	}
+	if len(queryParams.UserIDs) != 0 {
+		query = query.Where("user_id in (?)", queryParams.UserIDs)
+	}
+	if queryParams.TaskID != "" {
+		query = query.Where("task_id = ?", queryParams.TaskID)
+	}
+	if queryParams.Action != "" {
+		query = query.Where("action = ?", queryParams.Action)
+	}
+	if queryParams.Status != "" {
+		query = query.Where("status = ?", queryParams.Status)
+	}
+	if queryParams.StartTimestamp != 0 {
+		query = query.Where("submit_time >= ?", queryParams.StartTimestamp)
+	}
+	if queryParams.EndTimestamp != 0 {
+		query = query.Where("submit_time <= ?", queryParams.EndTimestamp)
+	}
+	_ = query.Count(&total).Error
+	return total
+}
+
+// TaskCountAllUserTask returns total tasks for given user
+func TaskCountAllUserTask(userId int, queryParams SyncTaskQueryParams) int64 {
+	var total int64
+	query := DB.Model(&Task{}).Where("user_id = ?", userId)
+	if queryParams.TaskID != "" {
+		query = query.Where("task_id = ?", queryParams.TaskID)
+	}
+	if queryParams.Action != "" {
+		query = query.Where("action = ?", queryParams.Action)
+	}
+	if queryParams.Status != "" {
+		query = query.Where("status = ?", queryParams.Status)
+	}
+	if queryParams.Platform != "" {
+		query = query.Where("platform = ?", queryParams.Platform)
+	}
+	if queryParams.StartTimestamp != 0 {
+		query = query.Where("submit_time >= ?", queryParams.StartTimestamp)
+	}
+	if queryParams.EndTimestamp != 0 {
+		query = query.Where("submit_time <= ?", queryParams.EndTimestamp)
+	}
+	_ = query.Count(&total).Error
+	return total
+}

+ 7 - 0
model/token.go

@@ -320,3 +320,10 @@ func decreaseTokenQuota(id int, quota int) (err error) {
 	).Error
 	return err
 }
+
+// CountUserTokens returns total number of tokens for the given user, used for pagination
+func CountUserTokens(userId int) (int64, error) {
+	var total int64
+	err := DB.Model(&Token{}).Where("user_id = ?", userId).Count(&total).Error
+	return total, err
+}

+ 14 - 33
web/src/components/table/ChannelsTable.js

@@ -865,32 +865,22 @@ const ChannelsTable = () => {
         tagChannelDates.response_time = tagChannelDates.response_time / 2;
       }
     }
-    // data.key = '' + data.id
     setChannels(channelDates);
-    if (channelDates.length >= pageSize) {
-      setChannelCount(channelDates.length + pageSize);
-    } else {
-      setChannelCount(channelDates.length);
-    }
   };
 
-  const loadChannels = async (startIdx, pageSize, idSort, enableTagMode) => {
+  const loadChannels = async (page, pageSize, idSort, enableTagMode) => {
     setLoading(true);
     const res = await API.get(
-      `/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}`,
+      `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}`,
     );
     if (res === undefined) {
       return;
     }
     const { success, message, data } = res.data;
     if (success) {
-      if (startIdx === 0) {
-        setChannelFormat(data, enableTagMode);
-      } else {
-        let newChannels = [...channels];
-        newChannels.splice(startIdx * pageSize, data.length, ...data);
-        setChannelFormat(newChannels, enableTagMode);
-      }
+      const { items, total } = data;
+      setChannelFormat(items, enableTagMode);
+      setChannelCount(total);
     } else {
       showError(message);
     }
@@ -903,7 +893,6 @@ const ChannelsTable = () => {
     channelToCopy.created_time = null;
     channelToCopy.balance = 0;
     channelToCopy.used_quota = 0;
-    // 删除可能导致类型不匹配的字段
     delete channelToCopy.test_time;
     delete channelToCopy.response_time;
     if (!channelToCopy) {
@@ -927,7 +916,7 @@ const ChannelsTable = () => {
   const refresh = async () => {
     const { searchKeyword, searchGroup, searchModel } = getFormValues();
     if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
-      await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
+      await loadChannels(activePage, pageSize, idSort, enableTagMode);
     } else {
       await searchChannels(enableTagMode);
     }
@@ -944,7 +933,7 @@ const ChannelsTable = () => {
     setPageSize(localPageSize);
     setEnableTagMode(localEnableTagMode);
     setEnableBatchDelete(localEnableBatchDelete);
-    loadChannels(0, localPageSize, localIdSort, localEnableTagMode)
+    loadChannels(1, localPageSize, localIdSort, localEnableTagMode)
       .then()
       .catch((reason) => {
         showError(reason);
@@ -1052,7 +1041,6 @@ const ChannelsTable = () => {
     try {
       if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
         await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
-        // setActivePage(1);
         return;
       }
 
@@ -1191,24 +1179,18 @@ const ChannelsTable = () => {
     }
   };
 
-  let pageData = channels.slice(
-    (activePage - 1) * pageSize,
-    activePage * pageSize,
-  );
+  let pageData = channels;
 
   const handlePageChange = (page) => {
     setActivePage(page);
-    if (page === Math.ceil(channels.length / pageSize) + 1) {
-      // In this case we have to load more data and then append them.
-      loadChannels(page - 1, pageSize, idSort, enableTagMode).then((r) => { });
-    }
+    loadChannels(page, pageSize, idSort, enableTagMode).then(() => { });
   };
 
   const handlePageSizeChange = async (size) => {
     localStorage.setItem('page-size', size + '');
     setPageSize(size);
     setActivePage(1);
-    loadChannels(0, size, idSort, enableTagMode)
+    loadChannels(1, size, idSort, enableTagMode)
       .then()
       .catch((reason) => {
         showError(reason);
@@ -1218,8 +1200,6 @@ const ChannelsTable = () => {
   const fetchGroups = async () => {
     try {
       let res = await API.get(`/api/group/`);
-      // add 'all' option
-      // res.data.data.unshift('all');
       if (res === undefined) {
         return;
       }
@@ -1514,7 +1494,7 @@ const ChannelsTable = () => {
               onChange={(v) => {
                 localStorage.setItem('id-sort', v + '');
                 setIdSort(v);
-                loadChannels(0, pageSize, v, enableTagMode);
+                loadChannels(activePage, pageSize, v, enableTagMode);
               }}
             />
           </div>
@@ -1541,7 +1521,8 @@ const ChannelsTable = () => {
               onChange={(v) => {
                 localStorage.setItem('enable-tag-mode', v + '');
                 setEnableTagMode(v);
-                loadChannels(0, pageSize, idSort, v);
+                setActivePage(1);
+                loadChannels(1, pageSize, idSort, v);
               }}
             />
           </div>
@@ -1703,7 +1684,7 @@ const ChannelsTable = () => {
             formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
               start: page.currentStart,
               end: page.currentEnd,
-              total: channels.length,
+              total: channelCount,
             }),
             onPageSizeChange: (size) => {
               handlePageSizeChange(size);

+ 27 - 45
web/src/components/table/MjLogsTable.js

@@ -601,7 +601,7 @@ const LogsTable = () => {
   const [logs, setLogs] = useState([]);
   const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
-  const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
+  const [logCount, setLogCount] = useState(0);
   const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
   const [isModalOpenurl, setIsModalOpenurl] = useState(false);
   const [showBanner, setShowBanner] = useState(false);
@@ -649,69 +649,53 @@ const LogsTable = () => {
     };
   };
 
-  const setLogsFormat = (logs) => {
-    for (let i = 0; i < logs.length; i++) {
-      logs[i].timestamp2string = timestamp2string(logs[i].created_at);
-      logs[i].key = '' + logs[i].id;
-    }
-    // data.key = '' + data.id
-    setLogs(logs);
-    setLogCount(logs.length + pageSize);
-    // console.log(logCount);
+  const enrichLogs = (items) => {
+    return items.map((log) => ({
+      ...log,
+      timestamp2string: timestamp2string(log.created_at),
+      key: '' + log.id,
+    }));
   };
 
-  const loadLogs = async (startIdx, pageSize = ITEMS_PER_PAGE) => {
-    setLoading(true);
+  const syncPageData = (payload) => {
+    const items = enrichLogs(payload.items || []);
+    setLogs(items);
+    setLogCount(payload.total || 0);
+    setActivePage(payload.page || 1);
+    setPageSize(payload.page_size || pageSize);
+  };
 
-    let url = '';
+  const loadLogs = async (page = 1, size = pageSize) => {
+    setLoading(true);
     const { channel_id, mj_id, start_timestamp, end_timestamp } = getFormValues();
     let localStartTimestamp = Date.parse(start_timestamp);
     let localEndTimestamp = Date.parse(end_timestamp);
-    if (isAdminUser) {
-      url = `/api/mj/?p=${startIdx}&page_size=${pageSize}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
-    } else {
-      url = `/api/mj/self/?p=${startIdx}&page_size=${pageSize}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
-    }
+    const url = isAdminUser
+      ? `/api/mj/?p=${page}&page_size=${size}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
+      : `/api/mj/self/?p=${page}&page_size=${size}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
     const res = await API.get(url);
     const { success, message, data } = res.data;
     if (success) {
-      if (startIdx === 0) {
-        setLogsFormat(data);
-      } else {
-        let newLogs = [...logs];
-        newLogs.splice(startIdx * pageSize, data.length, ...data);
-        setLogsFormat(newLogs);
-      }
+      syncPageData(data);
     } else {
       showError(message);
     }
     setLoading(false);
   };
 
-  const pageData = logs.slice(
-    (activePage - 1) * pageSize,
-    activePage * pageSize,
-  );
+  const pageData = logs;
 
   const handlePageChange = (page) => {
-    setActivePage(page);
-    if (page === Math.ceil(logs.length / pageSize) + 1) {
-      // In this case we have to load more data and then append them.
-      loadLogs(page - 1, pageSize).then((r) => { });
-    }
+    loadLogs(page, pageSize).then();
   };
 
   const handlePageSizeChange = async (size) => {
     localStorage.setItem('mj-page-size', size + '');
-    setPageSize(size);
-    setActivePage(1);
-    await loadLogs(0, size);
+    await loadLogs(1, size);
   };
 
   const refresh = async () => {
-    // setLoading(true);
-    setActivePage(1);
-    await loadLogs(0, pageSize);
+    await loadLogs(1, pageSize);
   };
 
   const copyText = async (text) => {
@@ -726,7 +710,7 @@ const LogsTable = () => {
   useEffect(() => {
     const localPageSize = parseInt(localStorage.getItem('mj-page-size')) || ITEMS_PER_PAGE;
     setPageSize(localPageSize);
-    loadLogs(0, localPageSize).then();
+    loadLogs(1, localPageSize).then();
   }, []);
 
   useEffect(() => {
@@ -936,7 +920,7 @@ const LogsTable = () => {
         >
           <Table
             columns={getVisibleColumns()}
-            dataSource={pageData}
+            dataSource={logs}
             rowKey='key'
             loading={loading}
             scroll={{ x: 'max-content' }}
@@ -962,9 +946,7 @@ const LogsTable = () => {
               total: logCount,
               pageSizeOptions: [10, 20, 50, 100],
               showSizeChanger: true,
-              onPageSizeChange: (size) => {
-                handlePageSizeChange(size);
-              },
+              onPageSizeChange: handlePageSizeChange,
               onPageChange: handlePageChange,
             }}
           />

+ 35 - 51
web/src/components/table/TaskLogsTable.js

@@ -451,10 +451,16 @@ const LogsTable = () => {
     return allColumns.filter((column) => visibleColumns[column.key]);
   };
 
-  const [logs, setLogs] = useState([]);
-  const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
-  const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
+  const [logCount, setLogCount] = useState(0);
+  const [logs, setLogs] = useState([]);
+  const [loading, setLoading] = useState(false);
+
+  useEffect(() => {
+    const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
+    setPageSize(localPageSize);
+    loadLogs(1, localPageSize).then();
+  }, []);
 
   let now = new Date();
   // 初始化start_timestamp为前一天
@@ -494,67 +500,53 @@ const LogsTable = () => {
     };
   };
 
-  const setLogsFormat = (logs) => {
-    for (let i = 0; i < logs.length; i++) {
-      logs[i].timestamp2string = timestamp2string(logs[i].created_at);
-      logs[i].key = '' + logs[i].id;
-    }
-    // data.key = '' + data.id
-    setLogs(logs);
-    setLogCount(logs.length + ITEMS_PER_PAGE);
-    // console.log(logCount);
+  const enrichLogs = (items) => {
+    return items.map((log) => ({
+      ...log,
+      timestamp2string: timestamp2string(log.created_at),
+      key: '' + log.id,
+    }));
   };
 
-  const loadLogs = async (startIdx, pageSize = ITEMS_PER_PAGE) => {
-    setLoading(true);
+  const syncPageData = (payload) => {
+    const items = enrichLogs(payload.items || []);
+    setLogs(items);
+    setLogCount(payload.total || 0);
+    setActivePage(payload.page || 1);
+    setPageSize(payload.page_size || pageSize);
+  };
 
-    let url = '';
+  const loadLogs = async (page = 1, size = pageSize) => {
+    setLoading(true);
     const { channel_id, task_id, start_timestamp, end_timestamp } = getFormValues();
     let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000);
     let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000);
-    if (isAdminUser) {
-      url = `/api/task/?p=${startIdx}&page_size=${pageSize}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
-    } else {
-      url = `/api/task/self?p=${startIdx}&page_size=${pageSize}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
-    }
+    let url = isAdminUser
+      ? `/api/task/?p=${page}&page_size=${size}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
+      : `/api/task/self?p=${page}&page_size=${size}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
     const res = await API.get(url);
-    let { success, message, data } = res.data;
+    const { success, message, data } = res.data;
     if (success) {
-      if (startIdx === 0) {
-        setLogsFormat(data);
-      } else {
-        let newLogs = [...logs];
-        newLogs.splice(startIdx * pageSize, data.length, ...data);
-        setLogsFormat(newLogs);
-      }
+      syncPageData(data);
     } else {
       showError(message);
     }
     setLoading(false);
   };
 
-  const pageData = logs.slice(
-    (activePage - 1) * pageSize,
-    activePage * pageSize,
-  );
+  const pageData = logs;
 
   const handlePageChange = (page) => {
-    setActivePage(page);
-    if (page === Math.ceil(logs.length / pageSize) + 1) {
-      loadLogs(page - 1, pageSize).then((r) => { });
-    }
+    loadLogs(page, pageSize).then();
   };
 
   const handlePageSizeChange = async (size) => {
     localStorage.setItem('task-page-size', size + '');
-    setPageSize(size);
-    setActivePage(1);
-    await loadLogs(0, size);
+    await loadLogs(1, size);
   };
 
   const refresh = async () => {
-    setActivePage(1);
-    await loadLogs(0, pageSize);
+    await loadLogs(1, pageSize);
   };
 
   const copyText = async (text) => {
@@ -565,12 +557,6 @@ const LogsTable = () => {
     }
   };
 
-  useEffect(() => {
-    const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
-    setPageSize(localPageSize);
-    loadLogs(0, localPageSize).then();
-  }, []);
-
   // 列选择器模态框
   const renderColumnSelector = () => {
     return (
@@ -763,7 +749,7 @@ const LogsTable = () => {
         >
           <Table
             columns={getVisibleColumns()}
-            dataSource={pageData}
+            dataSource={logs}
             rowKey='key'
             loading={loading}
             scroll={{ x: 'max-content' }}
@@ -789,9 +775,7 @@ const LogsTable = () => {
               total: logCount,
               pageSizeOptions: [10, 20, 50, 100],
               showSizeChanger: true,
-              onPageSizeChange: (size) => {
-                handlePageSizeChange(size);
-              },
+              onPageSizeChange: handlePageSizeChange,
               onPageChange: handlePageChange,
             }}
           />

+ 25 - 37
web/src/components/table/TokensTable.js

@@ -408,31 +408,20 @@ const TokensTable = () => {
     }, 500);
   };
 
-  const setTokensFormat = (tokens) => {
-    setTokens(tokens);
-    if (tokens.length >= pageSize) {
-      setTokenCount(tokens.length + pageSize);
-    } else {
-      setTokenCount(tokens.length);
-    }
+  // 将后端返回的数据写入状态
+  const syncPageData = (payload) => {
+    setTokens(payload.items || []);
+    setTokenCount(payload.total || 0);
+    setActivePage(payload.page || 1);
+    setPageSize(payload.page_size || pageSize);
   };
 
-  let pageData = tokens.slice(
-    (activePage - 1) * pageSize,
-    activePage * pageSize,
-  );
-  const loadTokens = async (startIdx) => {
+  const loadTokens = async (page = 1, size = pageSize) => {
     setLoading(true);
-    const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}`);
+    const res = await API.get(`/api/token/?p=${page}&size=${size}`);
     const { success, message, data } = res.data;
     if (success) {
-      if (startIdx === 0) {
-        setTokensFormat(data);
-      } else {
-        let newTokens = [...tokens];
-        newTokens.splice(startIdx * pageSize, data.length, ...data);
-        setTokensFormat(newTokens);
-      }
+      syncPageData(data);
     } else {
       showError(message);
     }
@@ -440,7 +429,7 @@ const TokensTable = () => {
   };
 
   const refresh = async () => {
-    await loadTokens(activePage - 1);
+    await loadTokens(1);
   };
 
   const copyText = async (text) => {
@@ -473,7 +462,7 @@ const TokensTable = () => {
   };
 
   useEffect(() => {
-    loadTokens(0)
+    loadTokens(1)
       .then()
       .catch((reason) => {
         showError(reason);
@@ -487,7 +476,7 @@ const TokensTable = () => {
 
       if (idx > -1) {
         newDataSource.splice(idx, 1);
-        setTokensFormat(newDataSource);
+        setTokens(newDataSource);
       }
     }
   };
@@ -518,7 +507,7 @@ const TokensTable = () => {
       } else {
         record.status = token.status;
       }
-      setTokensFormat(newTokens);
+      setTokens(newTokens);
     } else {
       showError(message);
     }
@@ -528,8 +517,7 @@ const TokensTable = () => {
   const searchTokens = async () => {
     const { searchKeyword, searchToken } = getFormValues();
     if (searchKeyword === '' && searchToken === '') {
-      await loadTokens(0);
-      setActivePage(1);
+      await loadTokens(1);
       return;
     }
     setSearching(true);
@@ -538,7 +526,8 @@ const TokensTable = () => {
     );
     const { success, message, data } = res.data;
     if (success) {
-      setTokensFormat(data);
+      setTokens(data);
+      setTokenCount(data.length);
       setActivePage(1);
     } else {
       showError(message);
@@ -561,10 +550,12 @@ const TokensTable = () => {
   };
 
   const handlePageChange = (page) => {
-    setActivePage(page);
-    if (page === Math.ceil(tokens.length / pageSize) + 1) {
-      loadTokens(page - 1).then((r) => { });
-    }
+    loadTokens(page, pageSize).then();
+  };
+
+  const handlePageSizeChange = async (size) => {
+    setPageSize(size);
+    await loadTokens(1, size);
   };
 
   const rowSelection = {
@@ -707,7 +698,7 @@ const TokensTable = () => {
       >
         <Table
           columns={columns}
-          dataSource={pageData}
+          dataSource={tokens}
           scroll={{ x: 'max-content' }}
           pagination={{
             currentPage: activePage,
@@ -719,12 +710,9 @@ const TokensTable = () => {
               t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
                 start: page.currentStart,
                 end: page.currentEnd,
-                total: tokens.length,
+                total: tokenCount,
               }),
-            onPageSizeChange: (size) => {
-              setPageSize(size);
-              setActivePage(1);
-            },
+            onPageSizeChange: handlePageSizeChange,
             onPageChange: handlePageChange,
           }}
           loading={loading}