优化移动端显示,解决筛选项潜在问题

This commit is contained in:
tamakyi
2026-09-18 15:42:07 +08:00
parent b327bc1230
commit fd23560bab
4 changed files with 240 additions and 52 deletions
+28
View File
@@ -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 {
+10
View File
@@ -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
}
+68 -2
View File
@@ -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% 不能省:这是 <button>,Chrome 下 display:flex + width:auto 仍是「收缩到内容宽」,
于是它只盖住左边一小条,右边的内容会从粘性条旁边穿过去。 */
display: flex; align-items: center; gap: 8px; width: 100%; min-height: 44px;
padding: 0 13px; border: 0; background: var(--glass-strong); color: var(--text-2);
font: inherit; font-size: 13.5px; font-weight: 500; text-align: left; cursor: pointer;
position: sticky; top: 0; z-index: 2;
}
.map-panel:not(.collapsed) .panel-fold { border-bottom: 1px solid var(--line); }
.panel-fold .pf-n { color: var(--text-3); font-size: 12px; font-weight: 400; }
.panel-fold .pf-c { margin-left: auto; padding-left: 4px; font-size: 12px; color: var(--text-3); }
.panel-fold:hover { color: var(--text); }
.panel-fold:hover .pf-c { color: var(--text); }
/* 「查询 / 重置」粘在面板底部:展开后表单 600+px、可视窗不到 400px,
不粘的话每改一次条件都要先滚到底才点得到查询。它必须是表单最后一个元素,
否则粘不住(实测后面还挂着内容时,底部会跟着内容一起往上跑)。 */
.filter-actions {
position: sticky; bottom: 0; z-index: 1;
margin: 0 -13px -13px; padding: 9px 13px;
background: var(--glass-strong); border-top: 1px solid var(--line);
}
.map-panel.collapsed { max-height: none; padding: 4px 0; }
.map-panel.collapsed #filter-form { display: none; }
/* 预设区间在窄屏会折成两三行(7 个预设 +「含家/工作」),单行横滑省下约 30~60px。 */
.map-panel .presets { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
.map-panel .presets::-webkit-scrollbar { display: none; }
.map-panel .presets > .chip { flex: 0 0 auto; }
.map-panel .map-count { display: none; }
}
.map-preview { height: 320px; border-radius: 8px; overflow: hidden; border: 1px solid var(--line); }
+99 -15
View File
@@ -21,22 +21,42 @@
<button class="btn sm" type="button" id="map-retry" hidden>重试</button>
</div>
<div class="map-panel">
<div class="map-panel" id="map-panel">
{{/* 窄屏折叠开关:面板在手机上铺满整行、还盖住小半个地图,默认收成这一条。
桌面端它 display:none,面板照旧常开。窄屏的计数只在这里(表单里那行被隐藏),
展开/收起都要靠它显示当前筛出多少个点。 */}}
<button class="panel-fold" id="panel-fold" type="button" aria-controls="filter-form" aria-expanded="true">
<span>筛选</span>
<span class="pf-n" id="pf-count">{{num (len .Points)}} 个地点</span>
<span class="pf-c" id="pf-caret" aria-hidden="true">▾</span>
</button>
<form id="filter-form" method="get" action="/map">
<h3>时间范围</h3>
<div class="chips" style="margin-bottom:10px">
<div class="chips presets">
{{range .Presets}}<a class="chip {{if .Active}}on{{end}}" href="{{$.RangeLink .From .To}}">{{.Label}}</a>{{end}}
{{if .IsAdmin}}
<a class="chip {{if .ShowHomeWork}}on{{end}}" href="{{.HWLink}}" title="仅登录后的你自己可见,访客看不到这两类记录">含家/工作</a>
{{end}}
</div>
{{if .IsAdmin}}
{{/* 「含家/工作」原来跟在预设行末尾,而窄屏预设行改成单行横滑后它被滑到屏幕外、
没有任何提示(站长实测发现)。挪出来单独一行——它本来也不是时间段预设,
而是「这两类记录给不给自己看」的开关。 */}}
<div class="chips hw-row">
<a class="chip {{if .ShowHomeWork}}on{{end}}" href="{{.HWLink}}" title="仅登录后的你自己可见,访客看不到这两类记录">含家/工作</a>
</div>
{{end}}
<div class="row" style="gap:8px;margin-bottom:10px">
<input type="date" name="from" value="{{.From}}" style="flex:1">
<input type="date" name="to" value="{{.To}}" style="flex:1">
</div>
{{if .Activities}}
<h3>地点类型</h3>
{{/* 12 个类型标签在窄屏要占两百多像素,是面板里最高的一块,所以整块做成可折叠:
标题里带上已选的类型名,收起时也知道当前选了什么。默认展开/收起由下面的脚本按视口定。 */}}
<details class="type-fold" id="type-fold" open>
<summary>
<span>地点类型</span>
<span class="tf-sel">{{if .ActivityIDs}}{{range $i, $a := .Activities}}{{if $.IsAct $a.ID}}{{$a.Name}} {{end}}{{end}}{{else}}全部{{end}}</span>
<span class="tf-arrow" aria-hidden="true">▸</span>
</summary>
<div class="chips" style="margin-bottom:10px">
{{range $i, $a := .Activities}}{{if or (lt $i 12) ($.IsAct $a.ID)}}
<a class="chip {{if $.IsAct $a.ID}}on{{end}}" href="{{$.ActLink $a.ID}}">
@@ -54,8 +74,20 @@
</div>
</details>
{{end}}
</details>
{{end}}
{{/* 窄屏把「其它条件 + 图层开关」整块收进 details:这两块加起来近 240px,不收的话
展开后面板仍要高出一屏。默认展开/收起由下面的脚本按视口定(桌面展开、窄屏收起)。
标题里带上已生效的条件,收起时也知道当前不是「无条件」。
act / hw 这两个 hidden 留在 details 外面——表单要始终带上它们。 */}}
<details class="adv-fold" id="adv-fold">
<summary>
<span>更多筛选</span>
<span class="tf-sel">{{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}}</span>
<span class="tf-arrow" aria-hidden="true">▸</span>
</summary>
<h3>其它条件</h3>
<div class="row" style="gap:8px;margin-bottom:10px">
<select name="city" style="flex:1">
@@ -77,20 +109,26 @@
</select>
<input type="text" name="q" value="{{.Keyword}}" placeholder="关键词" style="flex:1">
</div>
{{range .ActivityIDs}}<input type="hidden" name="act" value="{{.}}">{{end}}
{{if .ShowHomeWork}}<input type="hidden" name="hw" value="1">{{end}}
<div class="row">
<button class="btn" type="submit" style="flex:1">查询</button>
<a class="btn" href="/map">重置</a>
</div>
<div class="chips" style="margin-top:10px">
<div class="chips" style="margin-bottom:12px">
<label class="chip" style="user-select:none"><input type="checkbox" id="track-toggle" style="accent-color:var(--accent)"> 行程轨迹</label>
<label class="chip" style="user-select:none"><input type="checkbox" id="heat-toggle" style="accent-color:var(--accent)"> 到访热力</label>
<label class="chip" style="user-select:none"><input type="checkbox" id="city-toggle" style="accent-color:var(--accent)"> 城市聚合</label>
<label class="chip" style="user-select:none"><input type="checkbox" id="gpx-toggle" style="accent-color:var(--accent)"> GPX 轨迹</label>
</div>
<div class="muted" style="margin-top:9px;font-size:12px">当前显示 <b id="pt-count">{{num (len .Points)}}</b> 个地点</div>
</details>
{{range .ActivityIDs}}<input type="hidden" name="act" value="{{.}}">{{end}}
{{if .ShowHomeWork}}<input type="hidden" name="hw" value="1">{{end}}
{{/* 计数:桌面端就留在表单里(折叠条在桌面 display:none,没有别处可放);
窄屏这行整行隐藏,改由粘顶条上的 #pf-count 承担——窄屏差的正是这一行的高度。 */}}
<div class="map-count muted">当前显示 <b id="pt-count">{{num (len .Points)}}</b> 个地点</div>
<div class="row filter-actions">
<button class="btn" type="submit" style="flex:1">查询</button>
<a class="btn" href="/map">重置</a>
</div>
</form>
</div>
@@ -104,6 +142,45 @@
{{end}}
</div>
<script>
// 筛选面板折叠:状态同步执行,免得手机上先闪一下整块面板再收起。
// 只切换 class,具体收起后什么样由 CSS 的窄屏媒体查询决定(桌面端按钮本身就不显示)。
(function () {
var panel = document.getElementById('map-panel');
var btn = document.getElementById('panel-fold');
var caret = document.getElementById('pf-caret');
if (!panel || !btn) return;
var KEY = 'rond-map-fold';
var saved = null;
try { saved = localStorage.getItem(KEY); } catch (e) { /* 隐私模式禁用了 localStorage,忽略 */ }
function apply(collapsed) {
panel.classList.toggle('collapsed', collapsed);
btn.setAttribute('aria-expanded', String(!collapsed));
btn.title = collapsed ? '展开筛选' : '收起筛选';
if (caret) caret.textContent = collapsed ? '▸' : '▾';
}
// 没记过状态时窄屏默认收起——「全部」这种宽区间能有一两百个点,
// 面板一上来就占掉 50% 的屏幕,地图反而看不清。
var desk = window.matchMedia('(min-width: 621px)');
apply(saved === null ? !desk.matches : saved === '1');
// 地点类型 / 更多筛选同理:窄屏收起(两块各占两百多像素),桌面有地方就一直展开。
// 不记本地:窄屏上点类型或改筛选会整页跳转,重载后本就该收回去。
['type-fold', 'adv-fold'].forEach(function (id) {
var d = document.getElementById(id);
if (d) d.open = desk.matches;
});
btn.addEventListener('click', function () {
var collapsed = !panel.classList.contains('collapsed');
apply(collapsed);
try { localStorage.setItem(KEY, collapsed ? '1' : '0'); } catch (e) { /* 同上 */ }
});
})();
</script>
<script>
window.addEventListener('load', function () {
// 首屏不带地点数据(全量内联会让 HTML 膨胀到 ~58KB),
@@ -115,6 +192,13 @@
var statusBox = document.getElementById('map-status');
var statusText = document.getElementById('map-status-text');
var retryBtn = document.getElementById('map-retry');
var pfCount = document.getElementById('pf-count');
// 计数两处同步:桌面端表单里那行(窄屏被隐藏)、窄屏粘顶条上的 #pf-count
function setCount(n) {
if (ptCount) ptCount.textContent = n;
pfCount.textContent = n + ' 个地点';
}
function qs() {
return new URLSearchParams(new FormData(form)).toString();
@@ -151,7 +235,7 @@
// 先交给时间轴:它自己会在「有时间的点」上做过滤再决定画什么
setupTimeline(pts);
window.rondMap.setPoints(pts);
ptCount.textContent = pts.length;
setCount(pts.length);
hideStatus();
return true;
}).catch(function (err) {