diff --git a/internal/web/static/css/app.css b/internal/web/static/css/app.css index 8210b29..a066039 100644 --- a/internal/web/static/css/app.css +++ b/internal/web/static/css/app.css @@ -1007,3 +1007,40 @@ label.f > span.span-actions { display: flex; align-items: center; justify-conten .filters-card.collapsed > .panel-fold { margin: 0; border-radius: var(--radius); } .filters-card.collapsed > :not(.panel-fold) { display: none; } } + +/* ---------- 窄屏:要横滑的宽表改成一屏一行的「字段 + 值」 ---------- + 地点库(8 列)、统计、专题、后台这几张表在手机上要横滑 300+px 才看得全,滑着还对不上表头。 + 窄屏改成每行一块、每格「字段名 + 值」竖排:不用横滑,也不用记「第几列是什么」。 + 字段名取 td 的 data-label,每行的首格留空当小标题(如地点名),操作格给「操作」。 + 表格结构与脚本都没动(只是隐藏 thead),链接 / 表单 / 按钮照旧;断点跟其它移动端适配一致 + (620px),桌面与平板仍是原来的表格,一行不差。 + 注:统计页那几张表在宽屏也被放在 g2 的窄卡片里(视口 1440 时卡片只有 494px),仍然要横滑 + 100+px —— 但那属于桌面版式,真要治得单说;这里先只管移动端(按容器宽度判断会让这些卡片 + 在宽屏也竖排,实测把 /stats 撑高 1300px,得不偿失)。 */ +@media (max-width: 620px) { + .tw > table.tb.tb-stack { min-width: 0; } + .tw > table.tb.tb-stack thead { display: none; } + .tw > table.tb.tb-stack, + .tw > table.tb.tb-stack > tbody, + .tw > table.tb.tb-stack > tbody > tr, + .tw > table.tb.tb-stack > tbody > tr > td { display: block; width: auto; } + .tw > table.tb.tb-stack > tbody > tr { + padding: 9px 11px; border-bottom: 1px solid var(--line-soft); + } + .tw > table.tb.tb-stack > tbody > tr:last-child { border-bottom: 0; } + /* 值用悬挂缩进:标签绝对定位在左边,值换行时对齐到值那一栏,而不是缩到标签下面 */ + .tw > table.tb.tb-stack > tbody > tr > td { + position: relative; padding: 2px 0 2px 5.6em; border-bottom: 0; + } + .tw > table.tb.tb-stack > tbody > tr > td::before { + content: attr(data-label); position: absolute; left: 0; top: 2px; + width: 5em; color: var(--text-3); font-size: 12px; + } + /* 首格是这一行的主角(地点名 / 天气 / 方式…):不缩进、字号抬一档当小标题 */ + .tw > table.tb.tb-stack > tbody > tr > td:not([data-label]) { + padding-left: 0; margin-bottom: 2px; font-size: 14px; + } + /* 数字列不必再右对齐(现在已经是「标签 + 值」两栏) */ + .tw > table.tb.tb-stack td.num { text-align: left; } +} + diff --git a/internal/web/tb_stack_test.go b/internal/web/tb_stack_test.go new file mode 100644 index 0000000..6d02e5b --- /dev/null +++ b/internal/web/tb_stack_test.go @@ -0,0 +1,94 @@ +package web + +import ( + "context" + "net/http" + "net/http/httptest" + "regexp" + "strconv" + "strings" + "testing" +) + +// TestStackedTablesHaveLabels 守住「窄屏宽表变一屏一行」这条改动的核心不变量: +// 每张打了 tb-stack 的表,窄屏会把表头藏起来、改成「字段名 + 值」竖排,字段名只能取自 +// td 的 data-label。少写一个标签,那一格在手机上就是没有名字的裸值——而桌面端一切正常, +// 很难发现(这轮就漏过两次:一张表的中间几行、后台两张表的「状态 / 对访客 / 操作」)。 +// 顺带钉住每行格子数 == 表头列数,否则说明模板结构歪了。 +// +// ROND_TEST_CONF=conf/app-test.ini ROND_SRC=data/xxx.rondbackup go test ./internal/web -run StackedTables -v +func TestStackedTablesHaveLabels(t *testing.T) { + f := newMovementFixture(t) + + var topicID int64 + if err := f.pg.QueryRowContext(f.ctx, `SELECT id FROM topics WHERE dataset_id=$1 ORDER BY id LIMIT 1`, + f.dsID).Scan(&topicID); err != nil { + topicID = 0 // 这个数据集没有专题,就少查一页 + } + + type target struct { + path string + h http.HandlerFunc + admin bool + } + targets := []target{ + {"/places", f.srv.placesPage, false}, + {"/stats", f.srv.statsPage, false}, + {"/", f.srv.home, false}, + {"/report", f.srv.reportPage, false}, + {"/admin", f.srv.adminHome, true}, + {"/admin/topics", f.srv.topicsAdminPage, true}, + } + if topicID > 0 { + targets = append(targets, target{"/topics/" + strconv.FormatInt(topicID, 10), f.srv.topicPage, false}) + } + + tableRe := regexp.MustCompile(`(?s)]*class="[^"]*tb-stack[^"]*"[^>]*>.*?`) + rowRe := regexp.MustCompile(`(?s)]*>(.*?)`) + tdRe := regexp.MustCompile(`(?s)]*)>`) + thRe := regexp.MustCompile(`]`) + + total := 0 + for _, tg := range targets { + ctx := context.Background() + if tg.admin { + ctx = f.ctx // 夹具的 ctx 里已经注入登录态 + } + w := httptest.NewRecorder() + tg.h(w, httptest.NewRequest("GET", tg.path, nil).WithContext(ctx)) + if w.Code != 200 { + t.Logf("%s 渲染 code=%d,跳过", tg.path, w.Code) + continue + } + for ti, tb := range tableRe.FindAllString(w.Body.String(), -1) { + total++ + head := "" + if i, j := strings.Index(tb, ""), strings.Index(tb, ""); i >= 0 && j > i { + head = tb[i:j] + } + cols := len(thRe.FindAllString(head, -1)) + for ri, row := range rowRe.FindAllString(tb, -1) { + if !strings.Contains(row, " 0 && len(cells) != cols { + t.Errorf("%s 第 %d 张表第 %d 行有 %d 格,表头 %d 列", tg.path, ti+1, ri+1, len(cells), cols) + } + for ci, c := range cells { + if ci == 0 { + continue // 首格是这一行的主角,故意不带标签 + } + if !strings.Contains(c[1], "data-label=") { + t.Errorf("%s 第 %d 张表第 %d 行第 %d 格没有 data-label(窄屏会变成没有名字的裸值)", + tg.path, ti+1, ri+1, ci+1) + } + } + } + } + } + if total == 0 { + t.Fatal("一页 tb-stack 表都没找到:模板里的类名是不是被改掉了") + } + t.Logf("检查了 %d 张 tb-stack 表", total) +} diff --git a/internal/web/templates/admin/index.html b/internal/web/templates/admin/index.html index 1fe9821..ea4dc5b 100644 --- a/internal/web/templates/admin/index.html +++ b/internal/web/templates/admin/index.html @@ -108,7 +108,7 @@
同一时刻只有一份数据处于「展示中」状态,主页与地图读取的就是它
{{if .Datasets}}
- +
@@ -116,18 +116,18 @@ {{range .Datasets}} - - - - - + + + + -
文件导入时间到访地点数据跨度状态
{{trunc .Name 34}}{{dt .UploadedAt}}{{num .Visits}}{{num .Places}}{{if .First}}{{d .First}} ~ {{d .Last}}{{else}}—{{end}} + {{dt .UploadedAt}}{{num .Visits}}{{num .Places}}{{if .First}}{{d .First}} ~ {{d .Last}}{{else}}—{{end}} {{if and $.Dataset (eq $.Dataset.ID .ID)}} 展示中 {{else}} 未启用 {{end}} + {{if not (and $.Dataset (eq $.Dataset.ID .ID))}}
@@ -152,15 +152,15 @@

