审计修复:前端(模板 / CSS / JS 与无障碍)

- 地图脚本:localStorage 读写走 lsGet/lsSet 兜底(隐私模式下裸访问会打断整段脚本);
  preview() 在 /api/map-config 还没回来时不丢点(previewQueue 在 if (live) 之前排队);
  异步回包一律发序号作废,取消后回来的包不再画
- photo-exif.js:EXIF 时间全 0 不再变成 1899-11-30 写进到访时间;读 count 前先按版本卡宽度
- 数据编辑页:三个筛选表单都把其余条件用隐藏域原样带上,提交不再把别的列表的筛选清掉
- 补全页:sessionStorage 的读写一律包 try(脏数据/隐私模式都会抛)
- 窄屏:删掉被后面同特异性规则盖掉的死内边距;宽表堆叠类名成对、判据扩到所有 tb 表
- 无障碍:图片删除按钮补 aria-label,访客画廊图片补 alt
- report.html 补上漏掉的 </tr>,fog-mask 的宽表带 tb-stack

本批不碰数据库与导出路径:往返包 823/323/8/30/791/557/2246 全等、
坐标偏差 0.000000000、时间偏差 0s、迷雾 8701 块一致。
This commit is contained in:
tamakyi
2026-09-24 20:37:57 +08:00
parent b4c2a8452f
commit d5f6dfa381
18 changed files with 675 additions and 77 deletions
+18
View File
@@ -205,6 +205,13 @@
<form method="get" action="/admin/edit" class="filter-row" style="margin-top:6px">
{{/* back 要跟着走:不然一筛选,「返回地点库」就没了 */}}
{{if .Back}}<input type="hidden" name="back" value="{{.BackRaw}}">{{end}}
{{/* 三个表单各自只带自己那几个条件,其余条件必须原样带上:
否则搜一下就把别的列表的筛选悄悄清掉了。 */}}
{{if .VisitQuery}}<input type="hidden" name="vq" value="{{.VisitQuery}}">{{end}}
{{if .MovementFilter.Transport}}<input type="hidden" name="mt" value="{{.MovementFilter.Transport}}">{{end}}
{{if .MovementFilter.PlaceID}}<input type="hidden" name="mp" value="{{.MovementFilter.PlaceID}}">{{end}}
{{if .MovementFilter.From}}<input type="hidden" name="mfrom" value="{{.MovementFilter.From}}">{{end}}
{{if .MovementFilter.To}}<input type="hidden" name="mto" value="{{.MovementFilter.To}}">{{end}}
<label class="f" style="flex:1 1 200px"><span>搜索</span><input type="text" name="q" value="{{.Query}}" placeholder="名称 / 城市 / 区县"></label>
<label class="f" style="flex:0 1 170px"><span>坐标来源</span>
<select name="src">
@@ -343,6 +350,15 @@
<h2>到访记录</h2>
<form method="get" action="/admin/edit" class="search-row" style="margin-top:6px">
{{if .Back}}<input type="hidden" name="back" value="{{.BackRaw}}">{{end}}
{{/* 只搜到访不能把别的列表的筛选清掉(原来只带 vq+back,一搜 q/src/sus
和行程的 mt/mp/mfrom/mto 全丢了)。 */}}
{{if .Query}}<input type="hidden" name="q" value="{{.Query}}">{{end}}
{{if .SourceFilter}}<input type="hidden" name="src" value="{{.SourceFilter}}">{{end}}
{{if .SuspectOnly}}<input type="hidden" name="sus" value="1">{{end}}
{{if .MovementFilter.Transport}}<input type="hidden" name="mt" value="{{.MovementFilter.Transport}}">{{end}}
{{if .MovementFilter.PlaceID}}<input type="hidden" name="mp" value="{{.MovementFilter.PlaceID}}">{{end}}
{{if .MovementFilter.From}}<input type="hidden" name="mfrom" value="{{.MovementFilter.From}}">{{end}}
{{if .MovementFilter.To}}<input type="hidden" name="mto" value="{{.MovementFilter.To}}">{{end}}
<label class="f"><span>搜索</span><input type="text" name="vq" value="{{.VisitQuery}}" placeholder="地点 / 城市 / 备注 / 活动 / 日期 / #编号"></label>
<div class="act"><button class="btn" type="submit">搜索</button><a class="btn" href="{{.EditLink "vq"}}">重置</a></div>
</form>
@@ -424,6 +440,8 @@
{{if .Back}}<input type="hidden" name="back" value="{{.BackRaw}}">{{end}}
{{if .Query}}<input type="hidden" name="q" value="{{.Query}}">{{end}}
{{if .VisitQuery}}<input type="hidden" name="vq" value="{{.VisitQuery}}">{{end}}
{{if .SourceFilter}}<input type="hidden" name="src" value="{{.SourceFilter}}">{{end}}
{{if .SuspectOnly}}<input type="hidden" name="sus" value="1">{{end}}
<label class="f" style="flex:0 1 150px"><span>交通方式</span>
<select name="mt">
<option value="">全部</option>
+5 -3
View File
@@ -40,14 +40,15 @@
<div class="card">
<h2>已有遮罩区({{len .Masks}})</h2>
{{if .Masks}}
<table class="tb">
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>名称</th><th>圆心(纬度, 经度)</th><th>半径</th><th></th></tr></thead>
<tbody>
{{range .Masks}}
<tr>
<td>{{if .Name}}{{.Name}}{{else}}<span class="muted">(未命名)</span>{{end}}</td>
<td class="mono">{{printf "%.5f" .Lat}}, {{printf "%.5f" .Lon}}</td>
<td>{{printf "%.0f" .RadiusM}} 米</td>
<td class="mono" data-label="圆心">{{printf "%.5f" .Lat}}, {{printf "%.5f" .Lon}}</td>
<td data-label="半径">{{printf "%.0f" .RadiusM}} 米</td>
<td>
<button class="btn sm" type="button" data-edit
data-id="{{.ID}}" data-name="{{.Name}}"
@@ -62,6 +63,7 @@
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="muted">还没有遮罩区。在地图上点选圆心、设置半径后保存即可。</div>
{{end}}
+15 -16
View File
@@ -520,7 +520,11 @@
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ coord: coordSel(), points: coords })
}).then(function (r) { return r.json(); }).then(function (d) {
}).then(function (r) {
// 500 时回的是 HTML,直接 r.json() 会抛 SyntaxError(英文),先判状态码
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
}).then(function (d) {
if (d.error) throw new Error(d.error);
return d.items || [];
});
@@ -671,29 +675,24 @@
// ---- 单个点位的地图弹窗:只建一个 Leaflet 实例,之后靠 setPoints/setView 复用 ----
var modal = document.getElementById('point-modal');
var modalInited = false;
function openPointMap(lat, lon, name, addr) {
document.getElementById('point-modal-title').textContent = name || '点位';
document.getElementById('point-modal-meta').textContent =
(addr && addr !== '—' ? addr + ' · ' : '') + lat.toFixed(6) + ', ' + lon.toFixed(6);
modal.style.display = 'flex';
var pts = [{ lat: lat, lon: lon, n: name || '', v: 1 }];
if (!modalInited) {
if (!window.rondMap) { document.getElementById('point-modal-meta').textContent += '(地图脚本未就绪)'; return; }
window.rondMap.preview({ id: 'point-modal-map', points: pts });
modalInited = true;
// 单个点 fitBounds 会顶到最大缩放,等底图就绪后固定到街区级
var pin = function () {
var st = window.rondMap._state['point-modal-map'];
if (st && st.map) { st.map.invalidateSize(); st.map.setView([lat, lon], 16); }
};
setTimeout(pin, 400);
setTimeout(pin, 1200);
} else {
if (!window.rondMap) { document.getElementById('point-modal-meta').textContent += '(地图脚本未就绪)'; return; }
// 统一交给 preview:它自己区分「首次建图」与「复用换点」,配置还没回来时也会把点排队补画。
// 原来这里同步置 modalInited=true,第二次打开时地图实例其实还没建好(_state 里没有条目),
// 于是走 else 分支拿不到 st,该画的点被静默丢掉。
window.rondMap.preview({ id: 'point-modal-map', points: pts });
// 单个点 fitBounds 会顶到最大缩放,等底图就绪后固定到街区级
var pin = function () {
var st = window.rondMap._state['point-modal-map'];
if (st && st.setPoints) st.setPoints(pts);
if (st && st.map) { st.map.invalidateSize(); st.map.setView([lat, lon], 16); }
}
};
setTimeout(pin, 400);
setTimeout(pin, 1200);
}
function closePointMap() { modal.style.display = 'none'; }
document.getElementById('point-modal-close').addEventListener('click', closePointMap);