先出方案(frontend-plan-2026-10-09.md)再动手,四批按顺序落地。方法上先存基线、再用同一份 探针在改动前后逐值 diff —— 这批里有 6 处问题只有「量」才看得见,看截图全是正常的。 A 设计令牌(app.css 为主) - 字阶收敛到 6 档 --fs-xs/sm/md/lg/xl/2xl = 11/13/15/21/26/34px。原来 app.css 有 19 种字号、 模板里另有 54 处内联字号,光 12~15px 之间就挤了 4 档 —— 层级挤不出来,只能靠加粗硬顶。 - 两个「字外档」不是漏的:--fs-control: 16px(窄屏表单控件必须 ≥16px,否则 iOS 聚焦放大整页, 属功能约束);--glyph-*(✕/箭头/空态图标按控件尺寸定,硬塞进字阶只会把按钮撑坏)。 - 字重收敛到 400/500/600(删掉 650 与 700)。 - 圆角 4 档 --r-*、阴影 3 档 --sh-1/2/3(暗色整套覆盖)、间距 --sp-1..5、 卡片内边距单列 --card-pad(与折叠卡片标题行的负 margin 绑在一起,改一个即改两处)。 - 门禁 TestNoBareFontSizePx:静态扫 app.css 与全部模板,出现 font-size: <数字>px 即红。 B 卡片分主次 - .card--hero / .card(默认)/ .card--quiet 三档。quiet 走「页面底色 + 细边 + 无阴影」—— 第一版拿 --bg-2 当底色,实测它与页面底 #f6f7f8 只差 4/255,看不出边界、像破了个洞。 - 首屏 KPI 从「4×2 等重白盒」改成 .kpi-lead(主数 + 一行上下文)+ 5 个常规指标,8 个数字一个没少。 C 图表说人话 - 月度分布:柱底加基线;峰值柱不透明拉满、柱顶标次数(.pv),副标题里不再重复写「最高 N 次」。 - 一天中的分布:把「0/6/12/18 四个数字」换成 6 小时一段的标尺(深夜/上午/下午/晚上), 用同一套 24 列网格,段边界与柱子严格对齐。 - 首页月度图同样加峰值锚点(给 HomeData 补 MaxMonthly,与统计页同口径 maxOf)。 - 空态没动:查下来已经统一(8 页共用 emptystate、局部走 .empty)。 D1 类型色语义化(结论与计划不同,按实测改了) - 先算对比度:白底上 yellow 1.87 / mint 2.12 / orange 2.20 / green 2.22 / cyan 2.54 / teal 2.57, 6 个色连图形对象 3:1 都到不了;暗底只有 indigo 2.90 不够。 - 所以**没有**把类型色用到 chips / 表格类型列 / KPI 强调 —— 那几处要的是文字色,只会更难读。 这也解释了全站为什么一直只用 8px 小圆点做身份标记。浅色那 6 个色一个没动: 它们是手机端与地图上的同一套身份色,改暗会破坏对应关系。 - 实际做的:色值收口成 --cat-* / --cat-*-soft(26 个变量);模板 colorof → csscolor / csssoft(19 处); 暗色只提亮 indigo(2.90 → 3.55:1);colorOf 成了死代码,删掉。 - ⚠️ 返回值必须是 template.CSS:html/template 的 CSS 过滤器不认识 var(),普通 string 会被静默 换成 ZgotmplZ —— 页面上所有圆点当场变透明,而且不报错、模板测试照过。挂了它等于关掉那道过滤, 所以数据侧色值先过严格校验(只有 # + 6 位 hex 放行),否则 #"><img… 能逃出 style 属性。 守卫 TestCSSColorSurvivesTemplateSanitizer 把两个方向都钉住(做过变异验证)。 D2 窄屏导航 - 改前 ≤620px 靠横向滚动兜底:390 溢 63px、320 溢 133px,「统计」被切成半个字紧贴后面的按钮, 渐隐提示实际看不出来。现在链接本体抽成 {{define "navitems"}} 渲染两遍:桌面 .nav-links 平铺、 窄屏 .nav-menu(原生 <details>,不靠 JS 也能开合);JS 只补点外面 / Esc / 点链接后收起。 - ⚠️ 顶栏 --z-nav 抬到 1200:菜单是 .nav 的子元素、自己抬不动层叠上下文,不抬就会被地图页的 --z-map-overlay(1100) 盖住。已在 /map 上用 elementFromPoint 实测。 - 守卫 TestNarrowNavCollapsesToMenu 钉「CSS 两半 + 模板两个类 + 渲染两遍」的成对关系。 途中发现并一并修掉的(都只有量 DOM 才看得见) - Leaflet 的 .leaflet-container 字号是 0.75rem、跟着根字号漂:根 14 → 13px 后地图署名/比例尺 掉到 9.8px(低于本站 11px 下限)、弹窗正文也跟着缩。已按自己的字阶钉死。 - h1..h4 原来只设了字重与 margin、没设字号,靠浏览器默认的 2em/1.5em/1.17em —— 同样跟着根字号漂: /stats 那两个 h2 从 21px 变成 19.5px,比同页卡片标题大一圈。已显式给出字号。 - <b>/<strong> 浏览器默认 700,比全站最高层级(标题 600)还重,已钉成 600。 回归 - ./scripts/verify.sh:PASS=348 FAIL=0 SKIP=0 - internal/fog 加 -race:PASS=22 FAIL=0 SKIP=0 - JS 单测 34/34 - 移动端 320 / 390 溢出全 0(改前 /admin 在 320 还溢 34px);桌面 1440 与改前一致(−15 是滚动条宽) - 全站 7 个页面都没有低于 11px 的文字 说明:A/B 与 C/D 本想拆两笔提交,但 app.css / layout.html / view.go 里两批改动交织,按文件拆不开、 按 hunk 拆又容易做出「中间那笔编译不过」的历史,所以合成一笔、在消息里分段。
378 lines
19 KiB
HTML
378 lines
19 KiB
HTML
{{/* navitems 是导航链接本体,抽出来是因为它要渲染两遍:桌面上在 .nav-links 里平铺,
|
||
窄屏整行一定放不下(实测 390 溢 63px、320 溢 133px,最深处的「统计」会被切成半个字),
|
||
所以另收进 .nav-menu 那个折叠菜单。两份靠 display 二选一,DOM 里不会同时可见。
|
||
|
||
页面开关只管访客:站长登录后始终能访问全部页面(guardPage 也是这么放的),
|
||
导航必须跟着一起放行,否则关掉页面开关后站长自己也只剩「主页」一个入口。 */}}
|
||
{{define "navitems"}}
|
||
<a href="/" class="{{if eq .Nav "home"}}on{{end}}">主页</a>
|
||
{{if or .IsAdmin (.Settings.HasPage "map")}}<a href="/map" class="{{if eq .Nav "map"}}on{{end}}">地图</a>{{end}}
|
||
{{if or .IsAdmin (.Settings.HasPage "places")}}<a href="/places" class="{{if eq .Nav "places"}}on{{end}}">地点</a>{{end}}
|
||
{{if or .IsAdmin (.Settings.HasPage "timeline")}}<a href="/timeline" class="{{if eq .Nav "timeline"}}on{{end}}">时间线</a>{{end}}
|
||
{{if or .IsAdmin (.Settings.HasPage "stats")}}<a href="/stats" class="{{if eq .Nav "stats"}}on{{end}}">统计</a>{{end}}
|
||
{{if .HasTopics}}<a href="/topics" class="{{if eq .Nav "topics"}}on{{end}}">专题</a>{{end}}
|
||
{{if or .IsAdmin (.Settings.HasPage "report")}}<a href="/report" class="{{if eq .Nav "report"}}on{{end}}">报告</a>{{end}}
|
||
{{end}}
|
||
|
||
{{define "nav"}}
|
||
<nav class="nav">
|
||
<div class="nav-inner">
|
||
<a class="brand" href="/" title="{{.Settings.SiteTitle}}"><span class="avatar">{{.Settings.OwnerAvatar}}</span><span class="t">{{.Settings.SiteTitle}}</span></a>
|
||
<div class="nav-links">{{template "navitems" .}}</div>
|
||
<details class="nav-menu">
|
||
<summary aria-label="导航菜单">菜单</summary>
|
||
<nav class="nav-menu-body">{{template "navitems" .}}</nav>
|
||
</details>
|
||
<div class="nav-right">
|
||
{{if .User}}
|
||
<span class="who">{{.User.DisplayName}}</span>
|
||
<a class="btn sm" href="/admin"><span class="hide-sm">后台</span><span class="only-sm">后台</span></a>
|
||
<form method="post" action="/admin/logout" style="display:inline"><button class="btn sm" type="submit">退出</button></form>
|
||
{{else}}
|
||
<a class="btn sm" href="/admin"><span class="hide-sm">登录后台</span><span class="only-sm">登录</span></a>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
<script>
|
||
/* 1) 620px 以上:链接行万一还是放不下(以后加了入口),横向可滑 + 右缘渐隐提示。
|
||
2) 620px 及以下:整行已经收进 .nav-menu,这里只管菜单开合——原生 <details> 不会
|
||
点外面自动收、也不响应 Esc,补上;点链接后也收掉,免得盖着刚打开的页面。 */
|
||
(function () {
|
||
var el = document.querySelector('.nav-links');
|
||
if (el) {
|
||
var update = function () {
|
||
el.classList.toggle('overflow-right', el.scrollWidth - el.clientWidth - el.scrollLeft > 2);
|
||
};
|
||
update();
|
||
el.addEventListener('scroll', update, { passive: true });
|
||
window.addEventListener('resize', update);
|
||
}
|
||
var menu = document.querySelector('.nav-menu');
|
||
if (!menu) return;
|
||
document.addEventListener('click', function (e) { if (!menu.contains(e.target)) menu.open = false; });
|
||
menu.addEventListener('click', function (e) {
|
||
if (e.target.closest && e.target.closest('a')) menu.open = false;
|
||
});
|
||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') menu.open = false; });
|
||
})();
|
||
</script>
|
||
{{end}}
|
||
|
||
{{define "filters"}}
|
||
<form class="card filters-card" method="get" action="{{.FilterBase}}" id="filters-form">
|
||
{{/* 窄屏折叠开关,与地图页筛选面板同一套(.panel-fold):桌面端它 display:none、表单照旧常开;
|
||
手机上默认收成这一条,只留「筛选 + 当前范围 + 箭头」。收起只是 CSS 隐藏,
|
||
表单里的控件都还在,筛选条件照旧随提交带上(act/hw/sort 那几个 hidden 也在)。 */}}
|
||
<button class="panel-fold" id="filters-fold" type="button" aria-controls="filters-form" aria-expanded="true">
|
||
<span>筛选</span>
|
||
<span class="pf-n">{{.RangeLabel}}{{if .ActiveChips}} · {{len .ActiveChips}} 个条件{{end}}</span>
|
||
<span class="pf-c" id="filters-fold-caret" aria-hidden="true">▾</span>
|
||
</button>
|
||
<div class="chips" style="margin-bottom:12px">
|
||
{{range .Presets}}<a class="chip {{if .Active}}on{{end}}" href="{{$.RangeLink .From .To}}">{{.Label}}</a>{{end}}
|
||
<span class="chip" style="background:transparent;color:var(--text-3)">{{.RangeLabel}}</span>
|
||
{{if .IsAdmin}}
|
||
<a class="chip {{if .ShowHomeWork}}on{{end}}" href="{{.HWLink}}" title="仅登录后的你自己可见,访客看不到这两类记录">含家/工作</a>
|
||
{{end}}
|
||
</div>
|
||
|
||
{{/* 已选条件:以前只能靠「重置」全清,想撤掉其中一个得手动改回「全部」再点应用 */}}
|
||
{{$chips := .ActiveChips}}
|
||
{{if $chips}}
|
||
<div class="chips active-filters">
|
||
<span class="af-label">已选</span>
|
||
{{range $chips}}<a class="chip on" href="{{.RemoveURL}}" title="去掉这个条件">{{.Label}}<span class="af-x">×</span></a>{{end}}
|
||
<a class="chip af-clear" href="{{.FilterBase}}">全部清空</a>
|
||
</div>
|
||
{{end}}
|
||
|
||
{{if .Activities}}
|
||
<div class="chips" style="margin-bottom:{{if gt (len .Activities) 14}}8{{else}}12{{end}}px">
|
||
{{range $i, $a := .Activities}}{{if or (lt $i 14) ($.IsAct $a.ID)}}
|
||
<a class="chip {{if $.IsAct $a.ID}}on{{end}}" href="{{$.ActLink $a.ID}}" title="{{num $a.Count}} 次到访">
|
||
<span class="dot" style="background:{{csscolor $a.Color}}"></span>{{$a.Name}}
|
||
<span class="muted">{{num $a.Count}}</span>
|
||
</a>
|
||
{{end}}{{end}}
|
||
</div>
|
||
{{if gt (len .Activities) 14}}
|
||
<details class="more-types">
|
||
<summary>展开其余 {{len .Activities}} 个类型</summary>
|
||
<div class="chips">
|
||
{{range $i, $a := .Activities}}{{if and (ge $i 14) (not ($.IsAct $a.ID))}}
|
||
<a class="chip" href="{{$.ActLink $a.ID}}">
|
||
<span class="dot" style="background:{{csscolor $a.Color}}"></span>{{$a.Name}}
|
||
<span class="muted">{{num $a.Count}}</span>
|
||
</a>
|
||
{{end}}{{end}}
|
||
</div>
|
||
</details>
|
||
{{end}}
|
||
{{end}}
|
||
|
||
<div class="row" style="align-items:flex-end">
|
||
<label class="f" style="flex:1 1 140px;margin-bottom:0"><span>起始日期</span><input type="date" name="from" value="{{.From}}"></label>
|
||
<label class="f" style="flex:1 1 140px;margin-bottom:0"><span>结束日期</span><input type="date" name="to" value="{{.To}}"></label>
|
||
<label class="f" style="flex:1 1 130px;margin-bottom:0"><span>城市</span>
|
||
<select name="city">
|
||
<option value="">全部城市</option>
|
||
{{range .CityList}}<option value="{{.}}" {{if eq . $.City}}selected{{end}}>{{.}}</option>{{end}}
|
||
</select>
|
||
</label>
|
||
<label class="f" style="flex:1 1 130px;margin-bottom:0"><span>标签</span>
|
||
<select name="tag">
|
||
<option value="0">全部标签</option>
|
||
{{range .TagOptions}}<option value="{{.ID}}" {{if eq .ID $.TagID}}selected{{end}}>{{.Name}}</option>{{end}}
|
||
</select>
|
||
</label>
|
||
<label class="f" style="flex:1 1 120px;margin-bottom:0"><span>最短停留</span>
|
||
<select name="min">
|
||
<option value="0" {{if eq .MinDwell 0}}selected{{end}}>不限</option>
|
||
<option value="5" {{if eq .MinDwell 5}}selected{{end}}>≥ 5 分钟</option>
|
||
<option value="15" {{if eq .MinDwell 15}}selected{{end}}>≥ 15 分钟</option>
|
||
<option value="30" {{if eq .MinDwell 30}}selected{{end}}>≥ 30 分钟</option>
|
||
<option value="60" {{if eq .MinDwell 60}}selected{{end}}>≥ 1 小时</option>
|
||
</select>
|
||
</label>
|
||
<label class="f" style="flex:2 1 180px;margin-bottom:0"><span>关键词</span><input type="text" name="q" value="{{.Keyword}}" placeholder="地点 / 城市 / 类型,回车即可"></label>
|
||
<div style="display:flex;gap:8px">
|
||
{{/* 下拉与日期改动即生效(见下面的脚本);这个按钮是给「关键词」和
|
||
关了 JS 的浏览器兜底的,所以不再是主按钮样式 */}}
|
||
<button class="btn" type="submit">查询</button>
|
||
<a class="btn" href="{{.FilterBase}}">重置</a>
|
||
</div>
|
||
</div>
|
||
{{range .ActivityIDs}}<input type="hidden" name="act" value="{{.}}">{{end}}
|
||
{{if .ShowHomeWork}}<input type="hidden" name="hw" value="1">{{end}}
|
||
{{if .Sort}}<input type="hidden" name="sort" value="{{.Sort}}">{{end}}
|
||
</form>
|
||
{{/* 窄屏折叠:脚本必须紧跟这个表单 —— 放到 body 末尾时,手机上会先按「全部展开」画一帧
|
||
再收起,看着就是闪一下整张大卡(地图页那块面板也是贴在面板后面)。收起态记在本地,
|
||
翻页/换页之后不该又自己弹开。 */}}
|
||
<script>
|
||
(function () {
|
||
var form = document.getElementById('filters-form');
|
||
var btn = document.getElementById('filters-fold');
|
||
if (!form || !btn) return;
|
||
var caret = document.getElementById('filters-fold-caret');
|
||
var KEY = 'rond-filters-fold';
|
||
var desk = window.matchMedia('(min-width: 621px)');
|
||
var saved = null;
|
||
try { saved = localStorage.getItem(KEY); } catch (e) { /* 隐私模式禁用了,忽略 */ }
|
||
|
||
function apply(collapsed) {
|
||
form.classList.toggle('collapsed', collapsed);
|
||
btn.setAttribute('aria-expanded', String(!collapsed));
|
||
btn.title = collapsed ? '展开筛选' : '收起筛选';
|
||
if (caret) caret.textContent = collapsed ? '▸' : '▾';
|
||
}
|
||
// 没记过状态时窄屏默认收起。桌面端这条 class 不生效(显示规则整条在窄屏媒体查询里),
|
||
// 所以同一个浏览器在手机上收起过、回桌面照样看得见筛选。
|
||
apply(saved === null ? !desk.matches : saved === '1');
|
||
|
||
btn.addEventListener('click', function () {
|
||
var collapsed = !form.classList.contains('collapsed');
|
||
apply(collapsed);
|
||
try { localStorage.setItem(KEY, collapsed ? '1' : '0'); } catch (e) { /* 同上 */ }
|
||
});
|
||
})();
|
||
</script>
|
||
{{end}}
|
||
|
||
{{define "emptystate"}}
|
||
<div class="card">
|
||
<div class="empty">
|
||
<div class="big">🗺️</div>
|
||
<div style="font-size:var(--fs-md);color:var(--text);margin-bottom:6px">还没有导入任何数据</div>
|
||
<div>登录后台,上传 rond 导出的 <span class="mono">.rondbackup</span> 备份文件即可生成足迹地图。</div>
|
||
<div style="margin-top:14px"><a class="btn primary" href="/admin">前往后台导入</a></div>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
{{define "pager"}}
|
||
{{if gt .PageCount 1}}
|
||
<div class="pager">
|
||
{{if gt .PageNo 1}}<a href="{{.Link .FilterBase "page" (printf "%d" (sub .PageNo 1))}}">上一页</a>{{end}}
|
||
<span class="cur">第 {{.PageNo}} / {{.PageCount}} 页</span>
|
||
{{if lt .PageNo .PageCount}}<a href="{{.Link .FilterBase "page" (printf "%d" (add .PageNo 1))}}">下一页</a>{{end}}
|
||
</div>
|
||
{{end}}
|
||
{{end}}
|
||
|
||
{{define "layout"}}<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
{{/* viewport-fit=cover 才会把刘海屏 / 底部 Home 指示条的安全区通过 env() 暴露出来,否则 CSS 里的 safe-area-inset 全是 0 */}}
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||
<title>{{.Title}} · {{.Settings.SiteTitle}}</title>
|
||
<script>window.__assetV="{{.AssetV}}"</script>
|
||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||
<link rel="manifest" href="/static/manifest.webmanifest">
|
||
<meta name="theme-color" content="{{.Settings.AccentColor}}">
|
||
<link rel="apple-touch-icon" href="/static/icons/icon-192.png">
|
||
{{if .HasMap}}
|
||
<link rel="stylesheet" href="/static/vendor/leaflet/leaflet.css?v={{.AssetV}}">
|
||
{{if or (eq .Nav "map") (eq .Nav "topics")}}
|
||
<link rel="stylesheet" href="/static/vendor/markercluster/MarkerCluster.css?v={{.AssetV}}">
|
||
<link rel="stylesheet" href="/static/vendor/markercluster/MarkerCluster.Default.css?v={{.AssetV}}">
|
||
{{end}}
|
||
{{end}}
|
||
{{if .Flatpickr}}
|
||
<link rel="stylesheet" href="/static/vendor/flatpickr/flatpickr.min.css?v={{.AssetV}}">
|
||
{{end}}
|
||
<link rel="stylesheet" href="/static/css/app.css?v={{.AssetV}}">
|
||
{{/* accent-base 是站长选的主色原值:暗色模式会用 color-mix 把它提亮,否则深色底上的链接会看不清(原来的深绿在深底上只有 3.3:1)。 */}}
|
||
<style>:root{--accent:{{.Settings.AccentColor}};--accent-soft:{{.Settings.AccentColor}}1a;--accent-base:{{.Settings.AccentColor}}}</style>
|
||
</head>
|
||
<body>
|
||
{{template "nav" .}}
|
||
{{template "content" .}}
|
||
{{if ne .Nav "map"}}
|
||
<footer class="ft">
|
||
{{.Settings.SiteTitle}}{{if .Settings.SiteSubtitle}} · {{.Settings.SiteSubtitle}}{{end}}
|
||
{{/* 到访被删光的数据集 last_visit_at 是 NULL,而 d 收的是 time.Time —— 少判这一层就会
|
||
nil 解引用、页脚之后整段不渲染(客户端还拿到 200)。 */}}
|
||
{{if .Dataset}}{{if .Dataset.Last}} · 数据截至 {{d .Dataset.Last}}{{end}}{{end}}
|
||
</footer>
|
||
{{end}}
|
||
{{if .HasMap}}
|
||
<script src="/static/vendor/leaflet/leaflet.js?v={{.AssetV}}"></script>
|
||
{{if or (eq .Nav "map") (eq .Nav "topics")}}<script src="/static/vendor/markercluster/leaflet.markercluster.js?v={{.AssetV}}"></script>{{end}}
|
||
<script src="/static/js/rond-map.js?v={{.AssetV}}"></script>
|
||
{{end}}
|
||
{{if .Flatpickr}}
|
||
<script src="/static/vendor/flatpickr/flatpickr.min.js?v={{.AssetV}}"></script>
|
||
<script src="/static/vendor/flatpickr/l10n/zh.js?v={{.AssetV}}"></script>
|
||
{{/* 时间框的解析器与输入引导。必须在 flatpickr 之后、页面内联脚本之前:
|
||
它不自己调 flatpickr,只提供 parseDate 与 attach,由各页 whenReady 调 attachPicker。 */}}
|
||
<script src="/static/js/datetime-input.js?v={{.AssetV}}"></script>
|
||
{{end}}
|
||
{{/* 输入弹层:替代 window.prompt(它会把提示文本与默认值写进地址栏)。
|
||
全站都可能用到(报告页复制链接、后台活动改名),所以无条件引入。 */}}
|
||
<script src="/static/js/prompt-box.js?v={{.AssetV}}"></script>
|
||
{{/* 第三方统计脚本:只在公开页面挂,后台/登录页不挂(免得把自己的操作也统计进去) */}}
|
||
{{if and .TrackScript (ne .Nav "admin")}}
|
||
{{.TrackScript}}
|
||
{{end}}
|
||
<script>
|
||
// 后台的表单提交 / 翻页 / 搜索都是整页刷新,浏览器会回到顶部,滚动位置就丢了。
|
||
// 这里在离开前把位置记进 sessionStorage,加载完再滚回去。
|
||
// 只在 /admin 下生效:前台页面(主页进详情之类)本来就该从头看。
|
||
(function () {
|
||
if (location.pathname.indexOf("/admin") !== 0) return;
|
||
var KEY = "rond:scroll:" + location.pathname;
|
||
function record() {
|
||
try {
|
||
sessionStorage.setItem(KEY, Math.round(window.scrollY || window.pageYOffset || 0) + ":" + Date.now());
|
||
} catch (e) {}
|
||
}
|
||
function restore() {
|
||
try {
|
||
var raw = sessionStorage.getItem(KEY);
|
||
if (!raw) return;
|
||
sessionStorage.removeItem(KEY);
|
||
var parts = raw.split(":"), y = parseInt(parts[0], 10), ts = parseInt(parts[1], 10);
|
||
// 10 秒内才认:确认框被取消时留下的记录不该在下次打开时把视图拽走
|
||
if (isFinite(y) && y > 0 && Date.now() - ts < 10000) window.scrollTo(0, y);
|
||
} catch (e) {}
|
||
}
|
||
// 等 load:解析阶段就滚的话文档可能还不够高,scrollTo 会被截断到 0
|
||
if (document.readyState === "complete") restore();
|
||
else window.addEventListener("load", restore);
|
||
document.addEventListener("submit", record, true);
|
||
// 翻页 / 重置这类是同路径的链接跳转,也要记(新开标签、锚点、跨页不记)
|
||
document.addEventListener("click", function (ev) {
|
||
var a = ev.target && ev.target.closest ? ev.target.closest("a") : null;
|
||
if (!a) return;
|
||
var href = a.getAttribute("href") || "";
|
||
if (!href || href.charAt(0) === "#" || a.target === "_blank") return;
|
||
var u;
|
||
try { u = new URL(a.href, location.href); } catch (e) { return; }
|
||
if (u.pathname === location.pathname) record();
|
||
}, true);
|
||
})();
|
||
</script>
|
||
|
||
{{/* 长列表(地点库 300+ 行、时间线 800+ 条)划到底后能一键回来。
|
||
地图页整页不滚动,所以这个按钮在那里不会出现。 */}}
|
||
{{/* 顶部细进度条:筛选是整页 GET,慢网下点了没反应会让人以为卡住 */}}
|
||
<div class="load-bar" id="load-bar" hidden></div>
|
||
<button class="to-top" id="to-top" type="button" aria-label="回到顶部" hidden>↑</button>
|
||
<script>
|
||
(function () {
|
||
/* ---------- 顶部加载进度条 ---------- */
|
||
var bar = document.getElementById('load-bar'), timer = null, safety = null, w = 0;
|
||
function start() {
|
||
bar.hidden = false;
|
||
w = 8;
|
||
bar.style.width = w + '%';
|
||
bar.style.opacity = '1';
|
||
clearInterval(timer);
|
||
timer = setInterval(function () {
|
||
w = Math.min(90, w + Math.max(0.4, (90 - w) / 14));
|
||
bar.style.width = w + '%';
|
||
}, 180);
|
||
// 兜底:万一这次「跳转」被阻止了(比如地图页是 AJAX 换数据),别让进度条一直挂着
|
||
clearTimeout(safety);
|
||
safety = setTimeout(finish, 8000);
|
||
}
|
||
function finish() {
|
||
clearInterval(timer);
|
||
clearTimeout(safety);
|
||
if (bar.hidden) return;
|
||
bar.style.width = '100%';
|
||
setTimeout(function () {
|
||
bar.style.opacity = '0';
|
||
setTimeout(function () { bar.hidden = true; bar.style.width = '0'; }, 260);
|
||
}, 140);
|
||
}
|
||
window.rondProgress = { start: start, finish: finish };
|
||
document.addEventListener('submit', start, true);
|
||
document.addEventListener('click', function (e) {
|
||
var a = e.target && e.target.closest ? e.target.closest('a') : null;
|
||
if (!a || a.target === '_blank' || a.hasAttribute('download')) return;
|
||
var u;
|
||
try { u = new URL(a.href, location.href); } catch (err) { return; }
|
||
// 只对「真的会跳走」的站内链接亮进度条
|
||
if (u.origin !== location.origin) return;
|
||
if (u.pathname === location.pathname && u.search === location.search) return;
|
||
start();
|
||
}, true);
|
||
window.addEventListener('pageshow', finish); // 从 bfcache 回来时也要收掉
|
||
|
||
/* ---------- 筛选:改动即生效 ---------- */
|
||
var form = document.getElementById('filters-form');
|
||
if (form) {
|
||
// 下拉与日期改了就提交,不用再点「查询」。
|
||
// 关键词不自动提交(每敲一个字都跳转会很难受)——回车即可。
|
||
function apply() {
|
||
// 用 requestSubmit 而不是 form.submit():前者会**触发 submit 事件**,
|
||
// 于是地图页自己那个「拦下提交、改走 AJAX」的处理器仍然生效;
|
||
// 用 submit() 会绕过它,把地图页的筛选变成整页刷新。
|
||
if (form.requestSubmit) form.requestSubmit();
|
||
else form.submit();
|
||
}
|
||
form.addEventListener('change', function (e) {
|
||
var el = e.target;
|
||
if (!el || !el.name) return;
|
||
if (el.tagName === 'SELECT' || el.type === 'date') apply();
|
||
});
|
||
}
|
||
|
||
var b = document.getElementById('to-top');
|
||
function sync() { b.hidden = window.scrollY < 600; }
|
||
window.addEventListener('scroll', sync, { passive: true });
|
||
sync();
|
||
b.addEventListener('click', function () {
|
||
// 尊重「减少动态效果」偏好:开了就别做平滑滚动
|
||
var smooth = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
window.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' });
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>{{end}}
|