数据说明

从备份中读取并转换的字段
- +
- - - - - - + + + + + +
来源表内容用途
ZVISIT到访记录(到达 / 离开时间、类型、地点、备注、标签)时间线、统计、地图
ZLOCATION地点(名称、经纬度、城市、省份、POI 分类)地图打点、地点库
ZACTIVITY自定义类型(颜色、图标、是否家 / 工作)类型分布
ZMOVEMENT位移段(交通方式、起止时间)出行方式统计
ZTAG自定义标签及分组标签筛选
ZHOURLYWEATHER逐小时天气(气温、天气现象、湿度)天气足迹
ZVISIT到访记录(到达 / 离开时间、类型、地点、备注、标签)时间线、统计、地图
ZLOCATION地点(名称、经纬度、城市、省份、POI 分类)地图打点、地点库
ZACTIVITY自定义类型(颜色、图标、是否家 / 工作)类型分布
ZMOVEMENT位移段(交通方式、起止时间)出行方式统计
ZTAG自定义标签及分组标签筛选
ZHOURLYWEATHER逐小时天气(气温、天气现象、湿度)天气足迹
diff --git a/internal/web/templates/admin/topics.html b/internal/web/templates/admin/topics.html index 7a0af67..7ab56c1 100644 --- a/internal/web/templates/admin/topics.html +++ b/internal/web/templates/admin/topics.html @@ -50,23 +50,23 @@
共 {{len .Topics}} 个;「对访客」一列是展示开关,关掉的专题只有你自己能预览
{{if .Topics}}
- +
{{range .Topics}} - - - - - + + + + -
标题时间范围地点到访区域对访客
{{.Title}}{{if .Subtitle}} · {{.Subtitle}}{{end}}{{.RangeLabel}}{{num .PlaceCount}}{{num .VisitCount}}{{if .HasArea}}{{printf "%.1f" .RadiusKm}} km 内{{else}}不限{{end}} + {{.RangeLabel}}{{num .PlaceCount}}{{num .VisitCount}}{{if .HasArea}}{{printf "%.1f" .RadiusKm}} km 内{{else}}不限{{end}} + 编辑
diff --git a/internal/web/templates/home.html b/internal/web/templates/home.html index 776cced..fe16942 100644 --- a/internal/web/templates/home.html +++ b/internal/web/templates/home.html @@ -132,15 +132,15 @@

