补全页恢复手工填写,把「不动」变成真正的默认

上一版改成 select 之后有两个问题:活动和类别都只能从列表里挑,填不进去的写不了;
而且建议被强制预选,用户选「不动」再点「用高德分析」(会整页重载)就白选了,
看起来像被锁死。

现在:活动是可输入的输入框,带已有类型提示,填新名字会新建类型;
类别下拉末尾加「其它(自己写)…」,选中后露出文本框,写什么存什么——
真机自己也存中文类别,写中文一样认得,不必强记 MKPOICategory 那串枚举名。

「不动」改成真正的默认:什么都不预选,想批量采纳就点「采纳本页建议」。
点「用高德分析」前会把本页填过的存进 sessionStorage,回来接着填,不会白改。
This commit is contained in:
tamakyi
2026-09-19 20:13:06 +08:00
parent 6b023fb9fd
commit 5aeded2d51
4 changed files with 126 additions and 33 deletions
+7 -1
View File
@@ -344,7 +344,13 @@ func (s *Server) adminFillupApply(w http.ResponseWriter, r *http.Request) {
acts += int(n)
}
}
if cat := strings.TrimSpace(r.PostFormValue("cat_" + strconv.FormatInt(pid, 10))); cat != "" {
key := "cat_" + strconv.FormatInt(pid, 10)
cat := strings.TrimSpace(r.PostFormValue(key))
if cat == "__custom__" {
// 下拉里选了「其它(自己写)」,真正要写的是旁边那个文本框
cat = strings.TrimSpace(r.PostFormValue(key + "__custom"))
}
if cat != "" {
res, err := ex.ExecContext(ctx, `UPDATE places SET poi_category=$1
WHERE dataset_id=$2 AND id=$3 AND COALESCE(poi_category,'')=''`, cat, ds.ID, pid)
if err != nil {
+34 -1
View File
@@ -215,7 +215,8 @@ func TestFillupPagination(t *testing.T) {
t.Fatalf("%s 渲染失败: %d", url, rec.Code)
}
body := rec.Body.String()
return strings.Count(body, `name="cat_`), strings.Count(body, `class="pager"`)
// 数类别下拉本身;旁边那个「其它(自己写)」文本框的 name 也以 cat_ 开头,得排掉
return strings.Count(body, `<select name="cat_`), strings.Count(body, `class="pager"`)
}
rows, pagers := countRows("/admin/fillup")
@@ -233,3 +234,35 @@ func TestFillupPagination(t *testing.T) {
t.Error("越界页码应夹到最后一页,不该是空白页")
}
}
// 类别选「其它(自己写)」时,写进去的是旁边文本框里的内容。
func TestFillupCustomCategory(t *testing.T) {
f := newMovementFixture(t)
placeID, _ := fillupPlace(t, f)
key := "cat_" + strconv.FormatInt(placeID, 10)
applyFillup(t, f, url.Values{key: {"__custom__"}, key + "__custom": {"美食街"}})
var got string
if err := f.pg.QueryRowContext(f.ctx, `SELECT COALESCE(poi_category,'') FROM places WHERE id=$1`, placeID).Scan(&got); err != nil {
t.Fatalf("读类别失败: %v", err)
}
if got != "美食街" {
t.Fatalf("应写入手工填的类别,实际 %q", got)
}
}
// 选了「其它」却什么也没写:不写库,别把空串当成类别。
func TestFillupCustomCategoryEmptyIsSkipped(t *testing.T) {
f := newMovementFixture(t)
placeID, _ := fillupPlace(t, f)
key := "cat_" + strconv.FormatInt(placeID, 10)
applyFillup(t, f, url.Values{key: {"__custom__"}, key + "__custom": {" "}})
var got string
if err := f.pg.QueryRowContext(f.ctx, `SELECT COALESCE(poi_category,'') FROM places WHERE id=$1`, placeID).Scan(&got); err != nil {
t.Fatalf("读类别失败: %v", err)
}
if got != "" {
t.Fatalf("没填内容时不该写库,实际 %q", got)
}
}
+83 -31
View File
@@ -18,22 +18,22 @@
<div class="card">
<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>补:一次把该地点名下所有没设活动的到访一起填上,不会只改一条。
填了现有列表之外的名字,会自动建成新的活动类型。
类别下拉里标着「真机用过」的是导出回 rond 时一定能认的;<br>
右侧「高德参考」写的是高德在这个坐标上找到的 POI 与类型,猜不出活动时拿它作判断。<br>
「用高德分析剩下的」每次处理 40 个、约 4 秒,结果会填进上面还没建议的行里,点多几次即可。
<strong>默认都是「不动」</strong>,你改过的才会写进去。
活动可以直接打字(会提示已有的类型),也可以点「采纳本页建议」一键填上;
类别从下拉里挑,都不合适就选「其它」自己写一个(真机也存中文类别,写中文同样认得)。<br>
活动按<strong>地点</strong>补:一次把它名下所有没设活动的到访一起填上,不会只改一条。
右侧「高德参考」是高德在这个坐标上找到的 POI 与类型,拿不准时作参考。
</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">
<form method="post" action="/admin/fillup/apply" id="fillup-form">
<input type="hidden" name="page" value="{{.PageNo}}">
<div class="tw">
<table class="tb tb-stack">
@@ -52,24 +52,21 @@
<td data-label="缺什么">{{$row.Missing}}</td>
<td data-label="活动">
{{if $row.NoAct}}
<select name="act_{{$row.PlaceID}}" class="fill-sel">
<option value=""{{if not $row.ActHint}} selected{{end}}>(不动)</option>
{{if $row.ActHint}}
<option value="{{$row.ActHint}}" selected>建议:{{$row.ActHint}}{{if not $row.ActID}}(新建){{end}}</option>
{{end}}
{{range $.Activities}}<option value="{{.Name}}">{{.Name}}</option>{{end}}
</select>
<input list="fill-act" name="act_{{$row.PlaceID}}" value=""
placeholder="(不动)" data-hint="{{$row.ActHint}}" style="min-width:8em">
{{else}}<span class="muted">—</span>{{end}}
</td>
<td data-label="类别">
{{if $row.NoCat}}
{{$hint := $row.CatHint}}
<select name="cat_{{$row.PlaceID}}" class="fill-sel">
<option value=""{{if not $hint}} selected{{end}}>(不动)</option>
{{range $.Categories}}
<option value="{{.Value}}"{{if eq .Value $hint}} selected{{end}}>{{.Label}}</option>
{{end}}
<select name="cat_{{$row.PlaceID}}" class="cat-sel" data-hint="{{$hint}}">
<option value="">(不动)</option>
{{if $hint}}<option value="{{$hint}}">建议:{{catName $hint}}</option>{{end}}
{{range $.Categories}}{{if ne .Value $hint}}<option value="{{.Value}}">{{.Label}}</option>{{end}}{{end}}
<option value="__custom__">其它(自己写)…</option>
</select>
<input type="text" name="cat_{{$row.PlaceID}}__custom" placeholder="例如 美食街"
style="display:none;margin-top:4px;min-width:10em">
{{else}}<span class="muted">—</span>{{end}}
</td>
<td data-label="建议来源">{{if $row.Source}}{{$row.Source}}{{else}}<span class="muted">—</span>{{end}}</td>
@@ -85,10 +82,12 @@
</div>
<div class="row" style="margin-top:12px">
<button class="btn primary" type="submit">执行补全</button>
<button class="btn" type="submit" formaction="/admin/fillup/amap"
title="用高德按坐标搜一遍,给还没建议的地点补上参考">用高德分析剩下的</button>
<button class="btn" type="button" id="pick-hints">采纳本页建议</button>
<button class="btn" type="submit" formaction="/admin/fillup/amap" id="amap-btn"
title="用高德按坐标搜一遍,给还没建议的地点补上参考(每批 40 个)">用高德分析剩下的</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}}
@@ -104,19 +103,72 @@
{{end}}
</div>
<datalist id="fill-act">
{{range .Activities}}<option value="{{.Name}}"></option>{{end}}
</datalist>
<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;
var KEY = 'rond-fillup-draft';
// 选了「其它(自己写)」就露出旁边的文本框
document.querySelectorAll('select.cat-sel').forEach(function (sel) {
var box = document.querySelector('input[name="' + sel.name + '__custom"]');
if (!box) return;
var sync = function () {
var custom = sel.value === '__custom__';
box.style.display = custom ? '' : 'none';
if (custom) box.focus();
};
sel.addEventListener('change', sync);
sync();
});
// 采纳本页建议:活动按 data-hint 填进输入框,类别把建议项选上
var pick = document.getElementById('pick-hints');
if (pick) {
pick.addEventListener('click', function () {
var n = 0;
document.querySelectorAll('input[name^="act_"]').forEach(function (inp) {
if (inp.value || !inp.dataset.hint) return;
inp.value = inp.dataset.hint;
n++;
});
if (dirty && !confirm('这一页还有没提交的选择,翻页会丢掉。继续翻页?')) e.preventDefault();
document.querySelectorAll('select.cat-sel').forEach(function (sel) {
if (sel.value || !sel.dataset.hint) return;
sel.value = sel.dataset.hint;
n++;
});
pick.textContent = n ? '已填入 ' + n + ' 条建议' : '本页没有可填的建议';
});
}
// 「用高德分析」会整页重载,先把这一页填过的存起来、回来再恢复——
// 否则挑了半天,点一下就全没了。
var save = function () {
var draft = {};
document.querySelectorAll('input[name^="act_"], input[name$="__custom"], select.cat-sel').forEach(function (el) {
if (el.value) draft[el.name] = el.value;
});
try { sessionStorage.setItem(KEY, JSON.stringify(draft)); } catch (e) { /* 隐私模式就算了 */ }
};
var amap = document.getElementById('amap-btn');
if (amap) amap.addEventListener('click', save);
try {
var draft = JSON.parse(sessionStorage.getItem(KEY) || '{}');
Object.keys(draft).forEach(function (name) {
var el = document.querySelector('[name="' + name + '"]');
if (!el) return;
el.value = draft[name];
if (el.tagName === 'SELECT') el.dispatchEvent(new Event('change'));
});
} catch (e) { /* 坏数据就当没有 */ }
// 提交成功后草稿就没用了,清掉,免得下次进来还带着旧选择
var form = document.getElementById('fillup-form');
if (form) form.addEventListener('submit', function () {
try { sessionStorage.removeItem(KEY); } catch (e) {}
});
})();
</script>
+2
View File
@@ -1327,6 +1327,8 @@ func templateFuncs() template.FuncMap {
"dt": func(t time.Time) string { return t.Local().Format("2006-01-02 15:04") },
// actKey 给活动冲突生成表单字段名,与处理函数共用同一套键
"actKey": rond.ActivityKey,
// catName 把 MKPOICategoryXxx 翻成中文,补全页的类别下拉要用
"catName": poiName,
// dtt 接受 *time.Time,nil 返回空串,便于可选时间字段直接渲染
"dtt": func(t *time.Time) string {
if t == nil {