Files
cmautobuy/admin/service/pagination.go
T

174 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 分页的通用规则。
//
// `[必须]` 这是全项目第一个做分页的页面(蝦皮数据页,工单 #43),
// 这里定的规则和常量供后面四个页面照抄,不要各写一份——分开写的话,
// 每页条数、越界兜底这些规则五个页面迟早会不一致。
package service
import (
"net/url"
"slices"
"strconv"
"strings"
)
// DefaultPageSize 是主列表没有指定 page_size 时使用的每页条数。
//
// 采购员通常需要在同一页批量勾选较多记录,因此默认显示白名单中的最大值 100;
// 实际页面请求仍必须通过 ParsePageSize 白名单解析,不能把任意输入交给 SQL。
const DefaultPageSize = 100
var allowedPageSizes = [...]int{20, 50, 100}
// ParsePage 解析 URL 上的 page 查询参数。
//
// `[必须]` 认不出来的一律当作第 1 页,不报错——用户手改地址栏或者用旧书签
// 不该把页面搞崩(工单 #43)。真正的越界(超过总页数)由 ClampPage 兜底,
// 这里只处理"不是正整数"的情况。
func ParsePage(s string) int {
n, err := strconv.Atoi(strings.TrimSpace(s))
if err != nil || n < 1 {
return 1
}
return n
}
// ParsePageSize 解析 URL 上的 page_size,只接受界面提供的三个白名单值。
// 非法值回退为默认 100;SQL 查询仍只可能使用固定白名单值。
func ParsePageSize(s string) int {
n, err := strconv.Atoi(strings.TrimSpace(s))
if err != nil {
return DefaultPageSize
}
return NormalizePageSize(n)
}
// NormalizePageSize 校验代码内部传入的页容量,规则和 URL 参数完全一致。
func NormalizePageSize(n int) int {
for _, allowed := range allowedPageSizes {
if n == allowed {
return n
}
}
return DefaultPageSize
}
// TotalPages 按实际 pageSize 把总数换算成总页数。
//
// `[必须]` 总数为 0 时返回 1,不是 0——页面要显示"第 1/1 页",
// 不能出现"第 1/0 页"(工单 #43)。
func TotalPages(total, pageSize int) int {
if total <= 0 {
return 1
}
pageSize = NormalizePageSize(pageSize)
return (total + pageSize - 1) / pageSize
}
// ClampPage 把请求的页码限制在 [1, totalPages] 范围内。
//
// `[必须]` page 超过总页数时兜到最后一页(显示有数据的那一页),
// 不是显示空表格——操作员看到空表格会以为数据没了(工单 #43)。
func ClampPage(page, totalPages int) int {
if page < 1 {
return 1
}
if page > totalPages {
return totalPages
}
return page
}
// PaginationView 是分页控件要显示的全部内容,模板不做判断和算术。
//
// `[必须]` FirstURL/PrevURL/NextURL/LastURL 是**整段拼好的**相对链接
// (形如 "?status=no_link&page=3"),模板里必须整体作为单个 pipeline 输出
// (`<a href="{{.FirstURL}}">`),不要在模板里把筛选参数和 page 分开拼接。
// html/template 的上下文转义规则对"字面量 & 中间插一段动态内容"的情况,
// 会把动态内容当成单个参数值整体转义,问号和等号会被转成 %3F/%3D,
// 链接直接失效——这是本工单实测踩到的一个坑,见 pagination_test.go。
type PaginationView struct {
Page int
PageSize int
TotalPages int
HasPrev bool
HasNext bool
FirstURL string
PrevURL string
NextURL string
LastURL string
PageSizeOptions []PageSizeOption
FilterFields []PaginationFilterField
}
// PageSizeOption 是“每页条数”下拉框的一项。
type PageSizeOption struct {
Value int
Selected bool
}
// PaginationFilterField 是切换每页条数时需要保留的一个 GET 筛选参数。
// page 和 page_size 不放在这里:切换容量必须从第 1 页重新开始。
type PaginationFilterField struct {
Name string
Value string
}
// NewPaginationView 根据当前页、总页数和筛选查询串组装分页控件视图。
//
// baseQuery 是当前筛选查询串,由调用方(handler)负责把有效筛选编码进去;
// 本函数会覆盖 page_size 并移除 page,避免旧参数污染生成的链接。
func NewPaginationView(page, pageSize, totalPages int, baseQuery string) PaginationView {
pageSize = NormalizePageSize(pageSize)
v := PaginationView{
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
HasPrev: page > 1,
HasNext: page < totalPages,
}
query, err := url.ParseQuery(baseQuery)
if err != nil {
query = url.Values{}
}
query.Del("page")
query.Set("page_size", strconv.Itoa(pageSize))
encodedQuery := query.Encode()
v.FirstURL = PaginationURL(encodedQuery, 1)
v.LastURL = PaginationURL(encodedQuery, totalPages)
if v.HasPrev {
v.PrevURL = PaginationURL(encodedQuery, page-1)
}
if v.HasNext {
v.NextURL = PaginationURL(encodedQuery, page+1)
}
for _, size := range allowedPageSizes {
v.PageSizeOptions = append(v.PageSizeOptions, PageSizeOption{Value: size, Selected: size == pageSize})
}
query.Del("page_size")
for _, name := range sortedQueryKeys(query) {
for _, value := range query[name] {
v.FilterFields = append(v.FilterFields, PaginationFilterField{Name: name, Value: value})
}
}
return v
}
func sortedQueryKeys(values url.Values) []string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
slices.Sort(keys)
return keys
}
// PaginationURL 把筛选查询串和目标页码拼成一个完整的相对链接("?...")。
func PaginationURL(baseQuery string, page int) string {
q := "page=" + strconv.Itoa(page)
if baseQuery != "" {
q = baseQuery + "&" + q
}
return "?" + q
}