Files
rond-with-you/internal/web/templates/admin/fillup.html
T
tamakyi 82e63412c0 类别显示名可自己维护,顺手把两套对不上的中文名合成一套
你要改的那个「海滩」原来根本不在数据里——它是 Go 里硬编码的 map,所以改不动。
更糟的是这样的 map 有**两张**,而且对不上:

  MKPOICategoryFoodMarket   菜市场(编辑页)  /  食品市场(补全页、GPX)
  MKPOICategoryCafe         咖啡馆              /  咖啡厅
  MKPOICategoryFitnessCenter 健身房             /  健身中心
  外加 ATM / Atm 大小写各写一份、一条永远匹配不上的 MKPOICategoryMarina2

现在合成一张内置表(42 条,冲突处取编辑页那套),并在它上面加了一层站点覆盖:

- 新表 category_labels(value, label),站点级、不挂 dataset——值本身才是稳定键
- 端点 POST /admin/edit/category/label;传空 label = 删掉覆盖行、恢复内置默认名
- 类别台账那行改成两个带标签的输入框:
      导出值 [MKPOICategoryBeach]   ← 会跟着导出写回手机
      显示名 [               ]      ← 只影响站内显示,placeholder 写着默认值
  是 MKPOICategory 枚举但内置表里没有的,会提示「填一个中文名」,这就是你要的补全入口

**显示名绝不写进 places.poi_category**——中文跟着写进去就把导出数据污染了,
专门加了 TestCategoryLabelNeverTouchesValue 钉这条。显示名会贯穿到所有中文名的地方:
编辑页/照片页/补全页的下拉、补全页「在用」标签、访客地点页、GPX 的 desc。

连带删掉两个绑在旧表上的模板函数 catName / poiname:模板里算不出显示名(要读站点覆盖),
改成在 Go 侧算好塞进数据(FillupRow.CatLabel、PlaceData.CategoryLabel)。
2026-09-20 10:54:51 +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}}">建议:{{$row.CatLabel}}</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}}