feat: 统一 Admin 主列表分页 (#64)

This commit is contained in:
chengma
2026-08-09 21:51:20 +08:00
parent 5c9ebd968c
commit 155dd3b4a2
23 changed files with 549 additions and 120 deletions
+35 -19
View File
@@ -563,7 +563,7 @@ func (h *Handler) TaskList(c *gin.Context) {
Keyword: c.Query("q"),
}
result, err := service.ListTasksView(h.db, filter)
result, err := service.ListTasksView(h.db, filter, service.ParsePage(c.Query("page")))
if err != nil {
fail(c, http.StatusInternalServerError,
"读取任务列表失败,数据没有被改动。刷新页面重试;一直失败请把这句话报给维护者。")
@@ -577,6 +577,16 @@ func (h *Handler) TaskList(c *gin.Context) {
statusLine = msg + " · " + statusLine
}
values := url.Values{}
if filter.Type != "" {
values.Set("type", string(filter.Type))
}
if filter.Status != "" {
values.Set("status", string(filter.Status))
}
if strings.TrimSpace(filter.Keyword) != "" {
values.Set("q", filter.Keyword)
}
c.HTML(http.StatusOK, "task/list", page(c, "tasks", "采集采购", gin.H{
"Rows": result.Rows,
"Keyword": filter.Keyword,
@@ -586,6 +596,8 @@ func (h *Handler) TaskList(c *gin.Context) {
"TypeOptions": service.TaskTypeOptions(),
"StatusOptions": service.TaskStatusOptions(),
"IsFiltered": result.IsFiltered,
"CurrentPage": result.Page,
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
}))
}
@@ -648,6 +660,9 @@ func (h *Handler) taskRedirect(c *gin.Context, msg string) {
if q := c.PostForm("q"); q != "" {
params.Set("q", q)
}
if p := c.PostForm("page"); p != "" {
params.Set("page", p)
}
if msg != "" {
params.Set("msg", msg)
}
@@ -672,28 +687,16 @@ func (h *Handler) ClientList(c *gin.Context) {
keyword := c.Query("name")
actor := currentUser(c)
views, err := service.ListClientViewsForUser(h.db, actor, keyword, h.onlineThreshold)
result, err := service.ListClientPageForUser(h.db, actor, keyword, h.onlineThreshold,
service.ParsePage(c.Query("page")))
if err != nil {
fail(c, http.StatusInternalServerError,
"读取客户端列表失败,数据没有被改动。请稍后重试,或查看 data/logs/ 里的日志。")
return
}
online := 0
for _, v := range views {
if v.Status == "在线" {
online++
}
}
status := fmt.Sprintf("共 %d 台客户端 · 在线 %d · 离线 %d",
len(views), online, len(views)-online)
if len(views) == 0 {
if actor != nil && actor.IsAdmin() {
status = "还没有客户端。客户端第一次调用领取接口时会自动登记。"
} else {
status = "当前没有绑定给你的客户端,请联系管理员。"
}
}
status := fmt.Sprintf("共 %d 台客户端 · 在线 %d · 离线 %d · 第 %d/%d 页",
result.Total, result.Online, result.Offline, result.Page, result.TotalPages)
purchasers := []model.User{}
if actor != nil && actor.IsAdmin() {
@@ -705,9 +708,16 @@ func (h *Handler) ClientList(c *gin.Context) {
}
}
values := url.Values{}
if strings.TrimSpace(keyword) != "" {
values.Set("name", keyword)
}
c.HTML(http.StatusOK, "client/list", page(c, "clients", "客户端列表", gin.H{
"Keyword": keyword, "Rows": views, "Status": status,
"Keyword": keyword, "Rows": result.Rows, "Status": status,
"Purchasers": purchasers, "Message": c.Query("msg"), "Error": c.Query("error"),
"CurrentPage": result.Page,
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
"IsFiltered": strings.TrimSpace(keyword) != "",
}))
}
@@ -754,6 +764,12 @@ func (h *Handler) clientActionFailure(c *gin.Context, err error, fallback string
func redirectClients(c *gin.Context, message, errorMessage string) {
query := url.Values{}
if name := c.PostForm("name"); name != "" {
query.Set("name", name)
}
if page := c.PostForm("page"); page != "" {
query.Set("page", page)
}
if message != "" {
query.Set("msg", message)
}
@@ -785,5 +801,5 @@ func (h *Handler) ClientDelete(c *gin.Context) {
return
}
log.Printf("clients_deleted count=%d", n)
c.Redirect(http.StatusSeeOther, "/clients")
redirectClients(c, fmt.Sprintf("已删除 %d 台客户端", n), "")
}
+77
View File
@@ -0,0 +1,77 @@
package web
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/gin-gonic/gin"
)
func listPostContext(t *testing.T, path string, values url.Values) (*gin.Context, *httptest.ResponseRecorder) {
t.Helper()
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
context, _ := gin.CreateTestContext(recorder)
request := httptest.NewRequest(http.MethodPost, path, strings.NewReader(values.Encode()))
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
context.Request = request
return context, recorder
}
func assertRedirectQuery(t *testing.T, recorder *httptest.ResponseRecorder, wantPath string, want map[string]string) {
t.Helper()
location, err := url.Parse(recorder.Header().Get("Location"))
if err != nil {
t.Fatal(err)
}
if location.Path != wantPath {
t.Fatalf("跳转路径=%s,期望 %s", location.Path, wantPath)
}
for key, value := range want {
if got := location.Query().Get(key); got != value {
t.Fatalf("%s=%q,期望 %q;Location=%s", key, got, value, location.String())
}
}
}
func Test列表写操作跳转保留筛选和页码(t *testing.T) {
t.Run("PDD", func(t *testing.T) {
context, recorder := listPostContext(t, "/pdd/delete", url.Values{
"q": {"737"}, "status": {"pending"}, "page": {"3"},
})
(&Handler{}).pddRedirect(context, "完成")
assertRedirectQuery(t, recorder, "/pdd", map[string]string{
"q": "737", "status": "pending", "page": "3", "msg": "完成",
})
})
t.Run("任务", func(t *testing.T) {
context, recorder := listPostContext(t, "/tasks/delete", url.Values{
"q": {"TASK"}, "type": {"collect"}, "status": {"pending"}, "page": {"2"},
})
(&Handler{}).taskRedirect(context, "完成")
assertRedirectQuery(t, recorder, "/tasks", map[string]string{
"q": "TASK", "type": "collect", "status": "pending", "page": "2", "msg": "完成",
})
})
t.Run("客户端", func(t *testing.T) {
context, recorder := listPostContext(t, "/clients/assign", url.Values{
"name": {"仓库"}, "page": {"4"},
})
redirectClients(context, "完成", "")
assertRedirectQuery(t, recorder, "/clients", map[string]string{
"name": "仓库", "page": "4", "msg": "完成",
})
})
t.Run("用户", func(t *testing.T) {
context, recorder := listPostContext(t, "/users/status", url.Values{
"q": {"buyer"}, "list_status": {"active"}, "page": {"5"},
})
redirectUsers(context, "完成", "")
assertRedirectQuery(t, recorder, "/users", map[string]string{
"q": "buyer", "status": "active", "page": "5", "msg": "完成",
})
})
}
+22 -1
View File
@@ -20,8 +20,9 @@ import (
func (h *Handler) PddList(c *gin.Context) {
keyword := c.Query("q")
status := service.ParseCollectStatus(c.Query("status"))
pageNum := service.ParsePage(c.Query("page"))
result, err := service.ListPddProducts(h.db, keyword, status)
result, err := service.ListPddProducts(h.db, keyword, status, pageNum)
if err != nil {
fail(c, http.StatusInternalServerError,
"读取 PDD 商品列表失败,数据没有被改动。刷新页面重试;一直失败请把这句话报给维护者。")
@@ -35,6 +36,18 @@ func (h *Handler) PddList(c *gin.Context) {
statusLine = msg + " · " + statusLine
}
values := url.Values{}
if keyword != "" {
values.Set("q", keyword)
}
if status != "" {
values.Set("status", string(status))
}
detailValues := url.Values{}
for key, entries := range values {
detailValues[key] = append([]string(nil), entries...)
}
detailValues.Set("page", strconv.Itoa(result.Page))
c.HTML(http.StatusOK, "pdd/list", page(c, "pdd", "PDD 商品", gin.H{
"Rows": result.Rows,
"Keyword": keyword,
@@ -43,6 +56,9 @@ func (h *Handler) PddList(c *gin.Context) {
"StatusOptions": service.CollectStatusOptions(),
"HasAnyProducts": result.Total > 0,
"IsFiltered": result.IsFiltered,
"CurrentPage": result.Page,
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
"DetailURL": "/pdd/detail?" + detailValues.Encode(),
}))
}
@@ -71,6 +87,8 @@ func (h *Handler) PddDetail(c *gin.Context) {
c.HTML(http.StatusOK, "pdd/edit_modal", gin.H{
"D": detail,
"CSRFToken": csrfToken(c),
"Keyword": c.Query("q"), "StatusFilter": c.Query("status"),
"CurrentPage": service.ParsePage(c.Query("page")),
})
}
@@ -156,6 +174,9 @@ func (h *Handler) pddRedirect(c *gin.Context, msg string) {
if q := c.PostForm("q"); q != "" {
params.Set("q", q)
}
if p := c.PostForm("page"); p != "" {
params.Set("page", p)
}
if msg != "" {
params.Set("msg", msg)
}
+23 -2
View File
@@ -34,9 +34,13 @@ func (h *Handler) UserList(c *gin.Context) {
}
func (h *Handler) renderUserList(c *gin.Context, httpStatus int, message, errorMessage string, needCreate bool, createUsername string) {
keyword := strings.TrimSpace(c.Query("q"))
keyword := strings.TrimSpace(userListValue(c, "q"))
listStatus := userListValue(c, "status")
if c.Request.Method == http.MethodPost {
listStatus = c.PostForm("list_status")
}
result, err := service.ListUsers(h.db, currentUser(c), keyword,
model.UserStatus(c.Query("status")), service.ParsePage(c.Query("page")))
model.UserStatus(listStatus), service.ParsePage(userListValue(c, "page")))
if err != nil {
fail(c, http.StatusInternalServerError, "读取用户列表失败,账号数据没有被改动。刷新后重试。")
return
@@ -54,9 +58,17 @@ func (h *Handler) renderUserList(c *gin.Context, httpStatus int, message, errorM
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
"StatusFilter": result.Status, "Message": message, "Error": errorMessage,
"NeedCreate": needCreate, "CreateUsername": createUsername,
"CurrentPage": result.Page,
}))
}
func userListValue(c *gin.Context, key string) string {
if c.Request.Method == http.MethodPost {
return c.PostForm(key)
}
return c.Query(key)
}
// UserCreate 只创建固定 purchaser 角色,密码不写入 URL、不回显。
func (h *Handler) UserCreate(c *gin.Context) {
err := service.CreatePurchaser(h.db, currentUser(c), c.PostForm("username"),
@@ -114,6 +126,15 @@ func isExpectedUserError(err error) bool {
func redirectUsers(c *gin.Context, message, errorMessage string) {
query := url.Values{}
if keyword := c.PostForm("q"); keyword != "" {
query.Set("q", keyword)
}
if status := c.PostForm("list_status"); status != "" {
query.Set("status", status)
}
if page := c.PostForm("page"); page != "" {
query.Set("page", page)
}
if message != "" {
query.Set("msg", message)
}