Files
rond-with-you/internal/web/templates/admin/fillup.html
T
tamakyi ffee082fad 补全页:手工填过的值自动进下拉候选,后面同类地点不用再打一遍
下拉的候选是渲染时定好的,这一页手工新填的活动名/类别进不去候选,
后面遇到同类地点只能照着再敲一遍——这正是「其它(自己写)」最费事的地方。

现在填过的值会被记住并补进同类下拉,标成「新填:X」,插在建议之后、
真正的候选之前,一眼就能挑到;本页实时同步(打字即生效),
翻页和点「用高德分析」重载也留得住,提交成功后清掉(那之后它们已经是真候选了)。

顺带两处:
- 草稿保存改成按 name 合并,翻页不再把上一页填的覆盖掉;分页链接也接了保存。
- 重画候选前先把「这一格已经选中的新填值」留住再还回去,
  否则改一下文本框就会把别的行选好的值悄悄抹成空。

文本框的监听直接挂在框上而不是委托到 document:`new Event('input')` 默认不冒泡,
探针第一次就是这么假绿的——直接挂不依赖冒泡,也少一层隐式约定。
2026-09-20 09:32:07 +08:00

252 lines
12 KiB
HTML
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.
{{define "content"}}
<div class="wrap">
<div class="page-head">
<div>
<h1>地点补全</h1>
<div class="sub">
共 <strong>{{num .Total}}</strong> 个地点缺活动或类别,其中 <strong>{{num .WithHint}}</strong> 个能给出建议。
<strong>只填空</strong>——已经有活动或类别的一律不动。
</div>
</div>
<div class="row"><a class="btn" href="/admin">← 返回后台</a></div>
</div>
{{if .Flash}}<div class="alert ok">{{.Flash}}</div>{{end}}
{{if .Err}}<div class="alert err">{{.Err}}</div>{{end}}
{{if .Rows}}
<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>,你改过的才会写进去。
活动可以直接打字(会提示已有的类型),也可以点「采纳本页建议」一键填上;
类别从下拉里挑,都不合适就选「其它」自己写一个(真机也存中文类别,写中文同样认得)。<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>
{{end}}
<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">
<thead>
<tr>
<th>地点</th><th>城市</th><th class="num">待设到访</th><th>缺什么</th>
<th>活动</th><th>类别</th><th>建议来源</th><th>高德参考</th>
</tr>
</thead>
<tbody>
{{range $row := .Rows}}
<tr>
<td>{{if $row.Name}}{{$row.Name}}{{else}}<span class="muted">(未命名)</span>{{end}}</td>
<td data-label="城市">{{if $row.City}}{{$row.City}}{{else}}<span class="muted">—</span>{{end}}</td>
<td class="num" data-label="待设到访">{{if $row.NoAct}}{{$row.NoAct}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="缺什么">{{$row.Missing}}</td>
<td data-label="活动">
{{if $row.NoAct}}
{{$ahint := $row.ActHint}}
<select name="act_{{$row.PlaceID}}" class="fill-sel" data-hint="{{$ahint}}" style="min-width:10em">
<option value="">(不动)</option>
{{if $ahint}}<option value="{{$ahint}}">建议:{{$ahint}}</option>{{end}}
{{range $.Activities}}{{if ne .Name $ahint}}<option value="{{.Name}}">{{.Name}}</option>{{end}}{{end}}
<option value="__custom__">其它(自己写)…</option>
</select>
<input type="text" name="act_{{$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.NoCat}}
{{$hint := $row.CatHint}}
<select name="cat_{{$row.PlaceID}}" class="fill-sel" data-hint="{{$hint}}" style="min-width:10em">
<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>
<td data-label="高德参考">
{{if $row.AmapType}}
<span style="font-size:12.5px">{{if $row.AmapPoi}}{{$row.AmapPoi}}<br>{{end}}<span class="muted">{{$row.AmapType}}</span></span>
{{else}}<span class="muted">—</span>{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<div class="row" style="margin-top:12px">
<button class="btn primary" type="submit">执行补全</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}}
<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">
<div class="alert ok">所有地点的活动和类别都齐了,没什么要补的。</div>
</div>
{{end}}
</div>
<script>
(function () {
var KEY = 'rond-fillup-draft';
var NEW = 'rond-fillup-newopts';
// 活动和类别是同一套下拉:选了「其它(自己写)」就露出旁边的文本框
document.querySelectorAll('select.fill-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();
});
// 下拉的候选是渲染时定好的,手工新填的名字进不了候选,后面遇到同类地点还得重打一遍。
// 所以把填过的值记下来补进同类下拉:本页实时生效,翻页/点「用高德分析」也留得住,
// 提交成功后清掉(那之后它们已经在真正的候选里了)。
var fromOther = {act: [], cat: []}; // 从别处(别的页/上次)带来的
var fromPage = {act: [], cat: []}; // 本页文本框里当前填着的
try {
var saved = JSON.parse(sessionStorage.getItem(NEW) || '{}');
fromOther.act = saved.act || [];
fromOther.cat = saved.cat || [];
} catch (e) { /* 坏数据就当没有 */ }
var paint = function (prefix) {
var vals = fromOther[prefix].concat(fromPage[prefix].filter(function (v) {
return fromOther[prefix].indexOf(v) < 0;
}));
document.querySelectorAll('select[name^="' + prefix + '_"]').forEach(function (sel) {
// 重画不能动用户已经选好的东西:这一格选的要是「新填」里的值,先留住它
var keep = sel.value;
if (keep && vals.indexOf(keep) < 0 && [].some.call(sel.options, function (o) {
return o.value === keep && o.getAttribute('data-local');
})) vals.push(keep);
[].forEach.call(sel.querySelectorAll('option[data-local]'), function (o) { o.remove(); });
// 插在「(不动)」/建议之后、真正的候选之前,免得沉到列表最底下去找
var anchor = null;
for (var i = 0; i < sel.options.length; i++) {
var o = sel.options[i];
if (o.getAttribute('data-local') || !o.value || o.value === sel.dataset.hint) continue;
anchor = o;
break;
}
vals.forEach(function (v) {
if ([].some.call(sel.options, function (o) { return o.value === v; })) return;
var o = document.createElement('option');
o.value = v;
o.textContent = '新填:' + v;
o.setAttribute('data-local', '1');
sel.insertBefore(o, anchor);
});
// 选项重插之后浏览器不会自己选回来,得手工还回去
if (keep && sel.value !== keep) sel.value = keep;
});
};
var collect = function (prefix) {
fromPage[prefix] = [];
document.querySelectorAll('input[name^="' + prefix + '_"][name$="__custom"]').forEach(function (box) {
var v = box.value.trim();
if (v && fromPage[prefix].indexOf(v) < 0) fromPage[prefix].push(v);
});
paint(prefix);
try {
sessionStorage.setItem(NEW, JSON.stringify({
act: fromOther.act.concat(fromPage.act),
cat: fromOther.cat.concat(fromPage.cat)
}));
} catch (e) { /* 隐私模式就算了 */ }
};
// 文本框里打字就同步,不用等失焦或提交。直接挂在框上,不靠事件冒泡——
// 省得以后有人在这行 DOM 上动手脚(或程序化派发)时静默失效。
document.querySelectorAll('input[name$="__custom"]').forEach(function (box) {
box.addEventListener('input', function () { collect(box.name.split('_')[0]); });
});
// 采纳本页建议:把建议项选上(活动和类别都是下拉)
var pick = document.getElementById('pick-hints');
if (pick) {
pick.addEventListener('click', function () {
var n = 0;
document.querySelectorAll('select.fill-sel').forEach(function (sel) {
if (sel.value || !sel.dataset.hint) return;
sel.value = sel.dataset.hint;
n++;
});
pick.textContent = n ? '已填入 ' + n + ' 条建议' : '本页没有可填的建议';
});
}
// 离开本页(翻页 / 「用高德分析」)之前把填过的存起来、回来接着填——
// 否则挑了半天,点一下就全没了。按 name 合并,所以翻页也留得住。
var save = function () {
var draft = {};
try { draft = JSON.parse(sessionStorage.getItem(KEY) || '{}'); } catch (e) { draft = {}; }
document.querySelectorAll('select.fill-sel, input[name$="__custom"]').forEach(function (el) {
if (el.value) draft[el.name] = el.value;
else delete draft[el.name]; // 本页明确清空的就是不要了,别让旧值顶上来
});
try { sessionStorage.setItem(KEY, JSON.stringify(draft)); } catch (e) { /* 隐私模式就算了 */ }
};
var amap = document.getElementById('amap-btn');
if (amap) amap.addEventListener('click', save);
document.querySelectorAll('.pager a').forEach(function (a) { a.addEventListener('click', save); });
// 先把「新填」的候选补上再恢复草稿:草稿里选中的可能就是新填的值,
// 选项不在跟前的话 select.value 是设不进去的。
paint('act');
paint('cat');
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) { /* 坏数据就当没有 */ }
// 恢复出来的文本框(`set value` 不触发 input)也要算进「新填」
collect('act');
collect('cat');
// 提交成功后草稿就没用了,清掉,免得下次进来还带着旧选择
var form = document.getElementById('fillup-form');
if (form) form.addEventListener('submit', function () {
try {
sessionStorage.removeItem(KEY);
sessionStorage.removeItem(NEW);
} catch (e) {}
});
})();
</script>
{{end}}