From fd23560babd1693409a5376412573e80c3457dd2 Mon Sep 17 00:00:00 2001 From: tamakyi Date: Fri, 18 Sep 2026 15:42:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=A7=BB=E5=8A=A8=E7=AB=AF?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=EF=BC=8C=E8=A7=A3=E5=86=B3=E7=AD=9B=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E6=BD=9C=E5=9C=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/stats/stats.go | 28 +++++ internal/web/handlers_public.go | 10 ++ internal/web/static/css/app.css | 70 +++++++++++- internal/web/templates/map.html | 184 +++++++++++++++++++++++--------- 4 files changed, 240 insertions(+), 52 deletions(-) diff --git a/internal/stats/stats.go b/internal/stats/stats.go index 0dcea78..3b81254 100644 --- a/internal/stats/stats.go +++ b/internal/stats/stats.go @@ -328,6 +328,34 @@ func (d DB) Activities(ctx context.Context, datasetID int64) ([]ActivityStat, er return out, rows.Err() } +// ActivityCounts 返回「类型 → 到访次数」,给筛选器上的类型标签显示数字用。 +// +// 用的是**除类型自身以外**的全部当前条件。以前标签上的数字取自 Activities(datasetID)、 +// 一个条件都不带,选了城市之后还是全库的数(站长实测反馈:「这个城市下家/工作各去过 +// 几次」看不出来);但也不能把 ActivityIDs 一起套进去——那样没选中的类型会全变成 0, +// 标签就失去意义了。 +// 区域限制照常生效(Regions 在 visitsWhere 里),访客不该从计数反推被隐藏区域的量级。 +func (d DB) ActivityCounts(ctx context.Context, f Filter) (map[int64]int, error) { + f.ActivityIDs = nil + w := f.visitsWhere(0) + rows, err := d.QueryContext(ctx, + `SELECT COALESCE(v.activity_id,0), count(*) FROM visits v`+w.SQL()+` GROUP BY 1`, w.args...) + if err != nil { + return nil, err + } + defer rows.Close() + out := map[int64]int{} + for rows.Next() { + var id int64 + var n int + if err := rows.Scan(&id, &n); err != nil { + return nil, err + } + out[id] = n + } + return out, rows.Err() +} + // ---------- 城市 / 省份 ---------- type CityStat struct { diff --git a/internal/web/handlers_public.go b/internal/web/handlers_public.go index cd67bf6..8cfc104 100644 --- a/internal/web/handlers_public.go +++ b/internal/web/handlers_public.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "log" "net/http" "sort" "strconv" @@ -71,6 +72,15 @@ func (s *Server) pageBase(r *http.Request, nav, title string) (Page, error) { if p.Activities, err = s.q.Activities(ctx, ds.ID); err != nil { return p, err } + // 类型标签上的次数要跟着当前筛选走(时间 / 城市 / 标签 / 区域…),否则选了城市之后 + // 数字仍是全库的。算失败就保留上面那份全量数字:一个角标不值得把整页搞成 500。 + if counts, cerr := s.q.ActivityCounts(ctx, s.filterOf(p)); cerr != nil { + log.Printf("类型分面计数失败: %v", cerr) + } else { + for i := range p.Activities { + p.Activities[i].Count = counts[p.Activities[i].ID] + } + } if p.TagOptions, err = s.q.Tags(ctx, ds.ID); err != nil { return p, err } diff --git a/internal/web/static/css/app.css b/internal/web/static/css/app.css index cc019f7..57a8491 100644 --- a/internal/web/static/css/app.css +++ b/internal/web/static/css/app.css @@ -305,9 +305,39 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent- .map-panel { position: absolute; top: 12px; left: 12px; z-index: 400; width: 286px; max-height: calc(100% - 24px); overflow-y: auto; background: var(--glass-strong); backdrop-filter: blur(8px); - border: 1px solid var(--line); border-radius: var(--radius); padding: 13px; box-shadow: var(--shadow); + border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); } +/* 内边距挂在表单上,不挂面板:窄屏的折叠条与操作行要 sticky 到面板的**内边缘**, + 面板自己带 padding 的话它们只能粘在内容区边界上,边上会留一条缝让滚动内容穿过去 + (实测:折叠条粘在内容区顶、上方 13px 仍能看到内容滚过)。 */ +#filter-form { padding: 13px; } +/* 时间范围预设行:原来靠行内 style 加的下边距,挪到类上,好让窄屏媒体查询能一起改。 */ +.map-panel .presets { margin-bottom: 10px; } +/* 「含家/工作」单独一行(不属于时间段预设)。窄屏预设行是单行横滑的, + 它留在里面会被滑到屏幕外;单独一行上下各留 10px,读起来也像另一个开关。 */ +.map-panel .hw-row { margin-bottom: 10px; } .map-panel h3 { font-size: 13px; margin-bottom: 9px; color: var(--text-2); } +/* 筛选面板的折叠开关:桌面上面板只占 286px、不碍事,按钮直接不显示,只在窄屏出现 */ +.panel-fold { display: none; } +/* 筛选面板里可折叠的两块(地点类型 / 更多筛选):这两块在窄屏各占两百多像素, + 是面板高度的大头。默认值在 map.html 的脚本里按视口定(桌面展开、窄屏收起); + 收起时标题里显示已选/已生效的那几项,否则收起后就不知道当前选了什么。 */ +.type-fold > summary, .adv-fold > summary { + list-style: none; display: flex; align-items: center; gap: 6px; + font-size: 13px; color: var(--text-2); cursor: pointer; padding: 2px 0 8px; +} +.type-fold > summary::-webkit-details-marker, .adv-fold > summary::-webkit-details-marker { display: none; } +.type-fold > summary:hover, .adv-fold > summary:hover { color: var(--accent); } +.type-fold > summary .tf-sel, .adv-fold > summary .tf-sel { + flex: 0 1 auto; min-width: 0; color: var(--text-3); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.type-fold > summary .tf-arrow, .adv-fold > summary .tf-arrow { flex: 0 0 auto; margin-left: auto; font-size: 11px; color: var(--text-3); } +.type-fold[open] > summary .tf-arrow, .adv-fold[open] > summary .tf-arrow { transform: rotate(90deg); } +/* 展开后标签行/控件自己就说明了当前值,标题里那份就不必再占一行 */ +.type-fold[open] > summary .tf-sel, .adv-fold[open] > summary .tf-sel { display: none; } +/* 「更多筛选」里的 h3 是二级标题,别跟折叠标题撞成一样大 */ +.adv-fold > h3 { font-size: 12.5px; color: var(--text-3); margin-bottom: 8px; } .map-legend { /* bottom 让开右下角那行底图版权(约 15px 高),否则两者会叠在一起 */ position: absolute; bottom: 26px; right: 12px; z-index: 400; background: var(--glass-strong); @@ -417,8 +447,44 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent- .map-status .btn { border-color: rgba(255,255,255,.35); background: rgba(255,255,255,.12); color: #fff; flex: 0 0 auto; } .map-status .btn:hover { background: rgba(255,255,255,.22); } @media (max-width: 620px) { - .map-panel { left: 8px; right: 8px; width: auto; max-height: 44%; top: 8px; } + /* 上限 50%(原 44%):折叠条本身占 44px,不加这 6% 的话展开后能看到的筛选内容 + 反而比改动前更少。50% 刚好把那一条的高度补回来。 */ + .map-panel { left: 8px; right: 8px; width: auto; max-height: 50%; top: 8px; } .map-legend { display: none; } + /* 窄屏上面板铺满整行、最多能吃掉 50% 的屏幕,所以默认收起成一条: + 只剩「筛选 + 数量 + 箭头」约 52px 高,想改条件再点开。 + 「筛选」两字与箭头之间的数量在展开时也保留——表单里那行重复的计数已去掉, + 两块状态共用这一个数字,展开/收起都能看到当前筛出多少个点。 + 展开时它 sticky 在面板顶部:表单比可视窗高,滚到下面之后还得能点回来收起。 + 不透明底是因为滚动内容会从它下面穿过(面板本身已无内边距,它天然铺满整行)。 */ + .panel-fold { + /* width:100% 不能省:这是 -
+
+ {{/* 窄屏折叠开关:面板在手机上铺满整行、还盖住小半个地图,默认收成这一条。 + 桌面端它 display:none,面板照旧常开。窄屏的计数只在这里(表单里那行被隐藏), + 展开/收起都要靠它显示当前筛出多少个点。 */}} +

