上一批把显示区改成了「一组圆」,这一批让前台真正认多个圆。 - clip-path 从 circle() 换成 path():circle() 只表达得了一个圆。每个圆一段 圆弧子路径、**同绕向**(sweep=0),nonzero 填充规则下多段自然取并集, 重叠处不会互相挖洞。 - 拼路径的活单独放 fog-clip.js(rond-map.js 是个大 IIFE,函数取不出来), 这样能直接跑 node 单测;layout 里必须排在 rond-map.js 之前加载。 - 顺带清掉已经死掉的 fitCircle:多圆之后「圆心 + 半径」表达不了,留着就是 两套并行的贴合口径。fitParams 现在只给 fitBBox(单圆时外接矩形正是那个 圆的外接正方形,视野等价)。 - 守卫改名 TestTopicFogIsClippedToAreas,钉住两端(模板发 fogAreas ↔ 脚本认 fogAreas)与两处顺序(裁剪注册在 applyFit 之后、fog-clip.js 在 rond-map.js 之前);fogAreas 必须**无条件**下发(空数组 = 不限区域),不要包在条件里。 - verify.sh 挂进前端单测:以前只靠人记得手动跑 = 等于没有。找不到 node 直接 失败而不是跳过(跳过会让验收看着全绿、实际一行前端脚本都没验)。 验证(临时两圆专题,SQL 造的,已删): - fog-clip.test.js 15 条:把生成的 path 解析回圆弧再验几何,含绕向一致性与 半径 0/负数的夹取。 - 端到端像素验证:自校准出「CSS 容器坐标 → 图像像素」(scale≈2、offset≈19.2, 正是 site_cdp 的 --clip 外扩 10px×scale,别再手算);三张掩码图证明并集语义 —— A 独有点只有 A 亮、B 独有点只有 B 亮、**重叠中心两圆都亮且并集也亮 (无洞)**,整体集合差异 0.013%。 - 迷雾掩码:86,776 个像素,按两圆并集的解析式判 **圆外 0 个**;两个圆内分别 有 52,609 / 46,757 个迷雾像素 ⇒ 第二个圆真的被认了,不是只看第一个。
396 lines
20 KiB
HTML
396 lines
20 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}}
|
||
{{/* 日期选择器的样式。**无条件加载**:筛选区(公共页)也有日期框,按页开关会被漏掉
|
||
(见下面 datetime-input 那段的长注释)。 */}}
|
||
<link rel="stylesheet" href="/static/vendor/flatpickr/flatpickr.min.css?v={{.AssetV}}">
|
||
<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}}
|
||
{{/* fog-clip.js 必须在 rond-map.js **之前**:专题的迷雾裁剪要拿它拼多圆的 clip-path: path() */}}
|
||
<script src="/static/js/fog-clip.js?v={{.AssetV}}"></script>
|
||
<script src="/static/js/rond-map.js?v={{.AssetV}}"></script>
|
||
{{end}}
|
||
{{/* 日期选择器 + 手输引导。**无条件加载**(js+css+l10n 约 18KB gzip),两个理由:
|
||
① 筛选区在公共页上也有日期框(/stats /places /timeline 的起止日期、/map 自己的),
|
||
「按页开开关」这种写法已经被证明会漏 —— 2026-10-09 的日期框 bug 就是只给
|
||
type=datetime-local 挂了控件,后来加的 type=date 没人管;
|
||
② 原生控件本身不可靠:Chrome 的年段能吞 6 位数字,敲 20260606 会变成 202606/06
|
||
且 el.value 为空(筛选条件静默丢掉)。
|
||
datetime-input.js 必须在 flatpickr 之后、页面内联脚本之前。 */}}
|
||
<script src="/static/vendor/flatpickr/flatpickr.min.js?v={{.AssetV}}"></script>
|
||
<script src="/static/vendor/flatpickr/l10n/zh.js?v={{.AssetV}}"></script>
|
||
<script src="/static/js/datetime-input.js?v={{.AssetV}}"></script>
|
||
<script>
|
||
/* 每一页都把日期输入改造一次(见 datetime-input.js 顶部说明)。
|
||
页面自己的 whenReady 里还会再调一次 attachAll —— 那是为了覆盖**动态创建**的输入
|
||
(照片页是分批建行的);重复调用安全,元素上有 _rondAttached 守卫。 */
|
||
(function () {
|
||
var run = function () { if (window.rondDatetime) window.rondDatetime.attachAll(); };
|
||
/* 这个脚本在 body 末尾,内容都已经解析出来了 —— 立刻改一次,别等 DOMContentLoaded:
|
||
否则用户会先看到一个原生日期框、过一会儿才变成文本框(本机实测 DCL 在 1.18s,
|
||
网络慢时更明显)。DCL 再补一次是为了兜住别的脚本往页面里塞的输入。 */
|
||
run();
|
||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", run);
|
||
})();
|
||
</script>
|
||
{{/* 输入弹层:替代 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}}
|