前端改造:设计令牌 + 卡片层级 + 图表可读性 + 类型色收口 + 窄屏导航

先出方案(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 拆又容易做出「中间那笔编译不过」的历史,所以合成一笔、在消息里分段。
This commit is contained in:
tamakyi
2026-10-09 17:01:08 +08:00
parent d4e4257a23
commit ab7ccd22ce
32 changed files with 997 additions and 283 deletions
+52
View File
@@ -0,0 +1,52 @@
package web
import (
"bytes"
"html/template"
"testing"
)
// TestCSSColorSurvivesTemplateSanitizer 钉住两个方向:
//
// 1. `var(--cat-*)` 必须原样渲染进 style。html/template 的 CSS 上下文过滤器**不认识 var()**,
// 函数返回普通 string 会被替换成 `ZgotmplZ` —— 页面上所有类型色圆点会当场变成透明,
// 而且不报错、模板测试也照过(实测踩过)。
// 2. 一旦返回 template.CSS 就等于关掉这道过滤,所以**数据侧来的色值必须先严格校验**:
// 只有「# + 6 位十六进制」才放行,否则 `#"><img …` 能直接逃出 style 属性。
func TestCSSColorSurvivesTemplateSanitizer(t *testing.T) {
tpl := template.Must(template.New("t").Funcs(templateFuncs()).
Parse(`<i style="background:{{csscolor .C}}"></i><b style="background:{{csssoft .C}}"></b>`))
render := func(v string) string {
t.Helper()
var buf bytes.Buffer
if err := tpl.Execute(&buf, map[string]string{"C": v}); err != nil {
t.Fatalf("渲染失败(色值 %q): %v", v, err)
}
return buf.String()
}
cases := []struct {
name string
in string
want string
}{
{"调色板色名走 CSS 变量", "teal",
`<i style="background:var(--cat-teal)"></i><b style="background:var(--cat-teal-soft)"></b>`},
{"自定义色值原样用(浅底拼 22 当 alpha)", "#a2845e",
`<i style="background:#a2845e"></i><b style="background:#a2845e22"></b>`},
{"空值退兜底", "",
`<i style="background:var(--cat-none)"></i><b style="background:var(--cat-none-soft)"></b>`},
{"想逃出属性的串退兜底", `#"><img src=x>`,
`<i style="background:var(--cat-none)"></i><b style="background:var(--cat-none-soft)"></b>`},
{"位数不对的 hex 退兜底", "#12345",
`<i style="background:var(--cat-none)"></i><b style="background:var(--cat-none-soft)"></b>`},
{"套了别的 CSS 声明也退兜底", "red;background:url(javascript:1)",
`<i style="background:var(--cat-none)"></i><b style="background:var(--cat-none-soft)"></b>`},
}
for _, c := range cases {
if got := render(c.in); got != c.want {
t.Errorf("%s:csscolor/csssoft(%q)\n 得到 %s\n 期望 %s", c.name, c.in, got, c.want)
}
}
}
+67 -9
View File
@@ -1,7 +1,9 @@
package web
import (
"io/fs"
"os"
"regexp"
"strings"
"testing"
)
@@ -152,19 +154,75 @@ func TestPlaceImageControlsAreLabelled(t *testing.T) {
}
}
// ---------- F14:窄屏导航链接的内边距别再写死一处(会被后面的触控块盖掉) ----------
func TestNavLinksNarrowRuleHasNoDeadPadding(t *testing.T) {
// ---------- 窄屏导航:链接行收进菜单(原来是横向滚动,最右边那个字会被切掉) ----------
//
// 620px 以下链接行一定放不下(实测:390 溢 63px、320 溢 133px,「统计」被切成半个字紧贴
// 后面的按钮,而渐隐提示实际上看不出来)。现在整行收进 .nav-menu 这个原生 <details>。
//
// 这条守卫钉的是**成对关系**:CSS 的两半(桌面藏菜单 / 窄屏藏链接行)与模板里的两个类
// 缺任何一个,窄屏就会退回老样子、或者宽屏凭空多出一个菜单按钮。
func TestNarrowNavCollapsesToMenu(t *testing.T) {
css, err := os.ReadFile(pkgFile(t, "static/css/app.css"))
if err != nil {
t.Fatalf("读取 app.css 失败: %v", err)
}
src := string(css)
for _, want := range []string{
".nav-menu { display: none; }", // 桌面:菜单按钮不出现
".nav-links { display: none; }", // 窄屏:链接行不出现
".nav-menu { display: block; flex: 1; }", // 窄屏:菜单按钮取而代之
} {
if !strings.Contains(src, want) {
t.Errorf("app.css 缺少 %q —— 「桌面藏菜单 / 窄屏藏链接行」这两半必须成对,"+
"否则窄屏会退回「横向滚动 + 最右边的字被切掉」", want)
}
}
b, err := tmplFS.ReadFile("templates/layout.html")
if err != nil {
t.Fatalf("读取 layout.html 失败: %v", err)
}
nav := string(b)
if !strings.Contains(nav, `class="nav-links"`) || !strings.Contains(nav, `class="nav-menu"`) {
t.Error("layout.html 里 .nav-links 与 .nav-menu 必须都在:CSS 的两半分别作用在这两个元素上")
}
if n := strings.Count(nav, `{{template "navitems" .}}`); n != 2 {
t.Errorf("导航链接应当渲染两遍(宽屏平铺 + 窄屏菜单),实际 %d 处", n)
}
}
// ---------- 字阶纪律:app.css 与模板内联样式都只走令牌 ----------
//
// 改前 app.css 里散着 10 / 10.5 / 11 / 11.5 / 12 / 12.5 / 12.8 / 13 / 13.5 / 14 / 14.5 / 15 /
// 17 / 18 / 21px 十几档字号,光 12~15px 之间就挤了 4 档 —— 层级挤不出来,只能靠加粗硬顶;
// 模板里另有 54 处内联字号。现在:文本走 --fs-*,表单控件走 --fs-control(窄屏必须 ≥16px,
// 否则 iOS 聚焦会放大整页),字形走 --glyph-*(22px 的 ✕ 塞进字阶只会把按钮撑坏)。
func TestNoBareFontSizePx(t *testing.T) {
re := regexp.MustCompile(`font-size:\s*[^;\n]*[0-9]+(\.[0-9]+)?px`)
check := func(name string, b []byte) {
for _, m := range re.FindAllString(string(b), -1) {
t.Errorf("%s 里还有裸字号 %q —— 一律走 --fs-* / --fs-control / --glyph-* 令牌", name, m)
}
}
b, err := os.ReadFile(pkgFile(t, "static/css/app.css"))
if err != nil {
t.Fatalf("读取 app.css 失败: %v", err)
}
src := string(b)
if !strings.Contains(src, ".nav-links a { font-size: 13px; }") {
t.Fatal("找不到窄屏的 `.nav-links a { font-size: 13px; }`(写法变了,这条守卫失效)")
}
if strings.Contains(src, ".nav-links a { padding:") {
t.Error("窄屏 .nav-links a 又写了 padding:同特异性的内边距由后面「窄屏触控」那块给," +
"写在这里会被盖掉(死代码)")
check("app.css", b)
for _, dir := range []string{"templates/*.html", "templates/admin/*.html"} {
names, err := fs.Glob(tmplFS, dir)
if err != nil {
t.Fatalf("列举 %s 失败: %v", dir, err)
}
for _, name := range names {
body, err := tmplFS.ReadFile(name)
if err != nil {
t.Fatalf("读取 %s 失败: %v", name, err)
}
check(name, body)
}
}
}
+1
View File
@@ -369,6 +369,7 @@ func (s *Server) home(w http.ResponseWriter, r *http.Request) {
d.MaxCity = maxOf(d.CityStats, func(c stats.CityStat) int { return c.Count })
d.MaxTransport = maxOf(d.Transports, func(t stats.TransportStat) int { return t.Count })
d.MaxTag = maxOf(d.Tags, func(t stats.TagStat) int { return t.Count })
d.MaxMonthly = maxOf(d.Monthly, func(m MonthBar) int { return m.Count })
s.render(w, "home", d)
}
+332 -165
View File
@@ -19,11 +19,70 @@
--row-hover: #fbfcfc;
--bg-2: #fafbfb; /* 比卡片略深一层的面(行内编辑表单、空态框) */
--on-accent: #ffffff; /* 主色底上的文字 */
/* ---------- 类型色(活动 / 交通方式)----------
色名是 rond 里的 Apple 日历色名(与 Go 侧 appleColors 的键一一对应,模板经
csscolor / csscolorsoft 取用)。地图那份调色板在 rond-map.js 里、**故意独立**:
地图瓦片在暗色下保持浅色,标记得用浅色底上那套值,不能跟着页面的暗色变量走。
⚠️ 这套色的**明度差异很大**:在白底上 yellow 1.87、mint 2.12、orange 2.20、green 2.22、
cyan 2.54、teal 2.57 —— 都远低于图形对象 3:1。所以它们只能当「身份标记」(圆点、柱条),
**别拿去当文字色或承载唯一信息的图形**,这也正是全站只用小圆点而不是彩色标签的原因。 */
--cat-red: #ff3b30; --cat-orange: #ff9500; --cat-yellow: #e8b700;
--cat-green: #34c759; --cat-mint: #00c7be; --cat-teal: #30b0c7;
--cat-cyan: #32ade6; --cat-blue: #007aff; --cat-indigo: #5856d6;
--cat-purple: #af52de; --cat-pink: #ff2d55; --cat-brown: #a2845e;
--cat-gray: #8e8e93;
/* 色名不认识时的兜底,与 Go 里 cssColor 的 var(--cat-none) 对齐 */
--cat-none: #5b6169;
/* 浅底(emoji 方块那种):半透明叠在卡片上,浅色暗色自适应。
原来是模板里给 hex 拼 "22" 当 alpha——换成 3 位色值或 var() 就散了,这里收口。 */
--cat-red-soft: rgba(255, 59, 48, .14); --cat-orange-soft: rgba(255, 149, 0, .14);
--cat-yellow-soft: rgba(232, 183, 0, .14); --cat-green-soft: rgba(52, 199, 89, .14);
--cat-mint-soft: rgba(0, 199, 190, .14); --cat-teal-soft: rgba(48, 176, 199, .14);
--cat-cyan-soft: rgba(50, 173, 230, .14); --cat-blue-soft: rgba(0, 122, 255, .14);
--cat-indigo-soft: rgba(88, 86, 214, .14); --cat-purple-soft: rgba(175, 82, 222, .14);
--cat-pink-soft: rgba(255, 45, 85, .14); --cat-brown-soft: rgba(162, 132, 94, .16);
--cat-gray-soft: rgba(142, 142, 147, .16);
--cat-none-soft: rgba(91, 97, 105, .14);
--ok-bg: #eef8f5; --ok-fg: #17604f; --ok-line: #cfe6e0;
--err-bg: #fdf3f2; --err-fg: #b42318; --err-line: #f3dcda;
--info-bg: #f2f6fd; --info-fg: #2c5282; --info-line: #dbe5f5;
--radius: 10px;
--shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 1px 3px rgba(16, 24, 40, .06);
/* 圆角 4 档。原来是 7 / 8 / 9 / 10 一堆近似值混着用,肉眼分不出、改一处要改一片。 */
--r-xs: 3px; /* 小方块(热力格、气温柱顶) */
--r-sm: 6px; /* 控件(按钮、输入框、导航链接、标签) */
--r-md: 8px; /* 中号面(缩略图、提示条) */
--r-lg: 12px; /* 卡片与浮层 */
/* 阴影 3 档,暗色模式整套覆盖(见文件末尾的 prefers-color-scheme) */
--sh-1: 0 1px 2px rgba(16, 24, 40, .04), 0 1px 3px rgba(16, 24, 40, .06);
--sh-2: 0 4px 14px rgba(16, 24, 40, .10), 0 1px 3px rgba(16, 24, 40, .06);
--sh-3: 0 10px 30px rgba(16, 24, 40, .12), 0 2px 6px rgba(16, 24, 40, .06);
/* 间距 5 档。本批只用在卡片内边距这类「主节奏」上,其余逐批替换。 */
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 24px;
/* 卡片内边距单列一个变量:折叠卡片的标题行要用负 margin 顶到边(见 .filters-card),
两处必须永远相等,所以绑成同一个值。 */
--card-pad: 18px;
/* ---------- 字阶 ----------
文本只有 6 档。原来是 10/10.5/11/11.5/12/12.5/12.8/13/13.5/14/14.5/15/17/18/21px
十几档挤在一起,12~15px 之间就塞了 4 档 —— 层级挤不出来,只能靠加粗硬顶。 */
--fs-xs: 11px; /* 脚注 / 单位 / 轴标 / 图例 */
--fs-sm: 13px; /* 正文(默认) */
--fs-md: 15px; /* 卡片标题 / 强调文本 */
--fs-lg: 21px; /* 页面标题 */
--fs-xl: 26px; /* KPI 数值 */
--fs-2xl: 34px; /* 首屏主数 */
/* 表单控件字号单独一档:窄屏必须 ≥16px,否则 iOS 一聚焦就把整页放大。
它不属于文本层级,所以不并进上面的字阶。 */
--fs-control: 16px;
/* 字形尺寸(✕ / 箭头 / 空态图标 / 表情):按控件尺寸定,不是文字层级。
故意不进字阶 —— 把 22px 的 ✕ 塞进 26px 的字阶只会把按钮撑坏。 */
--glyph-xs: 12px;
--glyph-sm: 15px;
--glyph-md: 18px;
--glyph-lg: 22px;
--glyph-xl: 30px;
--glyph-2xl: 40px;
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
/* 顶栏高度:窄屏更矮。地图页的定位跟它对齐,避免出现几像素的缝。 */
--nav-h: 54px;
@@ -33,6 +92,10 @@
分两档是为了保住原有次序:加载状态条要压在所有浮层之上。 */
--z-map-overlay: 1100;
--z-map-status: 1150;
/* 顶栏。必须压过地图页的浮层(--z-map-overlay = 1100):窄屏的导航菜单是从顶栏
展开的,而它属于 .nav 这个层叠上下文、自己抬不动 —— 只有把整条顶栏放到浮层之上,
展开后才不会被地图的筛选面板盖住(实测过 elementFromPoint)。 */
--z-nav: 1200;
/* 刘海屏 / 底部 Home 指示条的安全区。不支持 env() 的浏览器回落到 0。 */
--safe-t: env(safe-area-inset-top, 0px);
--safe-r: env(safe-area-inset-right, 0px);
@@ -47,17 +110,28 @@ html, body {
padding: 0;
background: var(--bg);
color: var(--text);
font: 14px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif;
font-size: var(--fs-sm);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -.01em; }
/* 显式给字号。这条规则以前只管字重和 margin,字号靠浏览器默认的 2em / 1.5em / 1.17em
—— 也就是跟着**根字号**漂:根从 14px 降到 13px 后,没被 `.card > h2` 命中的那些 h2
从 21px 悄悄变成 19.5px(/stats 的「我的结论」「访客点赞」两块),比同页卡片标题大一圈。 */
h1 { font-size: var(--fs-lg); }
h2 { font-size: var(--fs-md); }
h3, h4 { font-size: var(--fs-sm); }
/* <b>/<strong> 的浏览器默认是 bold(700),而全站层级只到 600 —— 不钉住的话
行内加粗会比标题还重。 */
b, strong { font-weight: 600; }
/* ---------- 顶栏 ---------- */
.nav {
position: sticky; top: 0; z-index: 500;
position: sticky; top: 0; z-index: var(--z-nav);
background: var(--glass);
backdrop-filter: saturate(1.6) blur(10px);
border-bottom: 1px solid var(--line);
@@ -66,41 +140,108 @@ h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -.01em; }
}
.nav-inner {
max-width: 1120px; margin: 0 auto; padding: 0 20px;
/* 菜单是相对 .nav-inner 绝对定位的(要铺满整条),所以这里得有定位上下文 */
position: relative;
height: var(--nav-h); display: flex; align-items: center; gap: 22px;
}
.brand { display: flex; align-items: center; gap: 8px; font-weight: 600; color: var(--text); font-size: 15px; }
.brand { display: flex; align-items: center; gap: 8px; font-weight: 600; color: var(--text); font-size: var(--fs-md); }
.brand:hover { text-decoration: none; }
.brand .avatar { font-size: 18px; }
.nav-links { display: flex; gap: 4px; flex: 1; }
.brand .avatar { font-size: var(--glyph-md); }
/* 链接行自带横向兜底:620px 以下整行会收进 .nav-menu,这条就只为「以后又加了入口、
宽屏也放不下」兜底 —— 宁可让它能滑,也别把最右边那个字切掉一半。 */
.nav-links { display: flex; gap: 4px; flex: 1; overflow-x: auto; scrollbar-width: none; }
.nav-links::-webkit-scrollbar { display: none; }
/* 还能往右滑时右缘渐隐(由 nav 的脚本按 scrollLeft 加/去这个类)。用 mask 而不是盖一层
渐变伪元素:伪元素会把链接文字一起遮住,mask 是让内容本身渐隐。 */
.nav-links.overflow-right {
-webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
}
.nav-links a {
padding: 5px 11px; border-radius: 7px; color: var(--text-2); font-size: 13.5px; white-space: nowrap;
padding: 5px 11px; border-radius: var(--r-sm); color: var(--text-2); font-size: var(--fs-sm); white-space: nowrap;
}
.nav-links a:hover { background: var(--line-soft); color: var(--text); text-decoration: none; }
.nav-links a.on { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
.nav-right { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--text-3); }
.nav-right { display: flex; align-items: center; gap: 10px; font-size: var(--fs-sm); color: var(--text-3); }
/* 窄屏导航菜单。620px 以下链接行一定放不下(390 溢 63px、320 溢 133px),整行收进这里。
用原生 <details>,不靠 JS 也能开合;JS 只补三件原生不给的事:点外面 / Esc / 点链接后收起。 */
.nav-menu { display: none; }
.nav-menu > summary {
list-style: none; cursor: pointer; user-select: none; white-space: nowrap;
display: inline-flex; align-items: center; gap: 4px;
min-height: 44px; padding: 0 10px; border-radius: var(--r-sm);
color: var(--text-2); font-size: var(--fs-sm);
}
.nav-menu > summary::-webkit-details-marker { display: none; }
.nav-menu > summary::after { content: "▾"; font-size: var(--glyph-xs); color: var(--text-3); }
.nav-menu[open] > summary { background: var(--accent-soft); color: var(--accent); }
.nav-menu[open] > summary::after { content: "▴"; }
.nav-menu-body {
position: absolute; left: 0; right: 0; top: 100%; z-index: 1;
display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2px; padding: 6px;
background: var(--panel); border-bottom: 1px solid var(--line); box-shadow: var(--sh-2);
}
.nav-menu-body a {
display: flex; align-items: center; min-height: 44px; padding: 0 12px;
border-radius: var(--r-sm); color: var(--text-2); font-size: var(--fs-sm);
}
.nav-menu-body a:hover { background: var(--line-soft); color: var(--text); text-decoration: none; }
.nav-menu-body a.on { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
/* ---------- 布局 ---------- */
.wrap { max-width: 1120px; margin: 0 auto;
padding: 22px calc(20px + var(--safe-r)) calc(60px + var(--safe-b)) calc(20px + var(--safe-l)); }
.page-head { margin-bottom: 18px; display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.page-head h1 { font-size: 21px; }
.page-head .sub { color: var(--text-3); font-size: 13px; margin-top: 3px; }
.page-head h1 { font-size: var(--fs-lg); }
.page-head .sub { color: var(--text-3); font-size: var(--fs-sm); margin-top: 3px; }
.card {
background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
box-shadow: var(--shadow); padding: 18px; margin-bottom: 16px;
background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
box-shadow: var(--sh-1); padding: var(--card-pad); margin-bottom: var(--sp-4);
}
.card > h2 { font-size: 15px; margin-bottom: 2px; display: flex; align-items: center; gap: 8px; }
.card > h2 { font-size: var(--fs-md); margin-bottom: 2px; display: flex; align-items: center; gap: var(--sp-2); }
/* .card-body 是数据编辑页折叠用的容器,卡片内容被它包住后不再是 .card 的直接子元素,
所以下面两条用「后代」而不是「子」选择器——否则折叠脚本一加上,间距就全没了。 */
.card .card-sub { color: var(--text-3); font-size: 12.5px; margin-bottom: 14px; }
.card .card-sub { color: var(--text-3); font-size: var(--fs-sm); margin-bottom: var(--sp-3); }
.card.collapsed .card-body { display: none; }
/* 卡片三档。原来全站只有一种白盒(量下来 17 个盒子只有 2 种样式),KPI / 地图 / 图表 /
表格 / 筛选器全等重,扫视时没有落点。分档不是装饰,是为了「一眼知道先看哪」:
hero 只给首屏主数区与地图,quiet 只给纯说明性的内容。 */
.card--hero { border-color: transparent; box-shadow: var(--sh-3); padding: calc(var(--card-pad) + var(--sp-2)); }
/* quiet 用页面底色 + 细边 + 无阴影 = 贴在页面上的扁平面板。
注意别拿 --bg-2 (#fafbfb) 当底色:它和页面底 #f6f7f8 只差 4/255,
实测根本看不出边界,卡片会像「破了个洞」而不是「轻了一档」。 */
.card--quiet { background: var(--bg); border-color: var(--line); box-shadow: none; }
.card--quiet > h2 { font-size: var(--fs-sm); color: var(--text-2); }
/* 首屏主数:把最重要的那个数从 KPI 格里拎出来单独一行。
原来 8 个数字一样大一样重,扫过去不知道看哪个 —— 层级靠「只让一个特别大」,
而不是把所有的都调大。 */
.kpi-lead {
position: relative; overflow: hidden;
background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
box-shadow: var(--sh-3);
padding: calc(var(--card-pad) + var(--sp-1)) var(--card-pad) var(--card-pad);
margin-bottom: var(--sp-3);
}
.kpi-lead::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--accent);
}
.kpi-lead .kl-v {
font-size: clamp(var(--fs-xl), 7.2vw, var(--fs-2xl)); font-weight: 600;
letter-spacing: -.02em; line-height: 1.1; font-variant-numeric: tabular-nums;
}
.kpi-lead .kl-v small { font-size: var(--fs-md); font-weight: 500; color: var(--text-3); margin-left: var(--sp-1); }
.kpi-lead .kl-k { color: var(--text-2); font-size: var(--fs-sm); margin-top: var(--sp-1); }
.kpi-lead .kl-x { color: var(--text-3); font-size: var(--fs-sm); margin-top: var(--sp-2); }
/* 只有带 data-card 的卡片(数据编辑页)能折叠:标题整行可点,右侧一个箭头 */
.card[data-card] > h2 { cursor: pointer; }
.card[data-card] > h2 .card-toggle {
margin-left: auto; flex: 0 0 auto; cursor: pointer;
background: none; border: 0; padding: 0 2px; color: var(--text-3);
font: inherit; font-size: 12px; line-height: 1;
font: inherit; font-size: var(--glyph-xs); line-height: 1;
}
.card[data-card] > h2 .card-toggle:hover { color: var(--text); }
.grid { display: grid; gap: 16px; }
@@ -118,21 +259,13 @@ h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -.01em; }
.g2, .g3, .g4 { grid-template-columns: 1fr; }
.wrap { padding: 16px calc(14px + var(--safe-r)) calc(48px + var(--safe-b)) calc(14px + var(--safe-l)); }
.nav-inner { padding: 0 10px; gap: 6px; }
/* 窄屏只留头像,导航可横向滚动,避免换行把布局撑坏 */
/* 窄屏只留头像 */
.brand .t { display: none; }
.nav-links { overflow-x: auto; scrollbar-width: none; gap: 0; }
.nav-links::-webkit-scrollbar { display: none; }
/* 还能往右滑的时候右缘渐隐一下(由 nav 后面那几行脚本按 scrollLeft 加/去这个类)。
用 mask 而不是盖一层渐变伪元素:伪元素会把链接文字一起遮住,mask 是让内容本身渐隐。
不支持 mask 的浏览器只是少个提示,滑动照旧。 */
.nav-links.overflow-right {
-webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
}
/* 内边距由下面「窄屏触控」那块给(同特异性、写在后面才生效);这里只留字号。 */
.nav-links a { font-size: 13px; }
/* 链接行整行收进 .nav-menu(样式见上面那段),这里不再横向滚动 */
.nav-links { display: none; }
.nav-menu { display: block; flex: 1; }
.nav-right .who { display: none; }
.nav-right .btn.sm { padding: 4px 9px; font-size: 12.5px; }
.nav-right .btn.sm { padding: 4px 9px; font-size: var(--fs-sm); }
}
/* ---------- 窄屏触控与表单(移动端适配) ---------- */
@@ -164,26 +297,26 @@ h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -.01em; }
/* 用 flex + grow 而不是固定列数的网格:KPI 个数常常是 6、7 这种除不尽列数的,
网格会在最后一行留下空格子——而间隙是用底色画的,空格子就露成灰块,很难看。
flex 会让最后一行自己撑满,任何个数都不会有空格。 */
.kpis { display: flex; flex-wrap: wrap; gap: 1px; background: var(--line); border-radius: var(--radius); overflow: hidden; border: 1px solid var(--line); }
.kpi { flex: 1 1 158px; min-width: 0; background: var(--panel); padding: 15px 16px; }
.kpis { display: flex; flex-wrap: wrap; gap: 1px; background: var(--line); border-radius: var(--r-lg); overflow: hidden; border: 1px solid var(--line); }
.kpi { flex: 1 1 158px; min-width: 0; background: var(--panel); padding: var(--sp-4); }
/* 字号跟着视口收:窄屏上「112天19小时」这种值放不下就会折成两行,把整行撑高 */
.kpi .v { font-size: clamp(17px, 5.6vw, 23px); font-weight: 650; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.kpi .v small { font-size: 13px; font-weight: 500; color: var(--text-3); margin-left: 3px; }
.kpi .k { color: var(--text-3); font-size: 12.5px; margin-top: 1px; }
.kpi .v { font-size: clamp(var(--fs-md), 5.6vw, var(--fs-xl)); font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.kpi .v small { font-size: var(--fs-sm); font-weight: 500; color: var(--text-3); margin-left: 3px; }
.kpi .k { color: var(--text-3); font-size: var(--fs-sm); margin-top: 1px; }
/* ---------- 图表(纯 CSS/SVG) ---------- */
.bars { display: flex; flex-direction: column; gap: 9px; }
.bar-row { display: grid; grid-template-columns: 128px 1fr 104px; align-items: center; gap: 10px; font-size: 13px; }
.bar-row { display: grid; grid-template-columns: 128px 1fr 104px; align-items: center; gap: 10px; font-size: var(--fs-sm); }
/* 整行可点(下钻为筛选):给一点悬停反馈,并让点击区域覆盖整行;
柱条本身只有 9px 高,靠「行」来接收点击才够手指点 */
a.bar-row { margin: 0 -6px; padding: 4px 6px; border-radius: 6px; color: inherit; text-decoration: none; }
a.bar-row { margin: 0 -6px; padding: 4px 6px; border-radius: var(--r-sm); color: inherit; text-decoration: none; }
a.bar-row:hover { background: var(--line-soft); text-decoration: none; }
a.col { display: block; text-decoration: none; }
a.col:hover { opacity: 1; filter: brightness(1.12); }
.bar-row .nm { color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar-track { background: var(--line-soft); border-radius: 5px; height: 9px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 5px; background: var(--accent); min-width: 2px; }
.bar-row .vv { text-align: right; color: var(--text-2); font-variant-numeric: tabular-nums; font-size: 12.5px; white-space: nowrap; }
.bar-track { background: var(--line-soft); border-radius: var(--r-sm); height: 9px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: var(--r-sm); background: var(--accent); min-width: 2px; }
.bar-row .vv { text-align: right; color: var(--text-2); font-variant-numeric: tabular-nums; font-size: var(--fs-sm); white-space: nowrap; }
@media (max-width: 620px) {
.bar-row { grid-template-columns: 84px 1fr 92px; }
/* 图表行在手机上要能点中:整行 ~37px。再往上加(44px)会把「类型分布」这种
@@ -191,35 +324,54 @@ a.col:hover { opacity: 1; filter: brightness(1.12); }
a.bar-row { padding: 8px 6px; }
}
.cols { display: flex; align-items: flex-end; gap: 3px; height: 120px; }
.cols .col { flex: 1; background: var(--accent); border-radius: 3px 3px 0 0; min-height: 2px; opacity: .85; }
/* 基线:柱子底下压一道浅线,让「高度从哪儿量起」有个参照(原来柱子直接悬在空白里,
高低只能靠互相比)。
padding-top 是给峰值柱顶上那个数字留的位置:height 走 border-box,内容区仍是 120px。 */
.cols { position: relative; display: flex; align-items: flex-end; gap: 3px; height: 136px; padding-top: 16px; border-bottom: 1px solid var(--line); }
.cols .col { position: relative; flex: 1; background: var(--accent); border-radius: var(--r-xs) var(--r-xs) 0 0; min-height: 2px; opacity: .8; }
.cols .col:hover { opacity: 1; }
.cols-x { display: flex; gap: 3px; font-size: 10px; color: var(--text-3); margin-top: 5px; }
/* 峰值柱:不透明拉满,并在顶上标出次数。数值本来写在副标题里,但它不告诉你「哪一根最高」,
标在柱子上才是能一眼找到的锚点。 */
.cols .col.peak { opacity: 1; }
.cols .col .pv {
position: absolute; left: 50%; bottom: calc(100% + 2px); transform: translateX(-50%);
font-size: var(--fs-xs); line-height: 1.2; color: var(--text-2);
font-variant-numeric: tabular-nums; white-space: nowrap;
}
.cols-x { display: flex; gap: 3px; font-size: var(--fs-xs); color: var(--text-3); margin-top: 5px; }
.cols-x span { flex: 1; text-align: center; overflow: hidden; white-space: nowrap; }
/* 一天 24 根柱,光看柱子分不出「上午 / 下午」。这条标尺用同一套 24 列网格(不是 flex:6),
所以每段边界和柱子严格对齐;每段直接写上时段和小时区间,就不用再单独留一行坐标轴。 */
.cols-band { display: grid; grid-template-columns: repeat(24, minmax(0, 1fr)); gap: 3px; margin-top: var(--sp-2); }
.cols-band span {
grid-column: span 6; text-align: center; white-space: nowrap;
font-size: var(--fs-xs); color: var(--text-3);
background: var(--line-soft); border-radius: var(--r-xs); padding: 2px 0;
}
/* ---------- 逐月气温 ---------- */
.temp-chart { margin-top: 16px; }
.temp-head { font-size: 12.5px; color: var(--text-3); margin-bottom: 8px; }
.temp-head { font-size: var(--fs-sm); color: var(--text-3); margin-bottom: 8px; }
/* 横向可滚动:柱子固定宽,十几二十个月挤在窄屏上也不会把标签压成一团 */
.temp-strip { display: flex; gap: 4px; overflow-x: auto; padding-bottom: 4px; }
.temp-col { flex: 0 0 36px; display: flex; flex-direction: column; align-items: center; gap: 3px; cursor: default; }
.temp-col .tc-v { font-size: 10.5px; color: var(--text-2); font-variant-numeric: tabular-nums; }
.temp-col .tc-v { font-size: var(--fs-xs); color: var(--text-2); font-variant-numeric: tabular-nums; }
.temp-col .tc-track { width: 100%; height: 84px; display: flex; align-items: flex-end; }
.temp-col .tc-bar { width: 100%; border-radius: 4px 4px 0 0; min-height: 3px; }
.temp-col .tc-m { font-size: 10.5px; color: var(--text-3); white-space: nowrap; }
.temp-col .tc-bar { width: 100%; border-radius: var(--r-xs) var(--r-xs) 0 0; min-height: 3px; }
.temp-col .tc-m { font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; }
.temp-col:hover .tc-bar { filter: brightness(1.08); }
.temp-col:hover .tc-v { color: var(--text); }
/* ---------- 日历热力 ---------- */
.heat { display: flex; gap: 3px; overflow-x: auto; padding-bottom: 4px; }
.heat .wk { display: flex; flex-direction: column; gap: 3px; }
.heat .d { width: 11px; height: 11px; border-radius: 2.5px; background: var(--line-soft); }
.heat .d { width: 11px; height: 11px; border-radius: var(--r-xs); background: var(--line-soft); }
.heat .d.l1 { background: #cfe6e0; }
.heat .d.l2 { background: #9dcfc3; }
.heat .d.l3 { background: #5fae9c; }
.heat .d.l4 { background: var(--accent); }
.heat-legend { display: flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--text-3); margin-top: 8px; }
.heat-legend .d { width: 11px; height: 11px; border-radius: 2.5px; }
.heat-legend { display: flex; align-items: center; gap: 5px; font-size: var(--fs-xs); color: var(--text-3); margin-top: 8px; }
.heat-legend .d { width: 11px; height: 11px; border-radius: var(--r-xs); }
/* ---------- 列表 ---------- */
.list { display: flex; flex-direction: column; }
@@ -229,33 +381,33 @@ a.col:hover { opacity: 1; filter: brightness(1.12); }
}
.item:last-child { border-bottom: 0; }
.item .ic {
width: 34px; height: 34px; border-radius: 9px; display: flex; align-items: center; justify-content: center;
background: var(--line-soft); font-size: 17px;
width: 34px; height: 34px; border-radius: var(--r-md); display: flex; align-items: center; justify-content: center;
background: var(--line-soft); font-size: var(--glyph-md);
}
.item .t { font-weight: 500; }
.item .t a { color: var(--text); }
.item .m { color: var(--text-3); font-size: 12.5px; margin-top: 1px; }
.item .r { text-align: right; color: var(--text-2); font-size: 12.5px; white-space: nowrap; }
.item .m { color: var(--text-3); font-size: var(--fs-sm); margin-top: 1px; }
.item .r { text-align: right; color: var(--text-2); font-size: var(--fs-sm); white-space: nowrap; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 20px;
background: var(--line-soft); color: var(--text-2); font-size: 12.5px; border: 1px solid transparent;
display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 999px;
background: var(--line-soft); color: var(--text-2); font-size: var(--fs-sm); border: 1px solid transparent;
}
.chip.on { background: var(--accent-soft); color: var(--accent); border-color: #cfe6e0; font-weight: 500; }
.chip .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.tag { display: inline-block; padding: 1px 7px; border-radius: 5px; background: var(--accent-soft); color: var(--accent); font-size: 11.5px; }
.tag { display: inline-block; padding: 1px 7px; border-radius: var(--r-sm); background: var(--accent-soft); color: var(--accent); font-size: var(--fs-xs); }
/* ---------- 表单 ---------- */
label.f { display: block; margin-bottom: 13px; }
label.f > span { display: block; font-size: 12.5px; color: var(--text-2); margin-bottom: 5px; }
label.f > span { display: block; font-size: var(--fs-sm); color: var(--text-2); margin-bottom: 5px; }
/* 必填标记:字段名后的红色星号,以及表单顶部的统一说明 */
.req { color: #b42318; font-weight: 700; margin-left: 2px; }
.req-tip { font-size: 12.5px; color: var(--text-2); margin: 0 0 10px; }
.req { color: #b42318; font-weight: 600; margin-left: 2px; }
.req-tip { font-size: var(--fs-sm); color: var(--text-2); margin: 0 0 10px; }
.req-tip .req { margin-right: 3px; }
/* flatpickr 日期时间选择器:贴合站点风格 */
.flatpickr-calendar { border-radius: 10px; box-shadow: 0 8px 30px rgba(0,0,0,.14); font-family: inherit; }
.flatpickr-calendar { border-radius: var(--r-lg); box-shadow: var(--sh-2); font-family: inherit; }
.flatpickr-day.selected, .flatpickr-day.selected:hover { background: var(--accent); border-color: var(--accent); }
.flatpickr-day.today { border-color: var(--accent); }
.flatpickr-day.today:hover { background: var(--accent-soft); color: var(--text); }
@@ -268,30 +420,30 @@ label.f > span { display: block; font-size: 12.5px; color: var(--text-2); margin
没有圆角、没有内边距、13.33px 的字,跟旁边的输入框明显不是一套。 */
input:not([type=checkbox]):not([type=radio]):not([type=color]):not([type=range]):not([type=file]):not([type=submit]):not([type=button]):not([type=reset]):not([type=image]):not([type=hidden]),
select, textarea {
width: 100%; padding: 7px 10px; border: 1px solid var(--line); border-radius: 7px; font: inherit;
width: 100%; padding: 7px 10px; border: 1px solid var(--line); border-radius: var(--r-sm); font: inherit;
font-size: var(--field-fs, inherit);
background: var(--surface); color: var(--text); outline: none;
}
input:focus, select:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.check { display: flex; align-items: center; gap: 7px; font-size: 13px; color: var(--text-2); margin-bottom: 8px; word-break: keep-all; overflow-wrap: break-word; }
.check { display: flex; align-items: center; gap: 7px; font-size: var(--fs-sm); color: var(--text-2); margin-bottom: 8px; word-break: keep-all; overflow-wrap: break-word; }
.check input { width: 15px; height: 15px; accent-color: var(--accent); }
.nobr { white-space: nowrap; }
.btn {
display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; border-radius: 7px;
border: 1px solid var(--line); background: var(--surface); color: var(--text); font: inherit; font-size: 13.5px; cursor: pointer;
display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; border-radius: var(--r-sm);
border: 1px solid var(--line); background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-sm); cursor: pointer;
}
.btn:hover { background: var(--line-soft); text-decoration: none; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn.primary:hover { filter: brightness(1.06); }
.btn.sm { padding: 4px 10px; font-size: 12.5px; }
.btn.sm { padding: 4px 10px; font-size: var(--fs-sm); }
.btn.danger { color: #b42318; border-color: #f0cecb; }
.btn.danger:hover { background: var(--err-bg); }
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
/* ---------- 表格 ---------- */
table.tb { width: 100%; border-collapse: collapse; font-size: 13px; }
table.tb { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
table.tb th {
text-align: left; font-weight: 500; color: var(--text-3); font-size: 12px;
text-align: left; font-weight: 500; color: var(--text-3); font-size: var(--fs-sm);
padding: 7px 10px; border-bottom: 1px solid var(--line); white-space: nowrap;
}
table.tb td { padding: 9px 10px; border-bottom: 1px solid var(--line-soft); }
@@ -323,7 +475,7 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-
.map-panel {
position: absolute; top: 12px; left: 12px; z-index: var(--z-map-overlay); 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); box-shadow: var(--shadow);
border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--sh-2);
}
/* 内边距挂在表单上,不挂面板:窄屏的折叠条与操作行要 sticky 到面板的**内边缘**,
面板自己带 padding 的话它们只能粘在内容区边界上,边上会留一条缝让滚动内容穿过去
@@ -334,7 +486,7 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-
/* 「含家/工作」单独一行(不属于时间段预设)。窄屏预设行是单行横滑的,
它留在里面会被滑到屏幕外;单独一行上下各留 10px,读起来也像另一个开关。 */
.map-panel .hw-row { margin-bottom: 10px; }
.map-panel h3 { font-size: 13px; margin-bottom: 9px; color: var(--text-2); }
.map-panel h3 { font-size: var(--fs-sm); margin-bottom: 9px; color: var(--text-2); }
/* 筛选面板的折叠开关:桌面上面板只占 286px、不碍事,按钮直接不显示,只在窄屏出现 */
.panel-fold { display: none; }
/* 筛选面板里可折叠的两块(地点类型 / 更多筛选):这两块在窄屏各占两百多像素,
@@ -342,7 +494,7 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-
收起时标题里显示已选/已生效的那几项,否则收起后就不知道当前选了什么。 */
.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;
font-size: var(--fs-sm); 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); }
@@ -350,17 +502,17 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-
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 > summary .tf-arrow, .adv-fold > summary .tf-arrow { flex: 0 0 auto; margin-left: auto; font-size: var(--fs-xs); 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; }
.adv-fold > h3 { font-size: var(--fs-sm); color: var(--text-3); margin-bottom: 8px; }
.map-legend {
/* bottom 让开右下角那行底图版权(约 15px 高),否则两者会叠在一起 */
position: absolute; bottom: 26px; right: 12px; z-index: var(--z-map-overlay); background: var(--glass-strong);
border: 1px solid var(--line); border-radius: 8px; padding: 8px 11px; font-size: 12px;
max-height: 40%; overflow-y: auto; box-shadow: var(--shadow);
border: 1px solid var(--line); border-radius: var(--r-md); padding: 8px 11px; font-size: var(--fs-sm);
max-height: 40%; overflow-y: auto; box-shadow: var(--sh-2);
}
.map-legend .lg { display: flex; align-items: center; gap: 6px; margin-top: 4px; color: var(--text-2); }
.map-legend .lg i { width: 9px; height: 9px; border-radius: 50%; }
@@ -369,11 +521,11 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-
.fog-opacity { background: var(--surface); padding: 6px 8px; line-height: 1.2; }
.fog-opacity .fog-label {
display: block; text-align: left; border: 0; background: none; padding: 0;
font: inherit; font-size: 11px; color: var(--text-2); cursor: default;
font: inherit; font-size: var(--fs-xs); color: var(--text-2); cursor: default;
}
.fog-opacity .fog-slider { display: block; width: 96px; margin: 4px 0 0; accent-color: var(--accent); }
@media (max-width: 620px) {
.fog-opacity .fog-label { cursor: pointer; font-size: 12px; }
.fog-opacity .fog-label { cursor: pointer; font-size: var(--fs-sm); }
.fog-opacity .fog-slider { display: none; }
.fog-opacity.open .fog-slider { display: block; }
}
@@ -382,8 +534,8 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-
.to-top {
position: fixed; right: calc(16px + var(--safe-r)); bottom: calc(20px + var(--safe-b)); z-index: var(--z-map-status);
width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--line);
background: var(--panel); color: var(--text-2); font-size: 20px; line-height: 1;
cursor: pointer; box-shadow: var(--shadow);
background: var(--panel); color: var(--text-2); font-size: var(--glyph-lg); line-height: 1;
cursor: pointer; box-shadow: var(--sh-2);
}
.to-top[hidden] { display: none; }
.to-top:hover { color: var(--text); border-color: var(--text-3); }
@@ -430,23 +582,23 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-
bottom: 12px; z-index: var(--z-map-overlay);
display: flex; align-items: center; gap: 10px;
max-width: min(560px, calc(100% - 24px));
padding: 7px 12px; border-radius: 10px;
background: var(--glass-strong); border: 1px solid var(--line); box-shadow: var(--shadow);
padding: 7px 12px; border-radius: var(--r-lg);
background: var(--glass-strong); border: 1px solid var(--line); box-shadow: var(--sh-2);
backdrop-filter: blur(8px);
}
.timeline-bar[hidden] { display: none; }
.timeline-bar input[type=range] { flex: 1 1 auto; min-width: 90px; accent-color: var(--accent); margin: 0; }
.timeline-bar .tl-label { flex: 0 0 auto; font-size: 12px; color: var(--text-3); white-space: nowrap; font-variant-numeric: tabular-nums; }
.timeline-bar .tl-label { flex: 0 0 auto; font-size: var(--fs-sm); color: var(--text-3); white-space: nowrap; font-variant-numeric: tabular-nums; }
.timeline-bar button {
flex: 0 0 auto; border: 1px solid var(--line); background: var(--surface); color: var(--text);
border-radius: 7px; cursor: pointer; font: inherit; font-size: 12px; line-height: 1;
border-radius: var(--r-sm); cursor: pointer; font: inherit; font-size: var(--fs-sm); line-height: 1;
}
.timeline-bar .tl-play { width: 30px; height: 30px; font-size: 13px; padding: 0; }
.timeline-bar .tl-play { width: 30px; height: 30px; font-size: var(--fs-sm); padding: 0; }
.timeline-bar .tl-reset { padding: 7px 9px; }
.timeline-bar button:hover { background: var(--line-soft); }
@media (max-width: 620px) {
.timeline-bar { bottom: 26px; gap: 8px; padding: 6px 9px; }
.timeline-bar .tl-label { font-size: 11.5px; }
.timeline-bar .tl-label { font-size: var(--fs-xs); }
}
/* 地图上的状态条(加载失败 + 重试)。放底部中间:左上角是筛选面板、右下是图例与版权。 */
@@ -455,7 +607,7 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-
bottom: 62px;
z-index: 600; display: flex; align-items: center; gap: 10px;
max-width: calc(100% - 24px); background: rgba(30,40,50,.92); color: #fff;
border-radius: 8px; padding: 8px 12px; font-size: 13px; box-shadow: var(--shadow);
border-radius: var(--r-md); padding: 8px 12px; font-size: var(--fs-sm); box-shadow: var(--sh-2);
}
.map-status[hidden] { display: none; }
.map-status .btn { border-color: rgba(255,255,255,.35); background: rgba(255,255,255,.12); color: #fff; flex: 0 0 auto; }
@@ -476,12 +628,12 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-
于是它只盖住左边一小条,右边的内容会从粘性条旁边穿过去。 */
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;
font: inherit; font-size: var(--fs-sm); 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 .pf-n { color: var(--text-3); font-size: var(--fs-sm); font-weight: 400; }
.panel-fold .pf-c { margin-left: auto; padding-left: var(--sp-1); font-size: var(--glyph-xs); color: var(--text-3); }
.panel-fold:hover { color: var(--text); }
.panel-fold:hover .pf-c { color: var(--text); }
/* 「查询 / 重置」粘在面板底部:展开后表单 600+px、可视窗不到 400px,
@@ -500,21 +652,30 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-
.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); }
/* Leaflet 把 .leaflet-container 的字号写成 0.75rem(见 vendor/leaflet/leaflet.css 的
「general typography」),于是它跟着**根字号**漂:根从 14px 降到 13px 后,
右下角的署名与比例尺缩到了 9.8px(掉到本站 11px 的下限之下),弹窗正文也跟着缩。
这里按自己的字阶钉死,分两档:容器 = 正文(弹窗正文是 1.08333em,跟着它走),
署名与比例尺 = 脚注。 */
.leaflet-container { font-size: var(--fs-sm); }
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-scale-line { font-size: var(--fs-xs); }
.map-preview { height: 320px; border-radius: var(--r-md); overflow: hidden; border: 1px solid var(--line); }
/* ---------- 地点图片 ---------- */
/* 管理面板里的缩略图墙 */
.imgs-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; }
.imgs-cell { position: relative; margin: 0; border-radius: 8px; overflow: hidden; border: 1px solid var(--line); background: var(--line-soft); aspect-ratio: 4 / 3; }
.imgs-cell { position: relative; margin: 0; border-radius: var(--r-md); overflow: hidden; border: 1px solid var(--line); background: var(--line-soft); aspect-ratio: 4 / 3; }
.imgs-cell img { width: 100%; height: 100%; object-fit: cover; display: block; cursor: zoom-in; }
.imgs-del {
position: absolute; top: 5px; right: 5px; width: 22px; height: 22px; border-radius: 50%;
border: 0; background: rgba(0,0,0,.55); color: #fff; font-size: 15px; line-height: 1;
border: 0; background: rgba(0,0,0,.55); color: #fff; font-size: var(--glyph-sm); line-height: 1;
cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.imgs-del:hover { background: #b42318; }
.imgs-busy { opacity: .45; pointer-events: none; }
.imgs-hint { font-size: 12.5px; color: var(--text-3); margin-top: 8px; min-height: 18px; }
.imgs-hint { font-size: var(--fs-sm); color: var(--text-3); margin-top: 8px; min-height: 18px; }
.imgs-hint.ok { color: #17604f; }
.imgs-hint.err { color: #b42318; }
/* 存储没配好时把「选择图片」变成不可点的样子,而不是留个能点但必失败的按钮 */
@@ -530,26 +691,26 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-
/* 横滑留给我们做切换,纵向留给浏览器;pinch-zoom 让双指放大仍可用系统手势 */
touch-action: pan-y pinch-zoom;
}
.imgs-stage img { display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; border-radius: 8px; }
.imgs-stage img { display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; border-radius: var(--r-md); }
/* 双击放大:图放大到两倍、容器可拖动看局部(用尺寸而不是 transform,放大后不糊) */
.imgs-stage.zoomed { touch-action: pan-x pan-y; align-items: flex-start; justify-content: flex-start; }
.imgs-stage.zoomed img { max-width: none; max-height: none; width: 200%; border-radius: 0; }
.imgs-close {
position: fixed; top: calc(12px + var(--safe-t)); right: calc(14px + var(--safe-r)); z-index: 2001;
width: 44px; height: 44px; border-radius: 50%;
border: 1px solid rgba(255,255,255,.3); background: rgba(0,0,0,.4); color: #fff; font-size: 22px;
border: 1px solid rgba(255,255,255,.3); background: rgba(0,0,0,.4); color: #fff; font-size: var(--glyph-lg);
line-height: 1; cursor: pointer;
}
.imgs-close:hover { background: rgba(255,255,255,.18); }
.imgs-count {
position: fixed; top: calc(20px + var(--safe-t)); left: calc(18px + var(--safe-l)); z-index: 2001;
color: rgba(255,255,255,.78); font-size: 13px;
color: rgba(255,255,255,.78); font-size: var(--fs-sm);
}
/* 上一张 / 下一张:做成整条竖带,拇指够得着,不用瞄小按钮 */
.imgs-nav {
position: fixed; top: 50%; transform: translateY(-50%); z-index: 2001;
width: 44px; height: 76px; border: 0; border-radius: 10px; cursor: pointer;
background: rgba(255,255,255,.12); color: #fff; font-size: 30px; line-height: 1;
width: 44px; height: 76px; border: 0; border-radius: var(--r-lg); cursor: pointer;
background: rgba(255,255,255,.12); color: #fff; font-size: var(--glyph-xl); line-height: 1;
}
.imgs-nav:hover { background: rgba(255,255,255,.24); }
.imgs-prev { left: calc(6px + var(--safe-l)); }
@@ -558,26 +719,26 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-
/* ---------- 「停一下」提示页(内容对访客不开放之类) ---------- */
.notice-body {
min-height: 100vh; margin: 0; display: flex; align-items: center; justify-content: center;
padding: 24px 18px; background: var(--bg);
padding: var(--sp-5) 18px; background: var(--bg);
}
.notice-card {
max-width: 420px; text-align: center; background: var(--panel); border: 1px solid var(--line);
border-radius: var(--radius); padding: 30px 26px; box-shadow: var(--shadow);
border-radius: var(--r-lg); padding: 30px 26px; box-shadow: var(--sh-2);
}
.notice-card .notice-ico { font-size: 40px; line-height: 1; margin-bottom: 12px; }
.notice-card h1 { font-size: 18px; margin: 0 0 8px; }
.notice-card p { color: var(--text-2); font-size: 13.5px; margin: 0 0 18px; }
.notice-card .notice-tip { color: var(--text-3); font-size: 12px; margin-top: 12px; }
.notice-card .notice-ico { font-size: var(--glyph-2xl); line-height: 1; margin-bottom: var(--sp-3); }
.notice-card h1 { font-size: var(--fs-lg); margin: 0 0 var(--sp-2); }
.notice-card p { color: var(--text-2); font-size: var(--fs-sm); margin: 0 0 18px; }
.notice-card .notice-tip { color: var(--text-3); font-size: var(--fs-sm); margin-top: 12px; }
/* 后台设置页的「标签 / 值」信息行(地图底图、图片存储状态)。
这个 class 一直没写样式,之前是两段光秃秃的 inline 文本;长一点的 URL
(比如对象存储的 endpoint)会把整页撑宽 —— 所以要 minmax(0,1fr) + 允许断词。 */
.kv { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: 4px 12px; font-size: 13px; }
.kv { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: 4px 12px; font-size: var(--fs-sm); }
.kv .k { color: var(--text-3); }
.kv .v { color: var(--text); overflow-wrap: anywhere; }
@media (max-width: 620px) {
.kv { grid-template-columns: 1fr; gap: 2px; }
.kv .k { font-size: 12px; }
.kv .k { font-size: var(--fs-sm); }
}
/* URL 这类超长 token 不该把容器顶宽 */
.mono { overflow-wrap: anywhere; }
@@ -608,13 +769,19 @@ table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-
--bg-2: #191d21;
/* 浅绿底上用白字只有 2.4:1,反过来用深字才够 */
--on-accent: #0f1b18;
/* 类型色只有 indigo 在深底上不够:#5856d6 对 #1c2024 只有 2.90:1,其余都在 3.9 以上。
掺 10% 白提亮到 3.55:1。别的色一概不动 —— 它们本来就是浅亮色,深底上只会更清楚,
改了反而与手机端 / 地图上的颜色对不上。 */
--cat-indigo: #6967da;
--ok-bg: #16302a; --ok-fg: #7fd3bb; --ok-line: #245046;
--err-bg: #33201e; --err-fg: #f0a49b; --err-line: #5a3330;
--info-bg: #1b2534; --info-fg: #9dbdea; --info-line: #2f4055;
--shadow: 0 1px 2px rgba(0, 0, 0, .45), 0 1px 3px rgba(0, 0, 0, .35);
--sh-1: 0 1px 2px rgba(0, 0, 0, .45), 0 1px 3px rgba(0, 0, 0, .35);
--sh-2: 0 6px 18px rgba(0, 0, 0, .50);
--sh-3: 0 12px 34px rgba(0, 0, 0, .55);
}
/* 兜底:还有两处没走变量的白底 */
.flatpickr-calendar { background: var(--panel); border-color: var(--line); box-shadow: var(--shadow); }
.flatpickr-calendar { background: var(--panel); border-color: var(--line); box-shadow: var(--sh-2); }
.flatpickr-day { color: var(--text); }
.flatpickr-day.prevMonthDay, .flatpickr-day.nextMonthDay { color: var(--text-3); }
.flatpickr-day:hover { background: var(--line-soft); color: var(--text); }
@@ -667,40 +834,40 @@ input:focus-visible, select:focus-visible, textarea:focus-visible, summary:focus
/* ---------- 「已选条件」行 ---------- */
.active-filters { margin-bottom: 12px; align-items: center; }
.active-filters .af-label { font-size: 12.5px; color: var(--text-3); }
.active-filters .af-label { font-size: var(--fs-sm); color: var(--text-3); }
.active-filters .chip { user-select: none; }
.active-filters .af-x { margin-left: 4px; opacity: .6; font-size: 13px; }
.active-filters .af-x { margin-left: 4px; opacity: .6; font-size: var(--fs-sm); }
.active-filters .chip:hover .af-x { opacity: 1; }
.active-filters .af-clear { background: transparent; color: var(--text-3); border-color: var(--line); }
/* ---------- 其他 ---------- */
.alert { padding: 9px 13px; border-radius: 8px; font-size: 13px; margin-bottom: 14px; }
.alert { padding: 9px 13px; border-radius: var(--r-md); font-size: var(--fs-sm); margin-bottom: 14px; }
.alert.err { background: var(--err-bg); color: var(--err-fg); border: 1px solid var(--err-line); }
.alert.ok { background: var(--ok-bg); color: var(--ok-fg); border: 1px solid var(--ok-line); }
.alert.info { background: var(--info-bg); color: var(--info-fg); border: 1px solid var(--info-line); }
.muted { color: var(--text-3); }
.mono { font-family: var(--mono); font-size: 12px; }
.mono { font-family: var(--mono); font-size: var(--fs-sm); }
.empty { text-align: center; padding: 40px 16px; color: var(--text-3); }
.empty .big { font-size: 30px; margin-bottom: 8px; }
.pager { display: flex; gap: 6px; align-items: center; justify-content: center; margin-top: 18px; font-size: 13px; }
.pager a, .pager span { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--line); color: var(--text-2); }
.empty .big { font-size: var(--glyph-xl); margin-bottom: var(--sp-2); }
.pager { display: flex; gap: 6px; align-items: center; justify-content: center; margin-top: 18px; font-size: var(--fs-sm); }
.pager a, .pager span { padding: 4px 10px; border-radius: var(--r-sm); border: 1px solid var(--line); color: var(--text-2); }
.pager a:hover { background: var(--line-soft); text-decoration: none; }
.pager .cur { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
footer.ft { max-width: 1120px; margin: 0 auto; padding: 20px; color: var(--text-3); font-size: 12.5px; text-align: center; }
footer.ft { max-width: 1120px; margin: 0 auto; padding: 20px; color: var(--text-3); font-size: var(--fs-sm); text-align: center; }
.timeline-day { margin-bottom: 20px; }
.timeline-day .dh {
position: sticky; top: var(--nav-h); background: var(--bg); z-index: 5; padding: 6px 0 7px;
font-size: 13px; color: var(--text-2); font-weight: 500; display: flex; gap: 8px; align-items: baseline;
font-size: var(--fs-sm); color: var(--text-2); font-weight: 500; display: flex; gap: 8px; align-items: baseline;
}
.timeline-day .dh .muted { font-size: 12px; }
.timeline-day .dh .muted { font-size: var(--fs-sm); }
.tl-item { display: grid; grid-template-columns: 62px 20px 1fr; gap: 10px; align-items: start; padding: 7px 0; }
.tl-item .tm { color: var(--text-3); font-size: 12.5px; font-variant-numeric: tabular-nums; padding-top: 1px; }
.tl-item .tm { color: var(--text-3); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; padding-top: 1px; }
.tl-item .ln { display: flex; justify-content: center; position: relative; }
.tl-item .ln::before { content: ""; position: absolute; top: 12px; bottom: -12px; width: 1px; background: var(--line); }
.tl-item:last-child .ln::before { display: none; }
.tl-item .ln i { width: 9px; height: 9px; border-radius: 50%; background: var(--accent); margin-top: 5px; position: relative; z-index: 1; }
.tl-item .bd { font-size: 13.5px; }
.tl-item .bd .meta { color: var(--text-3); font-size: 12.5px; }
.tl-item .bd { font-size: var(--fs-sm); }
.tl-item .bd .meta { color: var(--text-3); font-size: var(--fs-sm); }
/* 聚合气泡改用主题色,默认的黄蓝配色和站点风格不一致 */
.marker-cluster-small,
@@ -716,7 +883,7 @@ footer.ft { max-width: 1120px; margin: 0 auto; padding: 20px; color: var(--text-
/* 更多筛选类型 */
.more-types { margin: 2px 0 10px; }
.more-types summary { font-size: 12.5px; color: var(--text-3); cursor: pointer; padding: 3px 0; }
.more-types summary { font-size: var(--fs-sm); color: var(--text-3); cursor: pointer; padding: 3px 0; }
.more-types summary:hover { color: var(--accent); }
.more-types .chips { margin-top: 8px; }
@@ -726,14 +893,14 @@ footer.ft { max-width: 1120px; margin: 0 auto; padding: 20px; color: var(--text-
/* 推荐 / 踩雷 表态按钮 */
.vote-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.vote-label { color: var(--text-3); font-size: 12.5px; min-width: 62px; }
.vote-label { color: var(--text-3); font-size: var(--fs-sm); min-width: 62px; }
/* 「推荐 / 踩雷」必须并排:把它们包成不可换行的组,窄屏时只让后面那句提示换行 */
.vote-btns { display: inline-flex; gap: 8px; flex-wrap: nowrap; }
.vote-btn {
display: inline-flex; align-items: center; gap: 5px;
padding: 6px 14px; border-radius: 999px; white-space: nowrap; flex: 0 0 auto;
border: 1px solid var(--line); background: var(--surface); color: var(--text);
font-size: 13.5px; font-weight: 500; cursor: pointer; line-height: 1;
font-size: var(--fs-sm); font-weight: 500; cursor: pointer; line-height: 1;
transition: background .12s, border-color .12s, color .12s;
}
.vote-btn:hover { border-color: var(--accent); color: var(--accent); }
@@ -749,7 +916,7 @@ footer.ft { max-width: 1120px; margin: 0 auto; padding: 20px; color: var(--text-
.btn.danger { color: var(--err-fg); border-color: var(--err-line); }
}
.vote-btn:disabled { opacity: .55; cursor: default; }
.vote-tip { font-size: 12.5px; color: var(--text-3); }
.vote-tip { font-size: var(--fs-sm); color: var(--text-3); }
.vote-tip.up { color: #1f8f43; }
.vote-tip.down { color: #c62a20; }
/* 站长页面上访客票数只读展示:沿用「我的结论」同一套行结构,尺寸/行距完全一致,只弱化配色 */
@@ -767,22 +934,22 @@ footer.ft { max-width: 1120px; margin: 0 auto; padding: 20px; color: var(--text-
/* 地图气泡:地点信息 + 只读备注 + 就地表决 */
.mp { min-width: 240px; max-width: 300px; }
.mp-n { font-weight: 600; margin-bottom: 2px; }
.mp-m { color: var(--text-3); font-size: 12px; line-height: 1.6; }
.mp-m { color: var(--text-3); font-size: var(--fs-sm); line-height: 1.6; }
.mp-note {
margin-top: 6px; padding: 6px 8px; border-radius: 6px;
margin-top: 6px; padding: 6px 8px; border-radius: var(--r-sm);
background: var(--bg, #f5f6f8); color: var(--text);
font-size: 12.5px; line-height: 1.55; white-space: pre-line;
font-size: var(--fs-sm); line-height: 1.55; white-space: pre-line;
max-height: 150px; overflow-y: auto;
}
.mp-foot { margin-top: 7px; font-size: 12.5px; }
.mp-foot { margin-top: 7px; font-size: var(--fs-sm); }
/* nowrap:气泡本来就窄,「👎 踩雷」被挤到第二行会很难看 */
.mp-vote { display: flex; align-items: center; gap: 6px; margin-top: 8px; flex-wrap: nowrap; }
.mp-vl { flex: 0 0 auto; color: var(--text-3); font-size: 12px; white-space: nowrap; }
.mp-vl { flex: 0 0 auto; color: var(--text-3); font-size: var(--fs-sm); white-space: nowrap; }
.mp-vb {
display: inline-flex; align-items: center; gap: 4px;
padding: 4px 10px; border-radius: 999px;
border: 1px solid var(--line); background: var(--surface); color: var(--text);
font-size: 12.5px; font-weight: 500; line-height: 1; cursor: pointer;
font-size: var(--fs-sm); font-weight: 500; line-height: 1; cursor: pointer;
flex: 0 0 auto; white-space: nowrap;
transition: background .12s, border-color .12s, color .12s;
}
@@ -794,9 +961,9 @@ footer.ft { max-width: 1120px; margin: 0 auto; padding: 20px; color: var(--text-
/* 备注块:rond 回填的备注是按次一行的,必须保留换行 */
.note-block {
margin-top: 4px; padding: 7px 9px; border-radius: 8px;
margin-top: 4px; padding: 7px 9px; border-radius: var(--r-md);
background: var(--bg, #f5f6f8); color: var(--text-2);
font-size: 12.8px; line-height: 1.65; white-space: pre-line;
font-size: var(--fs-sm); line-height: 1.65; white-space: pre-line;
}
/* 报告导出为 PDF / 长图:隐藏导航、页脚与操作按钮,卡片避免跨页断裂 */
@@ -820,7 +987,7 @@ footer.ft { max-width: 1120px; margin: 0 auto; padding: 20px; color: var(--text-
.inline-edit[open] { display: block; }
/* 行内编辑表单的宽度:桌面靠 --efw 撑到够填(各表不同,见模板里的 inline 变量),
放不下宽表时下面会放开这个下限——不然 320px 的表单会把 294px 的卡片顶破。 */
.inline-edit form { background: var(--bg-2, #fafbfb); border: 1px solid var(--line); border-radius: 10px; padding: 12px; min-width: var(--efw, 280px); }
.inline-edit form { background: var(--bg-2, #fafbfb); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 12px; min-width: var(--efw, 280px); }
.inline-edit form .f { margin-bottom: 8px; }
/* 「按名称搜坐标」输入框 + 按钮:必须始终同一行(含移动端),底边对齐。
@@ -828,7 +995,7 @@ footer.ft { max-width: 1120px; margin: 0 auto; padding: 20px; color: var(--text-
* 且 .row 会换行。这里显式 nowrap + 归零外边距,并让输入框可收缩。 */
.search-row { display: flex; flex-wrap: nowrap; align-items: flex-end; gap: 8px; margin-bottom: 12px; }
.search-row > label.f { flex: 1 1 auto; min-width: 0; margin-bottom: 0; }
.search-row .btn { flex: 0 0 auto; font-size: 14px; }
.search-row .btn { flex: 0 0 auto; font-size: var(--fs-sm); }
.search-row > .act { display: flex; flex: 0 0 auto; gap: 8px; }
/* 按名称搜坐标的候选列表:整行可点,名称与地址分两行 */
@@ -850,31 +1017,31 @@ footer.ft { max-width: 1120px; margin: 0 auto; padding: 20px; color: var(--text-
.filter-row > .act { display: flex; flex: 0 0 auto; gap: 8px; }
.geo-item {
display: block; width: 100%; text-align: left; cursor: pointer;
padding: 7px 10px; border: 1px solid var(--line); border-radius: 8px;
padding: 7px 10px; border: 1px solid var(--line); border-radius: var(--r-md);
background: var(--panel); color: var(--text); font: inherit;
}
.geo-item:hover { background: var(--line-soft); }
.geo-name { display: block; font-size: 13.5px; }
.geo-addr { display: block; color: var(--text-3); font-size: 12px; margin-top: 2px; }
.geo-name { display: block; font-size: var(--fs-sm); }
.geo-addr { display: block; color: var(--text-3); font-size: var(--fs-sm); margin-top: 2px; }
/* ---------- 专题 ---------- */
/* 必须带上 #map:站点里 #map{height:100%} 是 ID 选择器,
单靠 .topic-map 这个类压不过它,容器会被算成 0 高(地图看不见)。 */
#map.topic-map { height: 520px; width: 100%; border-radius: 10px; overflow: hidden; }
#map.topic-map { height: 520px; width: 100%; border-radius: var(--r-lg); overflow: hidden; }
@media (max-width: 620px) { #map.topic-map { height: 360px; } }
.topic-card { display: block; text-decoration: none; color: inherit; transition: box-shadow .15s, transform .15s; }
.topic-card:hover { box-shadow: 0 6px 22px rgba(0,0,0,.08); transform: translateY(-1px); }
.topic-card:hover { box-shadow: var(--sh-2); transform: translateY(-1px); }
.topic-head { display: flex; align-items: center; gap: 8px; }
.topic-head h2 { margin: 0; }
/* 地图兜底提示:脚本没就绪时留在容器里,解释为什么没有地图 */
.map-fallback {
display: flex; align-items: center; justify-content: center; height: 100%;
padding: 20px; text-align: center; color: var(--text-3); font-size: 13px;
padding: 20px; text-align: center; color: var(--text-3); font-size: var(--fs-sm);
}
/* 专题:时间范围独立成行(精确到分钟,塞进 KPI 网格会被挤成多行) */
.topic-range { margin-top: 5px; color: var(--text-2); font-size: 13.5px; font-variant-numeric: tabular-nums; }
.topic-range { margin-top: 5px; color: var(--text-2); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.topic-card .topic-range { margin: 7px 0 0; }
.topic-desc { color: var(--text-2); white-space: pre-wrap; line-height: 1.65; }
.kpis.kpis-3 .kpi { flex: 1 1 30%; }
@@ -882,13 +1049,13 @@ footer.ft { max-width: 1120px; margin: 0 auto; padding: 20px; color: var(--text-
/* 三个数字都很短,窄屏保持三列比 2+1 更好看 */
.topic-card .kpis.kpis-3 .kpi { flex: 1 1 30%; }
.topic-card .kpi { padding: 11px 12px; }
.topic-range { font-size: 12.5px; }
.topic-range { font-size: var(--fs-sm); }
}
/* ---------- 表单里的「文字 + 内联按钮」组合 ---------- */
/* label 的文字行右侧放次要操作(活动改名/删除),不额外占高度,也就不会把整格撑高 */
label.f > span.span-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.mini-actions { display: flex; gap: 8px; font-size: 12px; }
.mini-actions { display: flex; gap: 8px; font-size: var(--fs-sm); }
.mini-actions a { color: var(--text-3); }
.mini-actions a:hover { color: var(--accent); }
.mini-actions a.danger:hover { color: #b42318; }
@@ -900,7 +1067,7 @@ label.f > span.span-actions { display: flex; align-items: center; justify-conten
/* 到访列表里的收藏星标:点一下就切换,不用展开行内编辑 */
.star {
background: none; border: 0; padding: 0 3px; cursor: pointer; line-height: 1;
font-size: 15px; color: var(--text-3); vertical-align: -1px;
font-size: var(--glyph-sm); color: var(--text-3); vertical-align: -1px;
}
.star:hover { color: #e8b700; }
.star.on { color: #e8b700; }
@@ -912,11 +1079,11 @@ label.f > span.span-actions { display: flex; align-items: center; justify-conten
background: rgba(0, 0, 0, .45);
}
.point-modal-box {
width: 100%; max-width: 660px; background: var(--panel); border-radius: 12px;
padding: 14px; box-shadow: 0 12px 40px rgba(0, 0, 0, .25);
width: 100%; max-width: 660px; background: var(--panel); border-radius: var(--r-lg);
padding: 14px; box-shadow: var(--sh-3);
}
.point-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.point-modal-map { height: 360px; border-radius: 10px; overflow: hidden; }
.point-modal-map { height: 360px; border-radius: var(--r-lg); overflow: hidden; }
@media (max-width: 620px) {
#point-modal { padding: 16px 10px; }
.point-modal-map { height: 260px; }
@@ -931,7 +1098,7 @@ label.f > span.span-actions { display: flex; align-items: center; justify-conten
.photo-table { table-layout: fixed; width: 100%; min-width: 880px; }
.photo-table th, .photo-table td { padding: 7px 8px; vertical-align: top; }
.photo-table input, .photo-table select { width: 100%; min-width: 0; box-sizing: border-box; }
.photo-table input[type=datetime-local] { font-size: 12.5px; padding: 6px 6px; }
.photo-table input[type=datetime-local] { font-size: var(--fs-sm); padding: 6px 6px; }
.photo-table input[type=checkbox] { width: auto; }
.photo-table .chip { white-space: nowrap; }
@@ -954,14 +1121,14 @@ label.f > span.span-actions { display: flex; align-items: center; justify-conten
border-bottom: 1px solid var(--line-soft);
}
.photo-map {
height: 200px; width: 100%; border-radius: 10px; overflow: hidden;
height: 200px; width: 100%; border-radius: var(--r-lg); overflow: hidden;
border: 1px solid var(--line); transition: height .18s ease;
}
/* 粘住以后收矮:200px 的浮层会挡住从下面滚过去的半个屏幕,收到 132px 还能看清
高亮出来的那个点,占的高度也只剩一条。说明文字这时候就没必要留着了。 */
#map-card.stuck .photo-map { height: 132px; }
#map-card.stuck .card-sub { display: none; }
#map-card.stuck { box-shadow: 0 6px 14px rgba(16, 24, 40, .07); }
#map-card.stuck { box-shadow: var(--sh-1); }
@media (max-width: 700px) {
.photo-map { height: 170px; }
#map-card.stuck .photo-map { height: 110px; }
@@ -993,9 +1160,9 @@ label.f > span.span-actions { display: flex; align-items: center; justify-conten
table.tb select, .photo-table select { min-height: 0; }
/* file / color 这两种被基础规则排除在外(不套文本框外观),也就没吃到
--field-fs,窄屏仍是 13.3px ⇒ iOS 一聚焦就放大。单独提上来。 */
input[type=file], input[type=color] { font-size: 16px; }
input[type=file], input[type=color] { font-size: var(--fs-control); }
/* 11.5px 的标签在手机上偏小(/stats 一页 29 处、/ 15 处) */
.tag { font-size: 12px; }
.tag { font-size: var(--fs-sm); }
/* 导航左上角那个 logo 是回首页的入口,原来只有 25×29:给它撑到 44 高,
横向也补到 44(.brand .t 在窄屏被隐藏,加内边距就够了)。 */
.brand { min-width: 44px; min-height: 44px; justify-content: center; }
@@ -1007,10 +1174,10 @@ label.f > span.span-actions { display: flex; align-items: center; justify-conten
top 取 --nav-h:导航是 sticky 的,写 0 会被压在它下面。
这些规则整条在媒体查询里 ⇒ 桌面端 .collapsed 不生效,等于没影响。 */
.filters-card > .panel-fold {
margin: -18px -18px 14px; border-radius: var(--radius) var(--radius) 0 0; top: var(--nav-h);
margin: calc(var(--card-pad) * -1) calc(var(--card-pad) * -1) 14px; border-radius: var(--r-lg) var(--r-lg) 0 0; top: var(--nav-h);
}
.filters-card.collapsed { padding: 0; }
.filters-card.collapsed > .panel-fold { margin: 0; border-radius: var(--radius); }
.filters-card.collapsed > .panel-fold { margin: 0; border-radius: var(--r-lg); }
.filters-card.collapsed > :not(.panel-fold) { display: none; }
}
@@ -1038,21 +1205,21 @@ label.f > span.span-actions { display: flex; align-items: center; justify-conten
*/
dialog.prompt-box {
border: 1px solid var(--line);
border-radius: var(--radius);
border-radius: var(--r-lg);
padding: 0;
min-width: 0; width: min(420px, calc(100vw - 32px));
background: var(--surface);
color: var(--text);
box-shadow: 0 8px 28px rgba(16, 24, 40, .18);
box-shadow: var(--sh-2);
}
dialog.prompt-box::backdrop { background: rgba(16, 24, 40, .38); }
.prompt-box .pb-body { padding: 14px 16px 12px; }
.prompt-box .pb-title { font-size: 14.5px; font-weight: 500; margin-bottom: 8px; }
.prompt-box .pb-title { font-size: var(--fs-md); font-weight: 500; margin-bottom: 8px; }
/* 输入框字号必须 ≥16px:iOS Safari 聚焦时会自动放大页面,16px 以下会把布局顶歪 */
.prompt-box .pb-input {
width: 100%; font-size: 16px; font-family: inherit; color: var(--text);
width: 100%; font-size: var(--fs-control); font-family: inherit; color: var(--text);
background: var(--surface); border: 1px solid var(--line);
border-radius: 7px; padding: 7px 10px; min-height: 40px;
border-radius: var(--r-sm); padding: 7px 10px; min-height: 40px;
}
.prompt-box .pb-input:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
.prompt-box .pb-acts { display: flex; gap: 8px; justify-content: flex-end; padding: 0 16px 14px; }
@@ -1067,12 +1234,12 @@ dialog.prompt-box::backdrop { background: rgba(16, 24, 40, .38); }
.prompt-fallback {
position: fixed; inset: 8px 8px auto 8px; z-index: 2000;
display: flex; gap: 8px; align-items: center;
padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius);
background: var(--surface); box-shadow: 0 8px 28px rgba(16, 24, 40, .18);
padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-lg);
background: var(--surface); box-shadow: var(--sh-2);
}
.prompt-fallback input {
flex: 1; min-width: 0; font-size: 16px; font-family: inherit;
border: 1px solid var(--line); border-radius: 7px; padding: 7px 10px; min-height: 40px;
flex: 1; min-width: 0; font-size: var(--fs-control); font-family: inherit;
border: 1px solid var(--line); border-radius: var(--r-sm); padding: 7px 10px; min-height: 40px;
}
@media (max-width: 800px) {
@@ -1092,7 +1259,7 @@ dialog.prompt-box::backdrop { background: rgba(16, 24, 40, .38); }
}
.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;
width: 5em; color: var(--text-3); font-size: var(--fs-sm);
}
/* 首格是这一行的主角(地点名 / 天气 / 方式…):不缩进、字号抬一档当小标题;
操作格也是这一类(表头为空、整行铺开)。它们里面的内容是长名称或编辑表单,
@@ -1100,7 +1267,7 @@ dialog.prompt-box::backdrop { background: rgba(16, 24, 40, .38); }
于是「已选:#403 2026-09-13 19:53 · 某某地」这一行在窄处不换行、把卡片顶出 44px。
堆叠态每格本来就是一整行,nowrap 没有意义,放开。 */
.tw > table.tb.tb-stack > tbody > tr > td:not([data-label]) {
padding-left: 0; margin-bottom: 2px; font-size: 14px; white-space: normal;
padding-left: 0; margin-bottom: 2px; font-size: var(--fs-sm); white-space: normal;
}
/* 操作格(有的带「操作」标签、有的表头本来就是空的)在堆叠态整行铺开,
桌面那条 nowrap 会让格里的编辑表单不换行、把卡片顶出几十像素 */
@@ -131,7 +131,7 @@
<td data-label="活动">{{if .Activity}}{{.Activity}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="差异">
{{range .Fields}}
<div style="font-size:12.5px;line-height:1.7">
<div style="font-size:var(--fs-sm);line-height:1.7">
<span class="muted">{{.Label}}</span>
<span style="text-decoration:line-through;opacity:.65">{{if .From}}{{.From}}{{else}}(空){{end}}</span>
<span style="opacity:.5">→</span>
@@ -190,7 +190,7 @@
<td class="num mono" data-label="坐标">{{printf "%.5f" .Lat}}, {{printf "%.5f" .Lon}}</td>
<td data-label="合并时删除">
{{if gt .VisitCount 0}}
<span class="muted" style="font-size:12.5px">还有到访,不能删</span>
<span class="muted" style="font-size:var(--fs-sm)">还有到访,不能删</span>
{{else}}
<input type="checkbox" form="merge-form" name="dp_{{.SrcPK}}" value="1">
{{end}}
@@ -213,7 +213,7 @@
<td>{{if .DB.Name}}{{.DB.Name}}{{else}}<span class="muted">(未命名)</span>{{end}}</td>
<td data-label="差异">
{{range .Fields}}
<div style="font-size:12.5px;line-height:1.7">
<div style="font-size:var(--fs-sm);line-height:1.7">
<span class="muted">{{.Label}}</span>
<span style="text-decoration:line-through;opacity:.65">{{if .From}}{{.From}}{{else}}(空){{end}}</span>
<span style="opacity:.5">→</span>
@@ -241,17 +241,17 @@
</div>
<div class="row" style="gap:24px;align-items:flex-start;flex-wrap:wrap">
<div style="flex:1 1 220px">
<div class="muted" style="font-size:12.5px;margin-bottom:4px">备份新增</div>
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:4px">备份新增</div>
{{range .ActAdded}}<div>{{.Name}}</div>{{else}}<div class="muted">没有</div>{{end}}
</div>
<div style="flex:1 1 220px">
<div class="muted" style="font-size:12.5px;margin-bottom:4px">仅站内有(勾上=合并时删除)</div>
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:4px">仅站内有(勾上=合并时删除)</div>
{{range .ActGone}}
<label style="display:block"><input type="checkbox" form="merge-form" name="da_{{.SrcPK}}" value="1"> {{.Name}}</label>
{{else}}<div class="muted">没有</div>{{end}}
</div>
<div style="flex:1 1 260px">
<div class="muted" style="font-size:12.5px;margin-bottom:4px">名称不同</div>
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:4px">名称不同</div>
{{range .ActClash}}
<div style="margin-bottom:6px">
{{range .Fields}}<div><span style="text-decoration:line-through;opacity:.65">{{.From}}</span> → <strong>{{.To}}</strong></div>{{end}}
+1 -1
View File
@@ -121,7 +121,7 @@
<input type="file" name="backup" accept=".rondbackup,.zip" required>
<button class="btn primary" type="submit">上传并换底座</button>
</div>
<label class="muted" style="display:block;margin-top:10px;font-size:13px">
<label class="muted" style="display:block;margin-top:10px;font-size:var(--fs-sm)">
<input type="checkbox" name="force" value="1">
即使与当前数据集对不上也强制替换(重合率偏低时会再让你确认一次)
</label>
+1 -1
View File
@@ -518,7 +518,7 @@
<td class="muted mono">{{if .Span}}{{.Span}}{{else}}(无时间){{end}}
{{if .DurationMin}}<br><span class="muted">{{dur .DurationMin}}</span>{{end}}</td>
<td class="mv-route" data-label="路线" title="{{.FromLabel}}{{if .FromCity}} · {{.FromCity}}{{end}} → {{.ToLabel}}{{if .ToCity}} · {{.ToCity}}{{end}}">{{.FromLabel}}<span class="mv-city">{{if .FromCity}} · {{.FromCity}}{{end}}</span><br><span class="muted">→ {{.ToLabel}}<span class="mv-city">{{if .ToCity}} · {{.ToCity}}{{end}}</span></span></td>
<td class="muted" data-label="方式">{{if .TransportName}}<span class="dot" style="display:inline-block;background:{{colorof .TransportColor}}"></span> {{.TransportName}} <span class="muted mono">#{{.TransportSrc}}</span>{{else}}(无方式){{end}}</td>
<td class="muted" data-label="方式">{{if .TransportName}}<span class="dot" style="display:inline-block;background:{{csscolor .TransportColor}}"></span> {{.TransportName}} <span class="muted mono">#{{.TransportSrc}}</span>{{else}}(无方式){{end}}</td>
<td class="num" data-label="里程">{{if .DistanceKm}}{{km .DistanceKm}}{{else}}—{{end}}</td>
<td class="ops">
<details class="inline-edit">
+2 -2
View File
@@ -16,7 +16,7 @@
{{if .Rows}}
<div class="card">
<h2>待补全({{num .Total}}){{if gt .PageCount 1}}<span class="muted" style="font-size:13px;font-weight:400"> 第 {{.PageNo}} / {{.PageCount}} 页</span>{{end}}</h2>
<h2>待补全({{num .Total}}){{if gt .PageCount 1}}<span class="muted" style="font-size:var(--fs-sm);font-weight:400"> 第 {{.PageNo}} / {{.PageCount}} 页</span>{{end}}</h2>
<div class="card-sub">
<strong>默认都是「不动」</strong>,你改过的才会写进去。
活动可以直接打字(会提示已有的类型),也可以点「采纳本页建议」一键填上;
@@ -79,7 +79,7 @@
<td data-label="建议来源">{{if $row.Source}}{{$row.Source}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="高德参考">
{{if $row.AmapType}}
<span style="font-size:12.5px">{{if $row.AmapPoi}}{{$row.AmapPoi}}<br>{{end}}<span class="muted">{{$row.AmapType}}</span></span>
<span style="font-size:var(--fs-sm)">{{if $row.AmapPoi}}{{$row.AmapPoi}}<br>{{end}}<span class="muted">{{$row.AmapType}}</span></span>
{{else}}<span class="muted">—</span>{{end}}
</td>
</tr>
+1 -1
View File
@@ -33,7 +33,7 @@
<button class="btn primary" type="submit" id="mask-save">保存</button>
<button class="btn" type="button" id="mask-reset">清除选择</button>
</div>
<div class="muted" id="mask-hint" style="font-size:12.5px;margin-top:6px">在下方地图上点选圆心</div>
<div class="muted" id="mask-hint" style="font-size:var(--fs-sm);margin-top:6px">在下方地图上点选圆心</div>
</form>
</div>
@@ -55,7 +55,7 @@
<div class="card">
<h2>差异地图</h2>
<div class="card-sub">在真实底图上核对差异落在哪些区域,可以缩放平移。</div>
<div class="row" style="gap:16px;margin:2px 0 10px;font-size:12.5px;flex-wrap:wrap">
<div class="row" style="gap:16px;margin:2px 0 10px;font-size:var(--fs-sm);flex-wrap:wrap">
<span style="display:flex;align-items:center;gap:6px">
<i style="width:13px;height:13px;border-radius:3px;background:#24a45a;display:inline-block"></i>叠加会新增
</span>
@@ -110,7 +110,7 @@
⚠️ 快照里没有 <span class="mono">Model/~</span> 与 <span class="mono">Model/#</span> 两层数据,替换会把现有的这两层一并清掉。
</div>
{{else}}
<div class="muted" style="font-size:12.5px;margin-bottom:8px">
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:8px">
另外,站点不解析但会留存的 <span class="mono">Model/~</span>({{.TildeLayers}} 项)与
<span class="mono">Model/#</span>({{.HashLayers}} 项)也会一并换成这份快照里的{{if .OtherLayers}},另有 {{.OtherLayers}} 项其它条目{{end}}。
</div>
+1 -1
View File
@@ -66,7 +66,7 @@
</label>
<button class="btn primary" type="submit">分析差异</button>
</form>
<div class="muted" style="font-size:12.5px;margin-top:8px">
<div class="muted" style="font-size:var(--fs-sm);margin-top:8px">
上传后<strong>先不写入</strong>:会先给出这份快照能补多少新区域、以及「整体替换」会丢掉多少已有区域,
确认之后才真正导入。
</div>
+4 -4
View File
@@ -13,9 +13,9 @@
<div style="min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px">
<div style="width:100%;max-width:368px">
<div style="text-align:center;margin-bottom:22px">
<div style="font-size:30px">{{.Settings.OwnerAvatar}}</div>
<h1 style="font-size:19px;margin-top:6px">{{.Settings.SiteTitle}}</h1>
<div class="muted" style="font-size:13px">后台管理</div>
<div style="font-size:var(--glyph-xl)">{{.Settings.OwnerAvatar}}</div>
<h1 style="font-size:var(--fs-lg);margin-top:6px">{{.Settings.SiteTitle}}</h1>
<div class="muted" style="font-size:var(--fs-sm)">后台管理</div>
</div>
<div class="card" style="margin:0">
@@ -46,7 +46,7 @@
</script>
</div>
<div style="text-align:center;margin-top:16px"><a class="muted" href="/" style="font-size:13px">← 返回主页</a></div>
<div style="text-align:center;margin-top:16px"><a class="muted" href="/" style="font-size:var(--fs-sm)">← 返回主页</a></div>
</div>
</div>
</body>
+3 -3
View File
@@ -61,7 +61,7 @@
<form id="bulk-form" method="post" action="/admin/notes/bulk"></form>
<div class="card" style="position:sticky;top:8px;z-index:3;display:flex;gap:10px;align-items:center;flex-wrap:wrap">
<label class="check" style="margin:0"><input type="checkbox" id="chk-all"> 全选本页</label>
<span class="muted" style="font-size:12.5px">批量:</span>
<span class="muted" style="font-size:var(--fs-sm)">批量:</span>
<select name="verdict" form="bulk-form" style="padding:5px 8px;border:1px solid var(--line);border-radius:8px">
<option value="1">👍 推荐</option>
<option value="-1">👎 踩雷</option>
@@ -100,7 +100,7 @@
data-verdict-btn data-src="{{.SrcPK}}" data-v="-1" title="标为踩雷">👎</button>
</div>
</form>
<div class="muted" style="font-size:12px;margin-top:4px">
<div class="muted" style="font-size:var(--fs-sm);margin-top:4px">
地点 ID {{.SrcPK}}{{if .VisitCount}} · 到访 {{num .VisitCount}} 次{{end}}
<span class="vote-tip" data-verdict-status="{{.SrcPK}}">{{if eq .Verdict 1}} · 已标推荐{{else if eq .Verdict -1}} · 已标踩雷{{end}}</span>
{{if or .Up .Down}} · 访客 👍{{.Up}} 👎{{.Down}}{{end}}
@@ -171,7 +171,7 @@
<div class="item" style="grid-template-columns:1fr auto;gap:10px;align-items:center">
<div>
<div>{{if .Alias}}{{.Alias}}{{else}}<span class="muted">(无别名)</span>{{end}}{{if .Content}} · <span class="muted">{{.Content}}</span>{{end}}{{if .RondNote}} · <span class="muted">来自 rond:{{.RondNote}}</span>{{end}}</div>
<div class="muted" style="font-size:12px">地点 ID {{.SrcPK}} · 更新于 {{dt .UpdatedAt}}</div>
<div class="muted" style="font-size:var(--fs-sm)">地点 ID {{.SrcPK}} · 更新于 {{dt .UpdatedAt}}</div>
</div>
<form method="post" action="/admin/place/note">
<input type="hidden" name="src_pk" value="{{.SrcPK}}">
+2 -2
View File
@@ -8,7 +8,7 @@
</div>
<div class="card">
<h2 style="font-size:14px;margin:0 0 10px">用户名</h2>
<h2 style="font-size:var(--fs-sm);margin:0 0 10px">用户名</h2>
{{if eq .Flash "name"}}<div class="alert ok">用户名已更新</div>{{end}}
{{if .Err}}<div class="alert err">{{.Err}}</div>{{end}}
<form method="post" action="/admin/username">
@@ -19,7 +19,7 @@
</div>
<div class="card">
<h2 style="font-size:14px;margin:0 0 10px">密码</h2>
<h2 style="font-size:var(--fs-sm);margin:0 0 10px">密码</h2>
{{if and .Flash (ne .Flash "name")}}<div class="alert ok">密码已更新</div>{{end}}
<form method="post" action="/admin/password">
<label class="f"><span>当前密码</span><input type="password" name="current" autocomplete="current-password" required></label>
+2 -2
View File
@@ -36,7 +36,7 @@
一次几十张、几百 MB 都没有上传耗时或存储压力。
</div>
<div class="row" style="align-items:center;margin-bottom:10px">
<span class="muted" style="font-size:12.5px">照片里的坐标是</span>
<span class="muted" style="font-size:var(--fs-sm)">照片里的坐标是</span>
<label class="check" style="margin:0"><input type="radio" name="coord-sel" value="wgs84" checked> WGS-84(绝大多数手机)</label>
<label class="check" style="margin:0"><input type="radio" name="coord-sel" value="gcj02"> 已经是 GCJ-02(部分国产手机会这样写)</label>
</div>
@@ -46,7 +46,7 @@
<div class="row" style="align-items:center;gap:10px">
<label class="btn primary" for="photo-input" style="cursor:pointer">+ 选择照片</label>
<input type="file" id="photo-input" accept="image/*" multiple hidden>
<span class="muted" id="pick-count" style="font-size:12.5px">可多选,JPEG / HEIC 都行</span>
<span class="muted" id="pick-count" style="font-size:var(--fs-sm)">可多选,JPEG / HEIC 都行</span>
</div>
<div id="parse-status" class="alert info"></div>
</div>
+9 -9
View File
@@ -54,7 +54,7 @@
</label>
{{end}}
</div>
<div class="muted" style="font-size:12.5px;margin-top:8px">
<div class="muted" style="font-size:var(--fs-sm);margin-top:8px">
关闭的页面在访客导航中隐藏,直接输入网址也会得到 404。主页不受此开关影响。
</div>
<label class="f" style="max-width:320px;margin-top:14px">
@@ -64,7 +64,7 @@
{{range .Pages}}<option value="{{.Key}}" {{if eq $.Settings.VisitorHome .Key}}selected{{end}}>{{.Name}}</option>{{end}}
</select>
</label>
<div class="muted" style="font-size:12.5px;margin-top:6px">
<div class="muted" style="font-size:var(--fs-sm);margin-top:6px">
访客访问站点根路径时直接落到这里;你登录后访问根路径仍然看主页。
选择了一个没对访客开放的页面时不会跳转(会留在主页)。
</div>
@@ -92,7 +92,7 @@
<label class="check"><input type="checkbox" name="hide_work" {{if .Settings.HideWork}}checked{{end}}> 隐藏类型为「工作」的记录</label>
<label class="check"><input type="checkbox" name="notes_public" {{if .Settings.NotesPublic}}checked{{end}}> 地点备注对访客公开(不勾选则只有你自己能看到)</label>
<label class="check"><input type="checkbox" name="marks_public" {{if .Settings.MarksPublic}}checked{{end}}> 「推荐 / 踩雷」标记与票数对访客公开</label>
<div class="muted" style="font-size:12.5px">
<div class="muted" style="font-size:var(--fs-sm)">
隐藏后匿名访客看不到这两类记录。你登录后可在任意公开页面用筛选器上的
<span class="mono">含家/工作</span> 开关临时显示给自己看。
</div>
@@ -105,10 +105,10 @@
<option value="10" {{if eq .Settings.MinDwellMin 10}}selected{{end}}>少于 10 分钟</option>
</select>
</label>
<div class="muted" style="font-size:12.5px">该设置作用于全站统计与地图,可有效过滤路过点。</div>
<div class="muted" style="font-size:var(--fs-sm)">该设置作用于全站统计与地图,可有效过滤路过点。</div>
</div>
<div class="card">
<div class="card card--quiet">
<h2>地图底图</h2>
<div class="card-sub">在 <span class="mono">conf/app.ini</span> 中配置,改完重启生效</div>
<div class="kv"><span class="k">当前生效</span><span class="v">{{.MapInfo}}</span></div>
@@ -122,20 +122,20 @@
<div class="card-sub" style="margin-top:0">放大后底图自带的 POI 与路名标注太密、看不清自己的点时,换成<b>纯路网</b>。
访客在地图上手动切换过的话,浏览器会记住他自己的选择。</div>
{{end}}
<div class="muted" style="font-size:12.5px;margin-top:8px">
<div class="muted" style="font-size:var(--fs-sm);margin-top:8px">
<div><span class="mono">map_provider</span>:<span class="mono">amap</span>(默认,免密钥)/ <span class="mono">tianditu</span>(需 <span class="mono">map_key</span>)/ <span class="mono">custom</span>(自备 <span class="mono">map_tile_url</span>)</div>
<div><span class="mono">map_key</span>:填写后瓦片改由服务端代理,密钥不会出现在页面源码里。</div>
<div><span class="mono">map_crs</span>:仅 <span class="mono">custom</span> 使用,填 <span class="mono">gcj02</span> 或 <span class="mono">wgs84</span>;站点数据本身是 GCJ-02,换成 WGS-84 底图(天地图 / OSM)时会自动纠偏。</div>
</div>
</div>
<div class="card">
<div class="card card--quiet">
<h2>图片存储</h2>
<div class="card-sub">地点图片存到哪儿。图片在<b>浏览器里</b>转成 WebP 后才上传,服务器只负责收下与转存</div>
<label class="check"><input type="radio" name="media_backend" value="local" {{if ne .Settings.MediaBackend "s3"}}checked{{end}}> 服务器本地磁盘</label>
<label class="check"><input type="radio" name="media_backend" value="s3" {{if eq .Settings.MediaBackend "s3"}}checked{{end}}> 对象存储(S3 兼容,如 Cloudflare R2)</label>
<div class="kv" style="margin-top:8px"><span class="k">对象存储</span><span class="v">{{.S3Info}}</span></div>
<div class="muted" style="font-size:12.5px;margin-top:8px">
<div class="muted" style="font-size:var(--fs-sm);margin-top:8px">
<div>凭据放在 <span class="mono">conf/app.ini</span>,不进数据库、也不会下发到前端:</div>
<div><span class="mono">s3_endpoint</span>(R2 形如 <span class="mono">https://&lt;账号&gt;.r2.cloudflarestorage.com</span>)、<span class="mono">s3_bucket</span>、<span class="mono">s3_access_key</span>、<span class="mono">s3_secret_key</span>、<span class="mono">s3_region</span>(R2 填 <span class="mono">auto</span>)。</div>
<div><span class="mono">s3_public_base</span>:桶的公开域名(自定义域或 r2.dev),拼上对象名就是 <span class="mono">&lt;img src&gt;</span>;不填的话图片存得进去但浏览器取不到。</div>
@@ -152,7 +152,7 @@
<textarea name="track_script" rows="4"
placeholder="https://example.com/analytics.js">{{.Settings.TrackScript}}</textarea>
</label>
<div class="muted" style="font-size:12.5px">
<div class="muted" style="font-size:var(--fs-sm)">
<div>填一个 <span class="mono">https://…</span> 地址会包成 <span class="mono">&lt;script src=…&gt;</span>;
需要初始化代码的服务(Umami、百度统计等)可以把整段 <span class="mono">&lt;script&gt;</span> 贴进来。</div>
<div>整段脚本的 <span class="mono">&lt;script&gt;</span> 与 <span class="mono">&lt;/script&gt;</span> 必须数量相同:
@@ -13,9 +13,9 @@
<div style="min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px">
<div style="width:100%;max-width:368px">
<div style="text-align:center;margin-bottom:22px">
<div style="font-size:30px">🔐</div>
<h1 style="font-size:19px;margin-top:6px">两步验证</h1>
<div class="muted" style="font-size:13px">输入验证器 App 中的 6 位验证码</div>
<div style="font-size:var(--glyph-xl)">🔐</div>
<h1 style="font-size:var(--fs-lg);margin-top:6px">两步验证</h1>
<div class="muted" style="font-size:var(--fs-sm)">输入验证器 App 中的 6 位验证码</div>
</div>
<div class="card" style="margin:0">
@@ -26,13 +26,13 @@
<label class="f"><span>验证码</span>
<input type="text" name="code" inputmode="numeric" pattern="[0-9]*" maxlength="6"
autocomplete="one-time-code" autofocus required
style="text-align:center;font-size:20px;letter-spacing:8px">
style="text-align:center;font-size:var(--glyph-lg);letter-spacing:8px">
</label>
<button class="btn primary" type="submit" style="width:100%;justify-content:center">验证并登录</button>
</form>
</div>
<div style="text-align:center;margin-top:16px"><a class="muted" href="/admin/login" style="font-size:13px">← 重新登录</a></div>
<div style="text-align:center;margin-top:16px"><a class="muted" href="/admin/login" style="font-size:var(--fs-sm)">← 重新登录</a></div>
</div>
</div>
</body>
+4 -4
View File
@@ -18,7 +18,7 @@
<form method="post" action="/admin/twofa/disable" style="max-width:360px">
<label class="f"><span>输入当前验证码以关闭</span>
<input type="text" name="code" inputmode="numeric" maxlength="6" required
style="text-align:center;letter-spacing:6px;font-size:16px">
style="text-align:center;letter-spacing:6px;font-size:var(--fs-control)">
</label>
<button class="btn danger" type="submit">关闭两步验证</button>
</form>
@@ -35,13 +35,13 @@
<input type="text" readonly value="{{.Secret}}" onclick="this.select()"
style="font-family:monospace;letter-spacing:1px">
</label>
<div class="muted" style="font-size:12.5px">账户名:rond-with-you · 算法 SHA1 · 6 位 · 30 秒</div>
<div class="muted" style="font-size:var(--fs-sm)">账户名:rond-with-you · 算法 SHA1 · 6 位 · 30 秒</div>
</div>
</div>
<form method="post" action="/admin/twofa/confirm" style="max-width:360px;margin-top:6px">
<label class="f"><span>输入验证码确认开启</span>
<input type="text" name="code" inputmode="numeric" maxlength="6" required
style="text-align:center;letter-spacing:6px;font-size:16px">
style="text-align:center;letter-spacing:6px;font-size:var(--fs-control)">
</label>
<button class="btn primary" type="submit">确认开启</button>
</form>
@@ -53,7 +53,7 @@
<form method="post" action="/admin/twofa/setup">
<button class="btn primary" type="submit">开始绑定验证器</button>
</form>
<div class="muted" style="font-size:12.5px;margin-top:10px">
<div class="muted" style="font-size:var(--fs-sm);margin-top:10px">
需要一个 TOTP 验证器 App(Google Authenticator、Microsoft Authenticator、1Password、Aegis 等均可)。
</div>
</div>
+14 -10
View File
@@ -16,10 +16,14 @@
</div>
{{if .Settings.Has "overview"}}
{{/* 首屏主数:8 个数字全都在,但只让「去过的地方」占主位,城市/省份降为它下面的一行
上下文。原来 8 个一样大一样重,扫过去不知道先看哪个。 */}}
<div class="kpi-lead">
<div class="kl-v">{{num .Overview.Places}}<small>个</small></div>
<div class="kl-k">去过的地点</div>
<div class="kl-x">走过 {{num .Overview.Cities}} 座城市、{{num .Overview.Provinces}} 个省级行政区</div>
</div>
<div class="kpis" style="margin-bottom:16px">
<div class="kpi"><div class="v">{{num .Overview.Places}}<small>个</small></div><div class="k">去过的地点</div></div>
<div class="kpi"><div class="v">{{num .Overview.Cities}}<small>座</small></div><div class="k">到过的城市</div></div>
<div class="kpi"><div class="v">{{num .Overview.Provinces}}<small>个</small></div><div class="k">覆盖省份</div></div>
<div class="kpi"><div class="v">{{num .Overview.Days}}<small>天</small></div><div class="k">有记录的日子</div></div>
<div class="kpi"><div class="v">{{num .Overview.Visits}}<small>次</small></div><div class="k">累计到访</div></div>
<div class="kpi"><div class="v">{{dur .Overview.DwellMin}}</div><div class="k">累计停留</div></div>
@@ -29,11 +33,11 @@
{{end}}
{{if and (.Settings.Has "map") .Points}}
<div class="card">
<div class="card card--hero">
<h2>足迹地图</h2>
<div class="card-sub">共 {{num (len .Points)}} 个地点{{if .Settings.HideHome}} · 已隐藏「家」{{end}}{{if .Settings.HideWork}} · 已隐藏「工作」{{end}}</div>
<div id="home-map" class="map-preview"></div>
<div style="margin-top:10px;font-size:12.5px" class="muted">滚轮被禁用,点击地图进入全屏模式查看细节与筛选。</div>
<div style="margin-top:10px;font-size:var(--fs-sm)" class="muted">滚轮被禁用,点击地图进入全屏模式查看细节与筛选。</div>
</div>
{{end}}
@@ -46,7 +50,7 @@
{{range .ActivityStats}}
<div class="bar-row">
<div class="nm" title="{{.Name}}">{{with icon .Icon}}{{.}} {{end}}{{.Name}}</div>
<div class="bar-track"><div class="bar-fill" style="width:{{pct .Count $.MaxActivity}}%;background:{{colorof .Color}}"></div></div>
<div class="bar-track"><div class="bar-fill" style="width:{{pct .Count $.MaxActivity}}%;background:{{csscolor .Color}}"></div></div>
<div class="vv">{{num .Count}} 次</div>
</div>
{{end}}
@@ -77,7 +81,7 @@
<h2>月度活跃度</h2>
<div class="card-sub">每月到访次数</div>
<div class="cols">
{{range .Monthly}}<div class="col" style="height:{{.Pct}}%" title="{{.Key}}:{{.Count}} 次"></div>{{end}}
{{range .Monthly}}<div class="col{{if eq .Count $.MaxMonthly}} peak{{end}}" style="height:{{.Pct}}%" title="{{.Key}}:{{.Count}} 次">{{if eq .Count $.MaxMonthly}}<span class="pv">{{num .Count}}</span>{{end}}</div>{{end}}
</div>
<div class="cols-x">{{range .Monthly}}<span>{{.Label}}</span>{{end}}</div>
</div>
@@ -103,7 +107,7 @@
{{range .Transports}}
<div class="bar-row">
<div class="nm">{{with icon .Icon}}{{.}} {{end}}{{.Name}}</div>
<div class="bar-track"><div class="bar-fill" style="width:{{pct .Count $.MaxTransport}}%;background:{{colorof .Color}}"></div></div>
<div class="bar-track"><div class="bar-fill" style="width:{{pct .Count $.MaxTransport}}%;background:{{csscolor .Color}}"></div></div>
<div class="vv">{{num .Count}} 次</div>
</div>
{{end}}
@@ -118,7 +122,7 @@
<div class="chips">
{{range .Tags}}
<a class="chip" href="{{$.TagLink .ID}}" title="{{num .Count}} 次">
<span class="dot" style="background:{{colorof .Color}}"></span>{{.Name}}
<span class="dot" style="background:{{csscolor .Color}}"></span>{{.Name}}
<span class="muted">{{num .Count}}</span>
</a>
{{end}}
@@ -156,7 +160,7 @@
<div class="list">
{{range .Recent}}
<div class="item">
<div class="ic" style="background:{{colorof .Color}}22">{{iconor .Icon "📍"}}</div>
<div class="ic" style="background:{{csssoft .Color}}">{{iconor .Icon "📍"}}</div>
<div>
<div class="t">{{if .PlaceID}}<a href="{{$.PlaceLink .PlaceID}}">{{.PlaceName}}</a>{{else}}未命名地点{{end}}</div>
<div class="m">{{day .Arrival}} · {{hm .Arrival}}{{if .City}} · {{.City}}{{end}}{{if .ActivityName}} · {{.ActivityName}}{{end}}{{if .Tags}} · {{range $i, $t := .Tags}}{{if $i}}、{{end}}<span class="tag">{{$t}}</span>{{end}}{{end}}</div>
+39 -21
View File
@@ -1,11 +1,10 @@
{{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">
{{/* 页面开关只管访客:站长登录后始终能访问全部页面(guardPage 也是这么放的),
导航必须跟着一起放行,否则关掉页面开关后站长自己也只剩「主页」一个入口,
明明能访问却点不到。 */}}
{{/* 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}}
@@ -13,7 +12,17 @@
{{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}}
</div>
{{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>
@@ -26,17 +35,26 @@
</div>
</nav>
<script>
/* 手机上导航装不下这几项,可以横向滑动(滚动条已隐藏)——右侧加一层渐隐,提示「还能往右滑」;
滑到末端就收掉,免得让人以为后面还有。桌面端没有横向溢出,这段等于不做事。 */
/* 1) 620px 以上:链接行万一还是放不下(以后加了入口),横向可滑 + 右缘渐隐提示。
2) 620px 及以下:整行已经收进 .nav-menu,这里只管菜单开合——原生 <details> 不会
点外面自动收、也不响应 Esc,补上;点链接后也收掉,免得盖着刚打开的页面。 */
(function () {
var el = document.querySelector('.nav-links');
if (!el) return;
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);
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}}
@@ -73,7 +91,7 @@
<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:{{colorof $a.Color}}"></span>{{$a.Name}}
<span class="dot" style="background:{{csscolor $a.Color}}"></span>{{$a.Name}}
<span class="muted">{{num $a.Count}}</span>
</a>
{{end}}{{end}}
@@ -84,7 +102,7 @@
<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:{{colorof $a.Color}}"></span>{{$a.Name}}
<span class="dot" style="background:{{csscolor $a.Color}}"></span>{{$a.Name}}
<span class="muted">{{num $a.Count}}</span>
</a>
{{end}}{{end}}
@@ -166,7 +184,7 @@
<div class="card">
<div class="empty">
<div class="big">🗺️</div>
<div style="font-size:15px;color:var(--text);margin-bottom:6px">还没有导入任何数据</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>
+3 -3
View File
@@ -60,7 +60,7 @@
<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}}">
<span class="dot" style="background:{{colorof $a.Color}}"></span>{{$a.Name}}
<span class="dot" style="background:{{csscolor $a.Color}}"></span>{{$a.Name}}
</a>
{{end}}{{end}}
</div>
@@ -69,7 +69,7 @@
<summary>展开其余 {{len .Activities}} 个类型</summary>
<div class="chips">
{{range $i, $a := .Activities}}{{if and (ge $i 12) (not ($.IsAct $a.ID))}}
<a class="chip" href="{{$.ActLink $a.ID}}"><span class="dot" style="background:{{colorof $a.Color}}"></span>{{$a.Name}}</a>
<a class="chip" href="{{$.ActLink $a.ID}}"><span class="dot" style="background:{{csscolor $a.Color}}"></span>{{$a.Name}}</a>
{{end}}{{end}}
</div>
</details>
@@ -136,7 +136,7 @@
<div class="map-legend">
<div style="font-weight:500;color:var(--text-2)">类型分布</div>
{{range .Legend}}
<div class="lg"><i style="background:{{colorof .Color}}"></i>{{.Name}} <span class="muted">{{num .Count}}</span></div>
<div class="lg"><i style="background:{{csscolor .Color}}"></i>{{.Name}} <span class="muted">{{num .Count}}</span></div>
{{end}}
</div>
{{end}}
+2 -2
View File
@@ -82,7 +82,7 @@
<div class="row" style="margin-top:12px;gap:10px">
<label class="btn primary" for="imgs-input" id="imgs-pick" style="cursor:pointer">+ 选择图片</label>
<input type="file" id="imgs-input" accept="image/*" multiple hidden>
<span class="muted" id="imgs-count" style="font-size:12.5px"></span>
<span class="muted" id="imgs-count" style="font-size:var(--fs-sm)"></span>
</div>
<div class="imgs-hint" id="imgs-hint"></div>
</div>
@@ -200,7 +200,7 @@
<div class="list" style="max-height:280px;overflow-y:auto">
{{range .Visits}}
<div class="item" style="grid-template-columns:34px 1fr auto">
<div class="ic" style="background:{{colorof .Color}}22;width:30px;height:30px;font-size:15px">{{iconor .Icon "📍"}}</div>
<div class="ic" style="background:{{csssoft .Color}};width:30px;height:30px;font-size:var(--fs-md)">{{iconor .Icon "📍"}}</div>
<div>
<div class="t" style="font-weight:400">{{day .Arrival}}</div>
<div class="m">{{hm .Arrival}} – {{if .Departure}}{{hm .Departure}}{{else}}进行中{{end}}{{if .Remark}} · {{.Remark}}{{end}}</div>
+4 -4
View File
@@ -16,7 +16,7 @@
<div class="card">
<div class="row" style="margin-bottom:12px">
<span class="muted" style="font-size:12.5px">排序</span>
<span class="muted" style="font-size:var(--fs-sm)">排序</span>
<div class="chips">
<a class="chip {{if eq .Sort ""}}on{{end}}" href="{{.Link .FilterBase "sort" ""}}">到访次数</a>
<a class="chip {{if eq .Sort "dwell"}}on{{end}}" href="{{.Link .FilterBase "sort" "dwell"}}">停留时长</a>
@@ -28,7 +28,7 @@
{{if or .IsAdmin .Settings.MarksPublic}}
<div class="row" style="margin-bottom:12px">
<span class="muted" style="font-size:12.5px">标记</span>
<span class="muted" style="font-size:var(--fs-sm)">标记</span>
<div class="chips">
<a class="chip {{if eq .Verdict 0}}on{{end}}" href="{{.VerdictLink 0}}">全部</a>
<a class="chip {{if eq .Verdict 1}}on{{end}}" href="{{.VerdictLink 1}}">👍 推荐</a>
@@ -55,11 +55,11 @@
{{if .IsHome}}<span class="tag">家</span>{{end}}
{{if .IsWork}}<span class="tag">工作</span>{{end}}
{{if eq .Verdict 1}}<span class="tag vote-up">👍 推荐</span>{{else if eq .Verdict -1}}<span class="tag vote-down">👎 踩雷</span>{{end}}
{{if or .Up .Down}}<span class="muted" style="font-size:12px">👍{{.Up}} · 👎{{.Down}}</span>{{end}}
{{if or .Up .Down}}<span class="muted" style="font-size:var(--fs-sm)">👍{{.Up}} · 👎{{.Down}}</span>{{end}}
</td>
<td class="muted" data-label="城市">{{if .City}}{{.City}}{{else}}—{{end}}</td>
<td data-label="主要类型">
{{if .TopActivity}}<span class="chip" style="padding:1px 8px"><span class="dot" style="background:{{colorof .TopColor}}"></span>{{.TopActivity}}</span>{{else}}<span class="muted">—</span>{{end}}
{{if .TopActivity}}<span class="chip" style="padding:1px 8px"><span class="dot" style="background:{{csscolor .TopColor}}"></span>{{.TopActivity}}</span>{{else}}<span class="muted">—</span>{{end}}
</td>
<td data-label="图片">{{if .HasImage}}<span title="有图片">📷</span>{{else}}<span class="muted" title="还没有上传图片">—</span>{{end}}</td>
<td class="num" data-label="到访">{{num .VisitCount}}</td>
+1 -1
View File
@@ -86,7 +86,7 @@
<tbody>
{{range .Transports}}
<tr>
<td><span class="dot" style="display:inline-block;background:{{colorof .Color}}"></span> {{with icon .Icon}}{{.}} {{end}}{{.Name}}</td>
<td><span class="dot" style="display:inline-block;background:{{csscolor .Color}}"></span> {{with icon .Icon}}{{.}} {{end}}{{.Name}}</td>
<td class="num" data-label="次数">{{num .Count}}</td>
<td class="num" data-label="里程">{{km .Distance}} km</td>
</tr>
+18 -15
View File
@@ -27,14 +27,14 @@
<div class="card" style="margin-bottom:16px">
<div class="row" style="justify-content:space-between;align-items:center;flex-wrap:wrap">
<h2 style="margin:0">推荐 &amp; 踩雷({{if .IsAdmin}}我的结论{{else}}站长结论{{end}})</h2>
<span class="muted" style="font-size:12.5px">推荐 {{num .VoteRec}} · 踩雷 {{num .VoteAvoid}}</span>
<span class="muted" style="font-size:var(--fs-sm)">推荐 {{num .VoteRec}} · 踩雷 {{num .VoteAvoid}}</span>
</div>
<div class="card-sub">
只统计<strong>{{if .IsAdmin}}你自己{{else}}站长{{end}}</strong>在地点页或地图气泡里标的 👍/👎——按坐标键存,重新上传备份也不会丢。
</div>
<div class="grid g2">
<div>
<div class="muted" style="font-size:12.5px;margin-bottom:6px">👍 {{if .IsAdmin}}我{{else}}站长{{end}}推荐的地点</div>
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:6px">👍 {{if .IsAdmin}}我{{else}}站长{{end}}推荐的地点</div>
{{if .Recommended}}
<div class="chips">
{{range .Recommended}}<a class="chip" href="{{$.PlaceLink .ID}}"><span class="dot" style="background:#34c759"></span>{{if .Name}}{{.Name}}{{else}}未命名{{end}}{{if .City}}<span class="muted"> · {{.City}}</span>{{end}}</a>{{end}}
@@ -42,7 +42,7 @@
{{else}}<div class="empty" style="padding:10px 0">还没有标记</div>{{end}}
</div>
<div>
<div class="muted" style="font-size:12.5px;margin-bottom:6px">👎 {{if .IsAdmin}}我{{else}}站长{{end}}踩雷的地点</div>
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:6px">👎 {{if .IsAdmin}}我{{else}}站长{{end}}踩雷的地点</div>
{{if .Avoided}}
<div class="chips">
{{range .Avoided}}<a class="chip" href="{{$.PlaceLink .ID}}"><span class="dot" style="background:#ff3b30"></span>{{if .Name}}{{.Name}}{{else}}未命名{{end}}{{if .City}}<span class="muted"> · {{.City}}</span>{{end}}</a>{{end}}
@@ -56,14 +56,14 @@
<div class="card" style="margin-bottom:16px">
<div class="row" style="justify-content:space-between;align-items:center;flex-wrap:wrap">
<h2 style="margin:0">访客点赞</h2>
<span class="muted" style="font-size:12.5px">👍 {{num .VoteUp}} 票 · 👎 {{num .VoteDown}} 票</span>
<span class="muted" style="font-size:var(--fs-sm)">👍 {{num .VoteUp}} 票 · 👎 {{num .VoteDown}} 票</span>
</div>
<div class="card-sub">
访客在地点页点的 👍/👎,一人一票、可改可撤。与上面的站长结论<strong>互不影响</strong>:各存各的表,谁也不会覆盖谁。
</div>
<div class="grid g2">
<div>
<div class="muted" style="font-size:12.5px;margin-bottom:6px">👍 被点赞最多的地点</div>
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:6px">👍 被点赞最多的地点</div>
{{if .VisitorUp}}
<div class="chips">
{{range .VisitorUp}}<a class="chip" href="{{$.PlaceLink .ID}}"><span class="dot" style="background:#34c759"></span>{{if .Name}}{{.Name}}{{else}}未命名{{end}}<span class="muted"> · 👍 {{.Up}}{{if .Down}} · 👎 {{.Down}}{{end}}</span></a>{{end}}
@@ -71,7 +71,7 @@
{{else}}<div class="empty" style="padding:10px 0">还没有访客点赞</div>{{end}}
</div>
<div>
<div class="muted" style="font-size:12.5px;margin-bottom:6px">👎 被点踩最多的地点</div>
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:6px">👎 被点踩最多的地点</div>
{{if .VisitorDown}}
<div class="chips">
{{range .VisitorDown}}<a class="chip" href="{{$.PlaceLink .ID}}"><span class="dot" style="background:#ff3b30"></span>{{if .Name}}{{.Name}}{{else}}未命名{{end}}<span class="muted"> · 👎 {{.Down}}{{if .Up}} · 👍 {{.Up}}{{end}}</span></a>{{end}}
@@ -84,10 +84,11 @@
<div class="card">
<h2>月度分布</h2>
<div class="card-sub">每月到访次数,最高 {{num .MaxMonthly}} 次</div>
<div class="card-sub">每月到访次数 · 点柱条只看那个月</div>
<div class="cols">
{{/* 点柱条直接筛出那个月;title 是悬停读数 */}}
{{range .Monthly}}<a class="col" href="{{$.MonthLink .Key}}" title="{{.Key}}:{{.Count}} 次到访。点一下只看这个月" style="height:{{.Pct}}%"></a>{{end}}
{{/* 点柱条直接筛出那个月;title 是悬停读数。最高的那根在柱顶标出次数(.pv),
副标题里就不再重复写一遍「最高 N 次」。 */}}
{{range .Monthly}}<a class="col{{if eq .Count $.MaxMonthly}} peak{{end}}" href="{{$.MonthLink .Key}}" title="{{.Key}}:{{.Count}} 次到访。点一下只看这个月" style="height:{{.Pct}}%">{{if eq .Count $.MaxMonthly}}<span class="pv">{{num .Count}}</span>{{end}}</a>{{end}}
</div>
<div class="cols-x">{{range .Monthly}}<span>{{.Label}}</span>{{end}}</div>
</div>
@@ -100,7 +101,7 @@
{{range .ActivityStats}}
<a class="bar-row" href="{{$.ActLink .ID}}" title="{{.Name}}:{{num .Count}} 次,累计 {{dur .Dwell}}。点一下按这个类型筛选">
<div class="nm">{{with icon .Icon}}{{.}} {{end}}{{.Name}}</div>
<div class="bar-track"><div class="bar-fill" style="width:{{pct .Count $.MaxActivity}}%;background:{{colorof .Color}}"></div></div>
<div class="bar-track"><div class="bar-fill" style="width:{{pct .Count $.MaxActivity}}%;background:{{csscolor .Color}}"></div></div>
<div class="vv">{{num .Count}} 次 · {{dur .Dwell}}</div>
</a>
{{end}}
@@ -111,10 +112,12 @@
<h2>一天中的分布</h2>
<div class="card-sub">按到访开始时间统计</div>
<div class="cols" style="height:130px">
{{range $h, $c := .Hourly}}<div class="col" style="height:{{pct $c $.MaxHourly}}%" title="{{$h}} 时:{{$c}} 次"></div>{{end}}
{{range $h, $c := .Hourly}}<div class="col{{if eq $c $.MaxHourly}} peak{{end}}" style="height:{{pct $c $.MaxHourly}}%" title="{{$h}} 时:{{$c}} 次"></div>{{end}}
</div>
<div class="cols-x">
{{range $h, $c := .Hourly}}<span>{{if eq $h 0}}0{{else if eq $h 6}}6{{else if eq $h 12}}12{{else if eq $h 18}}18{{else}}&nbsp;{{end}}</span>{{end}}
{{/* 24 根柱光看高度分不出上午/下午:换成一行 6 小时一段的标尺,段边界与柱子严格
对齐(同一套 24 列网格),每段直接把时段与小时区间写出来,就不必再留坐标轴。 */}}
<div class="cols-band">
<span>深夜 0-6</span><span>上午 6-12</span><span>下午 12-18</span><span>晚上 18-24</span>
</div>
</div>
</div>
@@ -181,7 +184,7 @@
<tbody>
{{range .Transports}}
<tr>
<td><span class="dot" style="display:inline-block;background:{{colorof .Color}}"></span> {{with icon .Icon}}{{.}} {{end}}{{.Name}}</td>
<td><span class="dot" style="display:inline-block;background:{{csscolor .Color}}"></span> {{with icon .Icon}}{{.}} {{end}}{{.Name}}</td>
<td class="num" data-label="次数">{{num .Count}}</td>
<td class="num" data-label="里程">{{km .Distance}} km</td>
<td class="num" data-label="在途时间">{{dur .DwellMin}}</td>
@@ -202,7 +205,7 @@
<div class="bars">
{{range .Tags}}
<a class="bar-row" href="{{$.TagLink .ID}}" title="#{{.Name}}:{{num .Count}} 次到访。点一下按这个标签筛选">
<div class="nm"><span class="dot" style="display:inline-block;background:{{colorof .Color}}"></span> {{.Name}}</div>
<div class="nm"><span class="dot" style="display:inline-block;background:{{csscolor .Color}}"></span> {{.Name}}</div>
<div class="bar-track"><div class="bar-fill" style="width:{{pct .Count $.MaxTag}}%"></div></div>
<div class="vv">{{num .Count}} 次</div>
</a>
+2 -2
View File
@@ -29,11 +29,11 @@
{{range .Items}}
<div class="tl-item">
<div class="tm">{{hm .Arrival}}</div>
<div class="ln"><i style="background:{{colorof .Color}}"></i></div>
<div class="ln"><i style="background:{{csscolor .Color}}"></i></div>
<div class="bd">
<div>
{{if .PlaceID}}<a href="{{$.PlaceLink .PlaceID}}">{{if .PlaceName}}{{.PlaceName}}{{else}}未命名地点{{end}}</a>{{else}}未命名地点{{end}}
{{if .ActivityName}}<span class="chip" style="padding:1px 8px;font-size:11.5px"><span class="dot" style="background:{{colorof .Color}}"></span>{{.ActivityName}}</span>{{end}}
{{if .ActivityName}}<span class="chip" style="padding:1px 8px;font-size:var(--fs-xs)"><span class="dot" style="background:{{csscolor .Color}}"></span>{{.ActivityName}}</span>{{end}}
{{range .Tags}}<span class="tag">{{.}}</span>{{end}}
</div>
<div class="meta">
+1 -1
View File
@@ -49,7 +49,7 @@
<tr>
<td>{{if .Name}}{{.Name}}{{else}}<span class="muted">(未命名)</span>{{end}}</td>
<td class="muted" data-label="城市">{{.City}}</td>
<td class="muted" data-label="类型">{{if .Act}}<span class="dot" style="background:{{colorof .Color}}"></span>{{.Act}}{{end}}</td>
<td class="muted" data-label="类型">{{if .Act}}<span class="dot" style="background:{{csscolor .Color}}"></span>{{.Act}}{{end}}</td>
<td class="num" data-label="到访">{{num .Count}}</td>
<td class="num muted" data-label="停留">{{dur .Dwell}}</td>
<td class="muted mono" data-label="最近一次">{{.LastAt}}</td>
+36 -7
View File
@@ -7,6 +7,7 @@ import (
"html/template"
"net/http"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
@@ -521,6 +522,8 @@ type HomeData struct {
MaxCity int
MaxTransport int
MaxTag int
// MaxMonthly 是月度柱图里的最高值,用来在图上标出峰值那一根(首页与统计页口径一致)。
MaxMonthly int
}
type MapData struct {
@@ -1337,16 +1340,41 @@ func transportColorNames() []string {
return out
}
func colorOf(name string) string {
if c, ok := appleColors[name]; ok {
return c
// cssColor 给模板的内联 style 用,返回 CSS 变量名——只有走变量,配色才能跟着暗色模式走。
// 变量定义在 app.css 的 :root 里,名字就是 appleColors 的键,两边必须同步增删。
//
// 那还需要「真实色值」的地方怎么办:导出/下发的 JSON(地图点)带的是原始色名,
// 由 rond-map.js 自己解析——**那份调色板是故意独立的**:地图瓦片在暗色下保持浅色,
// 所以标记必须用浅色底上那套值,不能跟着页面的暗色变量走。
//
// ⚠️ 返回值必须是 template.CSS:html/template 的 CSS 上下文过滤器**不认识 var()**,
// 普通 string 会被判成不安全、替换成 ZgotmplZ(实测踩过,页面上的圆点全变成透明)。
// 而一旦挂了 template.CSS 就等于关掉这道过滤,所以色值必须先过 hexColorRe,
// 只有「# + 6 位十六进制」才原样放行 —— 否则 `#"><img…` 这种能直接逃出 style 属性。
func cssColor(name string) template.CSS {
if _, ok := appleColors[name]; ok {
return template.CSS("var(--cat-" + name + ")")
}
if len(name) == 7 && name[0] == '#' {
return name
if hexColorRe.MatchString(name) {
return template.CSS(name)
}
return "#5b6169"
return "var(--cat-none)"
}
// cssColorSoft 是同一色的浅底(emoji 方块那种)。自定义色值没有对应变量,
// 退回「hex + 22」这个 8 位带 alpha 的写法——它是合法 CSS,同样先过严格校验。
func cssColorSoft(name string) template.CSS {
if _, ok := appleColors[name]; ok {
return template.CSS("var(--cat-" + name + "-soft)")
}
if hexColorRe.MatchString(name) {
return template.CSS(name + "22")
}
return "var(--cat-none-soft)"
}
var hexColorRe = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
// iconOf 只保留 emoji 图标。rond 里的图标多为 SF Symbols 名称(如 house.fill),
// 在网页上无法渲染,直接返回空串让模板退回默认样式。
func iconOf(s string) string {
@@ -1424,7 +1452,8 @@ func templateFuncs() template.FuncMap {
}
return b
},
"colorof": colorOf,
"csscolor": cssColor,
"csssoft": cssColorSoft,
"icon": iconOf,
"iconor": iconOr,
"condition": stats.ConditionName,