时间范围

-
+
{{range .Presets}}{{.Label}}{{end}} - {{if .IsAdmin}} - 含家/工作 - {{end}}
+ {{if .IsAdmin}} + {{/* 「含家/工作」原来跟在预设行末尾,而窄屏预设行改成单行横滑后它被滑到屏幕外、 + 没有任何提示(站长实测发现)。挪出来单独一行——它本来也不是时间段预设, + 而是「这两类记录给不给自己看」的开关。 */}} + + {{end}}
{{if .Activities}} -

地点类型

-
- {{range $i, $a := .Activities}}{{if or (lt $i 12) ($.IsAct $a.ID)}} - - {{$a.Name}} - - {{end}}{{end}} -
- {{if gt (len .Activities) 12}} -
- 展开其余 {{len .Activities}} 个类型 -
- {{range $i, $a := .Activities}}{{if and (ge $i 12) (not ($.IsAct $a.ID))}} - {{$a.Name}} + {{/* 12 个类型标签在窄屏要占两百多像素,是面板里最高的一块,所以整块做成可折叠: + 标题里带上已选的类型名,收起时也知道当前选了什么。默认展开/收起由下面的脚本按视口定。 */}} +
+ + 地点类型 + {{if .ActivityIDs}}{{range $i, $a := .Activities}}{{if $.IsAct $a.ID}}{{$a.Name}} {{end}}{{end}}{{else}}全部{{end}} + + +
+ {{range $i, $a := .Activities}}{{if or (lt $i 12) ($.IsAct $a.ID)}} + + {{$a.Name}} + {{end}}{{end}}
+ {{if gt (len .Activities) 12}} +
+ 展开其余 {{len .Activities}} 个类型 +
+ {{range $i, $a := .Activities}}{{if and (ge $i 12) (not ($.IsAct $a.ID))}} + {{$a.Name}} + {{end}}{{end}} +
+
+ {{end}}
{{end}} - {{end}} -