天气足迹

记录期间经历过的天气
- +
{{range .Weather}} - - - + + + {{end}} diff --git a/internal/web/templates/places.html b/internal/web/templates/places.html index 253de8f..cb89f9d 100644 --- a/internal/web/templates/places.html +++ b/internal/web/templates/places.html @@ -39,7 +39,7 @@ {{if .Places}}
-
天气记录小时数平均气温范围
{{condition .Condition}}{{num .Count}}{{f1 .AvgTemp}} ℃{{f1 .MinTemp}} ~ {{f1 .MaxTemp}} ℃{{num .Count}}{{f1 .AvgTemp}} ℃{{f1 .MinTemp}} ~ {{f1 .MaxTemp}} ℃
+
@@ -57,15 +57,15 @@ {{if eq .Verdict 1}}👍 推荐{{else if eq .Verdict -1}}👎 踩雷{{end}} {{if or .Up .Down}}👍{{.Up}} · 👎{{.Down}}{{end}} - - + - - - - - + + + + + {{end}} diff --git a/internal/web/templates/report.html b/internal/web/templates/report.html index e259b19..4e43767 100644 --- a/internal/web/templates/report.html +++ b/internal/web/templates/report.html @@ -64,11 +64,11 @@
这一年到过纬度最高与最低的地点
{{if .North}}
-
地点城市主要类型图片{{if .City}}{{.City}}{{else}}—{{end}} + {{if .City}}{{.City}}{{else}}—{{end}} {{if .TopActivity}}{{.TopActivity}}{{else}}—{{end}} {{if .HasImage}}📷{{else}}—{{end}}{{num .VisitCount}}{{dur .DwellMinutes}}{{if .FirstAt}}{{d .FirstAt}}{{else}}—{{end}}{{if .LastAt}}{{d .LastAt}}{{end}}{{if .HasImage}}📷{{else}}—{{end}}{{num .VisitCount}}{{dur .DwellMinutes}}{{if .FirstAt}}{{d .FirstAt}}{{else}}—{{end}}{{if .LastAt}}{{d .LastAt}}{{end}}
+
- - {{if .South}}{{end}} + + {{if .South}}{{end}}
方位地点纬度
最北{{.North.Name}} · {{.North.City}}{{f1 .North.Lat}}°N
最南{{.South.Name}} · {{.South.City}}{{f1 .South.Lat}}°N
最北{{.North.Name}} · {{.North.City}}{{f1 .North.Lat}}°N
最南{{.South.Name}} · {{.South.City}}{{f1 .South.Lat}}°N
@@ -81,14 +81,14 @@

