审计修复:前端(模板 / 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
+135
View File
@@ -0,0 +1,135 @@
package web
import (
"io/fs"
"path/filepath"
"regexp"
"strings"
"testing"
)
// tbStackExempt 是明确不参与窄屏堆叠的表格类名,必须写清理由。
var tbStackExempt = map[string]string{
"photo-table": "照片表由脚本逐格生成、列数固定,移动端走「隐藏第 3 列」那条路",
}
// TestAllTBTablesAreStackable 静态扫全部模板:只要用了 `tb` 这个宽表基类,就必须带 `tb-stack`。
//
// 这是对 wide_table_layout_test 的补充——那条是**渲染后**按 DOM 栈判的,两种表能躲过去:
// ① 运行期的数据为空(比如库里没有遮罩区),那张表压根没渲染;
// ② 模板忘了写 .tw 包裹,判据里的「在 .tw 里」就不成立。
// /admin/fog/mask 的遮罩表两条都占了,2026-09-21 审计才发现。
func TestAllTBTablesAreStackable(t *testing.T) {
tableRe := regexp.MustCompile(`<table\b[^>]*class="([^"]*)"`)
scanned, tables := 0, 0
err := fs.WalkDir(tmplFS, "templates", func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || filepath.Ext(path) != ".html" {
return err
}
raw, err := fs.ReadFile(tmplFS, path)
if err != nil {
return err
}
scanned++
for _, m := range tableRe.FindAllStringSubmatch(string(raw), -1) {
classes := strings.Fields(m[1])
if !containsStr(classes, "tb") {
continue
}
tables++
if containsStr(classes, "tb-stack") {
continue
}
exempt := false
for _, c := range classes {
if _, ok := tbStackExempt[c]; ok {
exempt = true
}
}
if exempt {
continue
}
t.Errorf("%s:用了 tb 却没带 tb-stack(class=%q)——窄屏没有任何堆叠规则可命中,只能横滑",
path, m[1])
}
return nil
})
if err != nil {
t.Fatalf("遍历模板失败: %v", err)
}
if scanned == 0 || tables == 0 {
t.Fatalf("没扫到模板/表格(模板目录或类名写法变了?):模板 %d 个、tb 表 %d 张", scanned, tables)
}
t.Logf("扫了 %d 个模板里的 %d 张 tb 表", scanned, tables)
}
// TestTemplatesHaveBalancedRows 静态钉住模板里 `<tr>` 与 `</tr>` 数量相等。
//
// 浏览器会自己把漏掉的 `</tr>` 补上,页面看着完全正常——但 tb_stack_test 的行列校验
// 是按 `<tr>…</tr>` 配对的,一个没闭合的 `<tr>` 会让**那一行整行不被检查**
// (2026-09-21 审计在 report.html 的「最南」行上发现,之前一直静默跳过)。
//
// 先剥掉 <script> 块:照片表的行是脚本拼字符串生成的,那边的 `<tr>` 不算数。
func TestTemplatesHaveBalancedRows(t *testing.T) {
checked := 0
err := fs.WalkDir(tmplFS, "templates", func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || filepath.Ext(path) != ".html" {
return err
}
raw, err := fs.ReadFile(tmplFS, path)
if err != nil {
return err
}
body := stripScriptBlocks(string(raw))
open := len(regexp.MustCompile(`<tr\b`).FindAllString(body, -1))
close := strings.Count(body, "</tr>")
if open == 0 {
return nil
}
checked++
if open != close {
t.Errorf("%s:<tr> 有 %d 个、</tr> 只有 %d 个 —— 缺口那一行会被 tb_stack_test 静默跳过",
path, open, close)
}
return nil
})
if err != nil {
t.Fatalf("遍历模板失败: %v", err)
}
if checked == 0 {
t.Fatal("一个含表格行的模板都没扫到:模板目录变了?")
}
t.Logf("检查了 %d 个含表格行的模板", checked)
}
// stripScriptBlocks 去掉所有 <script>…</script> 的内容(含标签本身)。
func stripScriptBlocks(s string) string {
var b strings.Builder
for {
i := strings.Index(s, "<script")
if i < 0 {
b.WriteString(s)
return b.String()
}
b.WriteString(s[:i])
j := strings.Index(s[i:], ">")
if j < 0 {
return b.String()
}
end := strings.Index(s[i+j:], "</script>")
if end < 0 {
return b.String()
}
s = s[i+j+end+len("</script>"):]
}
}
func containsStr(list []string, want string) bool {
for _, v := range list {
if v == want {
return true
}
}
return false
}