其它条件

-
- - -
-
- - -
+ {{/* 窄屏把「其它条件 + 图层开关」整块收进 details:这两块加起来近 240px,不收的话 + 展开后面板仍要高出一屏。默认展开/收起由下面的脚本按视口定(桌面展开、窄屏收起)。 + 标题里带上已生效的条件,收起时也知道当前不是「无条件」。 + act / hw 这两个 hidden 留在 details 外面——表单要始终带上它们。 */}} +
+ + 更多筛选 + {{if or .City .Keyword .TagID (gt .MinDwell 0)}}{{if .City}}{{.City}} {{end}}{{range .TagOptions}}{{if and (eq .ID $.TagID) (ne .ID 0)}}{{.Name}} {{end}}{{end}}{{if gt .MinDwell 0}}停留≥{{.MinDwell}}分 {{end}}{{if .Keyword}}“{{.Keyword}}”{{end}}{{else}}城市 / 标签 / 停留 / 关键词{{end}} + + + +

其它条件

+
+ + +
+
+ + +
+ +
+ + + + +
+
+ {{range .ActivityIDs}}{{end}} {{if .ShowHomeWork}}{{end}} -
+ {{/* 计数:桌面端就留在表单里(折叠条在桌面 display:none,没有别处可放); + 窄屏这行整行隐藏,改由粘顶条上的 #pf-count 承担——窄屏差的正是这一行的高度。 */}} +
当前显示 {{num (len .Points)}} 个地点
+ +
重置
-
- - - - -
-
当前显示 {{num (len .Points)}} 个地点
@@ -104,6 +142,45 @@ {{end}}
+ +