在路上

这一年 {{num .Overview.Movements}} 段行程,共 {{km .Overview.DistanceKm}} km
- +
{{range .Transports}} - - + + {{end}} diff --git a/internal/web/templates/stats.html b/internal/web/templates/stats.html index c620807..e949afd 100644 --- a/internal/web/templates/stats.html +++ b/internal/web/templates/stats.html @@ -103,15 +103,15 @@

省级行政区

覆盖情况
-
方式次数里程
{{with icon .Icon}}{{.}} {{end}}{{.Name}}{{num .Count}}{{km .Distance}} km{{num .Count}}{{km .Distance}} km
+
{{range .ProvinceStats}} - - - + + + {{end}} @@ -141,15 +141,15 @@
次数与估算里程
{{if .Transports}}
-
省份城市地点到访
{{.Province}}{{num .Cities}}{{num .Places}}{{num .Count}}{{num .Cities}}{{num .Places}}{{num .Count}}
+
{{range .Transports}} - - - + + + {{end}} @@ -181,14 +181,14 @@

天气与气温

记录期间经历的小时级天气
-
方式次数里程在途时间
{{with icon .Icon}}{{.}} {{end}}{{.Name}}{{num .Count}}{{km .Distance}} km{{dur .DwellMin}}{{num .Count}}{{km .Distance}} km{{dur .DwellMin}}
+
{{range .Weather}} - - + + {{end}} diff --git a/internal/web/templates/topic.html b/internal/web/templates/topic.html index 919ad66..8ef8b8e 100644 --- a/internal/web/templates/topic.html +++ b/internal/web/templates/topic.html @@ -42,17 +42,17 @@
共 {{num .Topic.PlaceCount}} 个,按到访次数排序
{{if .Points}}
-
天气小时数平均气温
{{condition .Condition}}{{num .Count}}{{f1 .AvgTemp}} ℃{{num .Count}}{{f1 .AvgTemp}} ℃
+
{{range .Points}} - - - - - + + + + + {{end}}
地点城市类型到访停留最近一次
{{if .Name}}{{.Name}}{{else}}(未命名){{end}}{{.City}}{{if .Act}}{{.Act}}{{end}}{{num .Count}}{{dur .Dwell}}{{.LastAt}}{{.City}}{{if .Act}}{{.Act}}{{end}}{{num .Count}}{{dur .Dwell}}{{.LastAt}}