补全页加分页,每页 30 行
218 行在手机上堆成一屏一行有五万多像素长,分页后第 1 页只有八千多。 页码会夹紧:?page=999 停在最后一页而不是给空白页,解析不出来回第一页。 提交后留在同一页——处理完的行会从列表里消失,停在这儿才看得见剩下的。 翻页前若这一页有改过还没提交的下拉,先确认一次(预选的建议不算「改过」)。
This commit is contained in:
+53
-5
@@ -3,7 +3,9 @@ package web
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -82,6 +84,21 @@ type FillupData struct {
|
||||
Categories []fillupCategory // 类别下拉选项
|
||||
Total int
|
||||
WithHint int
|
||||
|
||||
// 分页。218 行在手机上堆成一屏一行有五万多像素长,一页 30 行刚好。
|
||||
PageNo int
|
||||
PageCount int
|
||||
}
|
||||
|
||||
// fillupPerPage 是补全页每页的行数。
|
||||
const fillupPerPage = 30
|
||||
|
||||
// PageLink 生成带页码的链接,供模板里的上一页/下一页用。
|
||||
func (d FillupData) PageLink(no int) template.URL {
|
||||
if no < 1 {
|
||||
no = 1
|
||||
}
|
||||
return template.URL("/admin/fillup?page=" + strconv.Itoa(no))
|
||||
}
|
||||
|
||||
// activityOption 只是给下拉用的(活动 id + 名字)。
|
||||
@@ -189,13 +206,26 @@ func (s *Server) adminFillupPage(w http.ResponseWriter, r *http.Request) {
|
||||
s.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
d := FillupData{Page: p, Rows: rows, Activities: acts, Categories: fillupCategories(), Total: len(rows)}
|
||||
total := len(rows)
|
||||
hinted := 0
|
||||
for _, row := range rows {
|
||||
if row.ActHint != "" || row.CatHint != "" {
|
||||
d.WithHint++
|
||||
hinted++
|
||||
}
|
||||
}
|
||||
s.render(w, "admin/fillup", d)
|
||||
pageCount := (total + fillupPerPage - 1) / fillupPerPage
|
||||
pageNo := clampPage(r.URL.Query().Get("page"), pageCount)
|
||||
from := (pageNo - 1) * fillupPerPage
|
||||
to := from + fillupPerPage
|
||||
if to > total {
|
||||
to = total
|
||||
}
|
||||
rows = rows[from:to]
|
||||
|
||||
s.render(w, "admin/fillup", FillupData{
|
||||
Page: p, Rows: rows, Activities: acts, Categories: fillupCategories(),
|
||||
Total: total, WithHint: hinted, PageNo: pageNo, PageCount: pageCount,
|
||||
})
|
||||
}
|
||||
|
||||
// activityOptions 列出该数据集的活动,供下拉选择。
|
||||
@@ -451,7 +481,25 @@ func (s *Server) adminFillupAmap(w http.ResponseWriter, r *http.Request) {
|
||||
s.fillupFlash(w, r, "ok", msg)
|
||||
}
|
||||
|
||||
// fillupFlash 把提示带回补全页本身(adminFlash 是跳后台首页的)。
|
||||
// fillupFlash 把提示带回补全页本身(adminFlash 是跳后台首页的),并保留当前页码——
|
||||
// 处理完的那几行会从列表里消失,停在同一页才看得见剩下的。
|
||||
func (s *Server) fillupFlash(w http.ResponseWriter, r *http.Request, kind, msg string) {
|
||||
http.Redirect(w, r, "/admin/fillup?"+kind+"="+urlQueryEscape(msg), http.StatusSeeOther)
|
||||
q := url.Values{}
|
||||
q.Set(kind, msg)
|
||||
if pg := strings.TrimSpace(r.PostFormValue("page")); pg != "" {
|
||||
q.Set("page", pg)
|
||||
}
|
||||
http.Redirect(w, r, "/admin/fillup?"+q.Encode(), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// clampPage 解析页码并夹到有效范围;解析不出来就回第一页。
|
||||
func clampPage(raw string, pageCount int) int {
|
||||
no, err := strconv.Atoi(strings.TrimSpace(raw))
|
||||
if err != nil || no < 1 {
|
||||
no = 1
|
||||
}
|
||||
if pageCount > 0 && no > pageCount {
|
||||
no = pageCount
|
||||
}
|
||||
return no
|
||||
}
|
||||
|
||||
@@ -168,3 +168,68 @@ func intToString(v int64) string {
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestClampPage(t *testing.T) {
|
||||
cases := []struct {
|
||||
raw string
|
||||
count int
|
||||
want int
|
||||
}{
|
||||
{"", 8, 1},
|
||||
{"1", 8, 1},
|
||||
{"8", 8, 8},
|
||||
{"999", 8, 8}, // 超了就停在最后一页,别给空白页
|
||||
{"0", 8, 1},
|
||||
{"-3", 8, 1},
|
||||
{"abc", 8, 1},
|
||||
{"3", 0, 3}, // 没有数据时不夹(总会在最后回到 1)
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := clampPage(c.raw, c.count); got != c.want {
|
||||
t.Errorf("clampPage(%q, %d) = %d,期望 %d", c.raw, c.count, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 每页只渲染 30 行;页码越界时夹回最后一页。
|
||||
func TestFillupPagination(t *testing.T) {
|
||||
f := newMovementFixture(t)
|
||||
// 造 35 个「缺类别」的地点,凑出两页
|
||||
for i := 0; i < 35; i++ {
|
||||
pk := 680000 + i
|
||||
if _, err := f.pg.ExecContext(f.ctx, `INSERT INTO places (dataset_id, src_pk, name, lat, lon)
|
||||
VALUES ($1,$2,$3,$4,$5)`, f.dsID, pk, "分页测试点", 30.0+float64(i)*0.001, 120.0); err != nil {
|
||||
t.Fatalf("造地点失败: %v", err)
|
||||
}
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
f.pg.ExecContext(f.ctx, `DELETE FROM places WHERE dataset_id=$1 AND src_pk BETWEEN 680000 AND 680099`, f.dsID)
|
||||
})
|
||||
|
||||
countRows := func(url string) (rows, pageCount int) {
|
||||
t.Helper()
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", url, nil).WithContext(f.ctx)
|
||||
f.srv.adminFillupPage(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("%s 渲染失败: %d", url, rec.Code)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
return strings.Count(body, `name="cat_`), strings.Count(body, `class="pager"`)
|
||||
}
|
||||
|
||||
rows, pagers := countRows("/admin/fillup")
|
||||
if rows != fillupPerPage {
|
||||
t.Fatalf("第 1 页应有 %d 行,实际 %d", fillupPerPage, rows)
|
||||
}
|
||||
if pagers == 0 {
|
||||
t.Error("多于 1 页时应出现分页控件")
|
||||
}
|
||||
if rows, _ := countRows("/admin/fillup?page=2"); rows == 0 || rows > fillupPerPage {
|
||||
t.Fatalf("第 2 页行数异常: %d", rows)
|
||||
}
|
||||
// 越界页码要夹回最后一页,而不是给一页空白
|
||||
if rows, _ := countRows("/admin/fillup?page=9999"); rows == 0 {
|
||||
t.Error("越界页码应夹到最后一页,不该是空白页")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
{{if .Rows}}
|
||||
<div class="card">
|
||||
<h2>待补全({{num .Total}})</h2>
|
||||
<h2>待补全({{num .Total}}){{if gt .PageCount 1}}<span class="muted" style="font-size:13px;font-weight:400"> 第 {{.PageNo}} / {{.PageCount}} 页</span>{{end}}</h2>
|
||||
<div class="card-sub">
|
||||
建议已经在下拉里<strong>选好</strong>了,不满意的改成别的、或选「(不动)」。
|
||||
活动按<strong>地点</strong>补:一次把该地点名下所有没设活动的到访一起填上,不会只改一条。
|
||||
@@ -25,7 +25,16 @@
|
||||
右侧「高德参考」写的是高德在这个坐标上找到的 POI 与类型,猜不出活动时拿它作判断。<br>
|
||||
「用高德分析剩下的」每次处理 40 个、约 4 秒,结果会填进上面还没建议的行里,点多几次即可。
|
||||
</div>
|
||||
{{if gt .PageCount 1}}
|
||||
<div class="pager">
|
||||
{{if gt .PageNo 1}}<a href="{{.PageLink (sub .PageNo 1)}}">上一页</a>{{end}}
|
||||
<span class="cur">第 {{.PageNo}} / {{.PageCount}} 页</span>
|
||||
{{if lt .PageNo .PageCount}}<a href="{{.PageLink (add .PageNo 1)}}">下一页</a>{{end}}
|
||||
</div>
|
||||
<div class="card-sub" style="margin-bottom:10px">只提交<strong>本页</strong>的选择,翻页前先把这页提交掉。</div>
|
||||
{{end}}
|
||||
<form method="post" action="/admin/fillup/apply">
|
||||
<input type="hidden" name="page" value="{{.PageNo}}">
|
||||
<div class="tw">
|
||||
<table class="tb tb-stack">
|
||||
<thead>
|
||||
@@ -80,6 +89,13 @@
|
||||
title="用高德按坐标搜一遍,给还没建议的地点补上参考">用高德分析剩下的</button>
|
||||
</div>
|
||||
</form>
|
||||
{{if gt .PageCount 1}}
|
||||
<div class="pager" style="margin-top:12px">
|
||||
{{if gt .PageNo 1}}<a href="{{.PageLink (sub .PageNo 1)}}">上一页</a>{{end}}
|
||||
<span class="cur">第 {{.PageNo}} / {{.PageCount}} 页</span>
|
||||
{{if lt .PageNo .PageCount}}<a href="{{.PageLink (add .PageNo 1)}}">下一页</a>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="card">
|
||||
@@ -87,4 +103,21 @@
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// 选了东西还没提交就翻页,等于白填——拦一下(预选的那些建议不算「改过」)
|
||||
var initial = {};
|
||||
document.querySelectorAll('select.fill-sel').forEach(function (s) { initial[s.name] = s.value; });
|
||||
document.querySelectorAll('.pager a').forEach(function (a) {
|
||||
a.addEventListener('click', function (e) {
|
||||
var dirty = false;
|
||||
document.querySelectorAll('select.fill-sel').forEach(function (s) {
|
||||
if (s.value !== initial[s.name]) dirty = true;
|
||||
});
|
||||
if (dirty && !confirm('这一页还有没提交的选择,翻页会丢掉。继续翻页?')) e.preventDefault();
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user