Files
pear-admin-flask/templates/public/base.html
T
bwstudio 4ece453f00 feat(public): 首页卡片/主题菜单/侧栏/移动端多项优化
卡片
- 描述与网址共用一行(.meta-line + grid 同格),默认显示描述、悬停交叉淡出为网址;
  触屏 (hover:none) 直接显示网址,卡片不再被两行文字撑高

主题菜单
- 修复「暗色」项与其他项不一致:主题变量块原用裸属性选择器 [data-theme="dark"],
  会连菜单里的 <li data-theme="dark"> 一起命中导致颜色被局部覆盖;
  全部改为 html[data-theme="…"](8 个主题 + 关于页 dark 特例)
- 每项加统一色块标识 + 选中对勾,宽高与对齐方式一致

左侧菜单栏
- 「我常访问」「热门网址」合并为一个「常用 & 热门」分区(侧栏 + 移动端抽屉),
  标题随数据动态切换;右侧内容区两个 section 保持独立不变
- addSideNavEntry/addDrawerEntry 改为往合并分区插 li,removeEntries 只摘单条,
  避免整块删除把热门入口一起干掉
- 新增 retitleFeatured():JS 补入/移除个人榜后同步标题与图标

PC 侧栏
- 修复滚轮无响应:overscroll-behavior 由 contain 改为 auto(contain 会阻断滚动链接,
  内容未溢出时侧栏上滚轮完全没反应)
- 滚动条加宽到 6px 并使用 --text-soft,加 scrollbar-gutter: stable

移动端顶栏
- 修复 360px 小屏三个控件挤成两行:.topbar-actions 改 flex: 1 1 0 + min-width: 0
  (flex 换行先于收缩,basis auto 会先按固有宽度判定放不下而换行)
- 恢复显示站点名与副标题作为第二行,消除顶部空白
- 手机端隐藏「搜索」按钮,改由左侧放大镜提交,input 加 enterkeyhint
- placeholder 按视口自适应:手机「搜索书签」

抽屉
- 主题菜单在窄屏改为内联展开(280px 抽屉放不下 160px fixed 浮层,
  原逻辑会算出负数 left 飞出屏幕)
- positionThemeMenu 增加边界兜底:两侧都放不下时贴可视区右边缘
- 面板补 width: min(280px,84vw) + overflow-y:auto(原被内容撑成 173px)
2026-09-07 20:38:06 +08:00

2023 lines
89 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="theme-color" content="#16baaa">
<meta name="format-detection" content="telephone=no">
<title>{% block title %}{{ site_name|default('旺珂 · 导航') }}{% endblock %}</title>
<meta name="description" content="旺珂 公开导航聚合页:精选开发工具、AI、设计资源、效率工具等优质站点。">
<meta property="og:title" content="{{ site_name|default('旺珂 · 导航') }}">
<meta property="og:description" content="精选优质站点,按分类聚合展示。">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/layui@2.9.7/dist/css/layui.css">
{# 首屏防闪烁:根据 localStorage / 系统偏好立即设置 data-theme #}
<script>
(function(){
try {
var saved = localStorage.getItem('bw-theme');
// 有效值:'default'、'sunset'、'forest'、'dark'、'auto'
if (saved === 'auto') {
var isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'default');
} else if (saved === 'default' || saved === 'sunset' || saved === 'forest' || saved === 'dark') {
document.documentElement.setAttribute('data-theme', saved);
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark');
}
} catch (e) { /* localStorage 不可用时跳过 */ }
})();
</script>
<style>
/* ===== 主题变量 =====
* 默认主题(清新蓝绿):无 data-theme 属性 / data-theme="default"
* 4 种可切换主题:
* default 蓝绿渐变(默认)
* sunset 暖橙 + 玫红
* forest 墨绿 + 青绿
* dark 深色背景
*/
:root, html[data-theme="default"] {
--brand: #16baaa;
--brand-2: #2e8de8;
--bg: #f5f7fa;
--card-bg: #ffffff;
--text: #2c3e50;
--text-muted: #6c7a89;
--text-soft: #b0bac3;
--border: #e8eef0;
--topbar-text: #ffffff;
--shadow: 0 2px 12px rgba(0,0,0,0.06);
--shadow-hover: 0 8px 24px rgba(22, 186, 170, 0.18);
}
/* 暮色橙(暖色系) */
html[data-theme="sunset"] {
--brand: #ff7043;
--brand-2: #e91e63;
--bg: #fff8f3;
--card-bg: #ffffff;
--text: #3e2723;
--text-muted: #8d6e63;
--text-soft: #bcaaa4;
--border: #ffe0d0;
--topbar-text: #ffffff;
--shadow: 0 2px 12px rgba(255,112,67,0.10);
--shadow-hover: 0 8px 24px rgba(255,112,67,0.25);
}
/* 森林绿(自然系) */
html[data-theme="forest"] {
--brand: #2e7d32;
--brand-2: #00897b;
--bg: #f3f8f4;
--card-bg: #ffffff;
--text: #1b3a1f;
--text-muted: #5a7260;
--text-soft: #9bb3a0;
--border: #d8e6da;
--topbar-text: #ffffff;
--shadow: 0 2px 12px rgba(46,125,50,0.08);
--shadow-hover: 0 8px 24px rgba(46,125,50,0.22);
}
/* 暗色(深色背景) */
html[data-theme="dark"] {
--brand: #16baaa;
--brand-2: #2e8de8;
--bg: #0f1419;
--card-bg: #1a1f29;
--text: #e6edf3;
--text-muted: #8b949e;
--text-soft: #6e7681;
--border: #2d333b;
--topbar-text: #ffffff;
--shadow: 0 2px 12px rgba(0,0,0,0.4);
--shadow-hover: 0 8px 24px rgba(22, 186, 170, 0.25);
}
/* 紫韵(优雅紫色) */
html[data-theme="lavender"] {
--brand: #8b5cf6;
--brand-2: #ec4899;
--bg: #faf5ff;
--card-bg: #ffffff;
--text: #2e1065;
--text-muted: #6d28d9;
--text-soft: #a78bfa;
--border: #e9d5ff;
--topbar-text: #ffffff;
--shadow: 0 2px 12px rgba(139,92,246,0.10);
--shadow-hover: 0 8px 24px rgba(139,92,246,0.25);
}
/* 深海(深蓝 navy */
html[data-theme="ocean"] {
--brand: #0ea5e9;
--brand-2: #06b6d4;
--bg: #f0f9ff;
--card-bg: #ffffff;
--text: #0c4a6e;
--text-muted: #0369a1;
--text-soft: #7dd3fc;
--border: #bae6fd;
--topbar-text: #ffffff;
--shadow: 0 2px 12px rgba(14,165,233,0.10);
--shadow-hover: 0 8px 24px rgba(14,165,233,0.28);
}
/* 青空(明亮天蓝) */
html[data-theme="sky"] {
--brand: #38bdf8;
--brand-2: #3b82f6;
--bg: #f0f9ff;
--card-bg: #ffffff;
--text: #0c4a6e;
--text-muted: #0284c7;
--text-soft: #7dd3fc;
--border: #e0f2fe;
--topbar-text: #ffffff;
--shadow: 0 2px 12px rgba(59,130,246,0.10);
--shadow-hover: 0 8px 24px rgba(59,130,246,0.28);
}
/* 玫红(热情玫红) */
html[data-theme="rose"] {
--brand: #f43f5e;
--brand-2: #ec4899;
--bg: #fff1f2;
--card-bg: #ffffff;
--text: #4a040c;
--text-muted: #be123c;
--text-soft: #fda4af;
--border: #fecdd3;
--topbar-text: #ffffff;
--shadow: 0 2px 12px rgba(244,63,94,0.10);
--shadow-hover: 0 8px 24px rgba(244,63,94,0.28);
}
/* 系统暗色兜底:未设置 data-theme 时跟随系统 */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--brand: #16baaa;
--brand-2: #2e8de8;
--bg: #0f1419;
--card-bg: #1a1f29;
--text: #e6edf3;
--text-muted: #8b949e;
--text-soft: #6e7681;
--border: #2d333b;
--shadow: 0 2px 12px rgba(0,0,0,0.4);
--shadow-hover: 0 8px 24px rgba(22, 186, 170, 0.25);
}
}
/* ===== 布局变量:容器宽度 / 侧栏宽度 / 卡片最小宽度 =====
* 默认 1200px 居中;宽屏按 1440 / 1700 / 2000 / 2560 四档逐级放大,
* 避免 2K、4K 下内容缩在中间一小条。
*/
:root {
--container-max: 1200px;
--side-w: 220px;
--card-min: 220px;
--layout-gap: 24px;
}
@media (min-width: 1440px) {
:root { --container-max: 1360px; --side-w: 232px; --card-min: 196px; --layout-gap: 28px; }
}
@media (min-width: 1700px) {
:root { --container-max: 1600px; --side-w: 244px; --card-min: 250px; --layout-gap: 32px; }
}
@media (min-width: 2000px) {
:root { --container-max: 1880px; --side-w: 258px; --card-min: 200px; --layout-gap: 36px; }
}
@media (min-width: 2560px) {
:root { --container-max: 2240px; --side-w: 276px; --card-min: 210px; --layout-gap: 44px; }
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 15px;
line-height: 1.5;
transition: background-color 0.2s, color 0.2s;
}
/* ===== Topbar ===== */
.topbar {
background: linear-gradient(135deg, var(--brand) 0%, var(--brand-2) 100%);
color: var(--topbar-text);
padding: 56px 0 40px;
position: relative;
}
.topbar-inner {
max-width: 100%;
margin: 0 auto;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
flex-wrap: wrap;
}
.topbar-brand { flex: 1 1 auto; min-width: 220px; }
.topbar-actions {
display: flex;
align-items: center;
gap: 14px;
flex: 0 1 auto;
justify-content: flex-end;
}
/* 顶栏右侧主题切换按钮(让原本藏在侧栏的入口在顶栏也可见,填充右侧空白) */
.top-theme-trigger { position: relative; }
.top-theme-toggle {
width: 44px; height: 44px;
border-radius: 50%;
border: 1px solid rgba(255,255,255,0.4);
background: rgba(255,255,255,0.18);
color: #fff; font-size: 20px;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
transition: background .2s;
}
.top-theme-toggle:hover { background: rgba(255,255,255,0.30); }
.topbar h1 { font-size: 36px; margin: 0 0 8px; font-weight: 600; line-height: 1.2; }
.topbar .sub { font-size: 15px; opacity: 0.92; }
/* 宽屏:标题与内边距同步放大,避免头重脚轻 */
@media (min-width: 1700px) {
.topbar { padding: 68px 0 52px; }
.topbar h1 { font-size: 44px; }
.topbar .sub { font-size: 17px; }
.site-search { max-width: 560px; }
/* 卡片描述放宽到 2 行,提升信息密度 */
.card .desc { -webkit-line-clamp: 2; min-height: 40px; }
}
@media (min-width: 2560px) {
.topbar { padding: 84px 0 64px; }
.topbar h1 { font-size: 52px; }
.site-search { max-width: 880px; }
}
/* 手机端分类入口按钮:默认桌面隐藏 */
.cat-trigger-mobile {
display: none;
background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.35);
color: var(--topbar-text);
border-radius: 10px;
width: 44px; height: 44px;
align-items: center; justify-content: center;
font-size: 20px;
cursor: pointer;
flex-shrink: 0;
}
.cat-trigger-mobile:active { background: rgba(255,255,255,0.32); }
/* ===== 主题下拉菜单 ===== */
.theme-picker-trigger { position: relative; }
.theme-menu {
display: none;
position: fixed;
top: 0;
left: 0;
min-width: 160px;
margin: 0; padding: 4px;
list-style: none;
background: var(--card-bg);
color: var(--text);
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: var(--shadow-hover);
z-index: 50;
}
.theme-menu.open { display: block; }
.theme-menu li {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
font-size: 14px;
border-radius: 6px;
cursor: pointer;
transition: background 0.12s;
color: var(--text);
}
/* 每项都带一枚同规格色块,保证「暗色」这类深底项不会视觉上落单 */
.theme-menu li::before {
content: '';
width: 12px; height: 12px;
border-radius: 50%;
flex-shrink: 0;
background: var(--sw, var(--brand));
box-shadow: 0 0 0 1px rgba(0,0,0,0.12) inset;
}
.theme-menu li[data-theme="default"] { --sw: #16baaa; }
.theme-menu li[data-theme="sunset"] { --sw: #ff7043; }
.theme-menu li[data-theme="forest"] { --sw: #2e7d32; }
.theme-menu li[data-theme="dark"] { --sw: #1a1f29; }
.theme-menu li[data-theme="lavender"] { --sw: #8b5cf6; }
.theme-menu li[data-theme="ocean"] { --sw: #0ea5e9; }
.theme-menu li[data-theme="sky"] { --sw: #38bdf8; }
.theme-menu li[data-theme="rose"] { --sw: #f43f5e; }
.theme-menu li[data-theme="auto"] { --sw: linear-gradient(135deg, #f5f7fa 50%, #1a1f29 50%); }
.theme-menu li:hover { background: rgba(22,186,170,0.10); }
.theme-menu li.active { background: rgba(22,186,170,0.18); font-weight: 600; }
.theme-menu li.active::after {
content: '✓';
margin-left: auto;
color: var(--brand);
font-weight: 700;
}
/* ===== 顶部全局搜索 ===== */
.site-search {
position: relative;
max-width: 460px;
flex: 1 1 320px;
margin-top: 0;
display: flex;
align-items: center;
background: rgba(255,255,255,0.95);
border-radius: 999px;
padding: 4px 6px 4px 18px;
box-shadow: 0 6px 20px rgba(0,0,0,0.12);
transition: box-shadow 0.2s;
}
.site-search:focus-within { box-shadow: 0 8px 28px rgba(0,0,0,0.18); }
.site-search i {
color: var(--text-muted);
font-size: 18px;
margin-right: 10px;
}
.site-search input[type="search"] {
flex: 1;
border: none;
background: transparent;
font-size: 15px;
color: var(--text);
outline: none;
padding: 10px 0;
min-width: 0;
}
.site-search input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
.site-search input[type="search"]::placeholder { color: #9aa5b1; }
.site-search .search-btn {
padding: 8px 22px;
background: linear-gradient(135deg, var(--brand) 0%, var(--brand-2) 100%);
color: #fff;
border: none;
border-radius: 999px;
font-size: 14px;
cursor: pointer;
transition: opacity 0.2s;
}
.site-search .search-btn:hover { opacity: 0.9; }
.container { max-width: 100%; margin: 0 auto; padding: 0 24px; }
/* ===== 分类目录条(横向滚动) ===== */
.cat-nav {
background: var(--card-bg);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 10;
backdrop-filter: saturate(180%) blur(8px);
-webkit-backdrop-filter: saturate(180%) blur(8px);
}
.cat-nav-inner {
max-width: 100%;
margin: 0 auto;
padding: 8px 24px;
display: flex;
gap: 6px;
overflow-x: auto;
scrollbar-width: thin;
scroll-behavior: smooth;
}
.cat-nav-inner::-webkit-scrollbar { height: 4px; }
.cat-nav-inner::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.cat-nav a {
flex-shrink: 0;
padding: 6px 14px;
border-radius: 999px;
font-size: 13px;
color: var(--text-muted);
text-decoration: none;
transition: all 0.15s;
white-space: nowrap;
}
.cat-nav a:hover { background: rgba(22,186,170,0.1); color: var(--brand); }
.cat-nav a.active { background: var(--brand); color: #fff; }
/* ===== 移动端分类 FAB + 抽屉 ===== */
.cat-fab {
display: none; /* 桌面隐藏 */
position: fixed;
right: 14px;
bottom: 76px; /* 留出回到顶部按钮的位置 */
padding: 10px 14px;
background: var(--brand);
color: #fff;
border: none;
border-radius: 999px;
font-size: 14px;
font-weight: 600;
box-shadow: 0 6px 16px rgba(22,186,170,0.35);
z-index: 25;
cursor: pointer;
white-space: nowrap;
}
.cat-fab:active { transform: scale(0.96); }
.cat-drawer {
display: none;
position: fixed;
inset: 0;
z-index: 30;
background: rgba(0,0,0,0.5);
}
.cat-drawer.open { display: block; }
/* 抽屉打开时锁定背景滚动 */
body.drawer-open { overflow: hidden; }
/* 搜索无结果空态 */
.search-empty {
text-align: center;
color: var(--text-muted);
padding: 64px 16px;
font-size: 15px;
}
.search-empty .ico { font-size: 40px; display: block; margin-bottom: 12px; opacity: 0.6; }
.cat-drawer-panel {
position: absolute;
left: 0; right: 0; bottom: 0;
background: var(--card-bg);
border-radius: 16px 16px 0 0;
padding: 16px 16px 28px;
max-height: 70vh;
overflow-y: auto;
transform: translateY(100%);
transition: transform 0.25s ease;
}
.cat-drawer.open .cat-drawer-panel { transform: translateY(0); }
.cat-drawer-head {
display: flex; align-items: center; justify-content: space-between;
font-size: 15px; font-weight: 600; color: var(--text);
margin-bottom: 14px;
}
.cat-drawer-head .close {
background: none; border: none; font-size: 22px; color: var(--text-muted); cursor: pointer;
}
.cat-drawer-list { list-style: none; padding: 0; margin: 0; }
.cat-drawer-list li a {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 8px;
border-bottom: 1px solid var(--border);
color: var(--text);
text-decoration: none;
font-size: 15px;
}
.cat-drawer-list li:last-child a { border-bottom: none; }
.cat-drawer-list li a:active { background: rgba(22,186,170,0.08); }
.cat-drawer-list li a .arrow { color: var(--text-muted); font-size: 13px; }
/* ===== 布局:左分类侧栏 + 右内容区 ===== */
.layout {
display: flex;
align-items: flex-start;
gap: var(--layout-gap);
}
.content { flex: 1 1 auto; min-width: 0; padding-top: 8px; }
.side-nav {
flex: 0 0 var(--side-w);
width: var(--side-w);
position: sticky;
top: 20px;
max-height: calc(100vh - 40px);
overflow-y: auto;
/* 不用 containcontain 会阻断滚动链接,侧栏滚到底(或内容没超出)时
鼠标在侧栏上滚轮会完全没反应,表现为"上下滚动没有呼应"。
保持默认的 auto,滚到底后自然接力给页面滚动。 */
overscroll-behavior: auto;
/* 预留滚动条位置,出现滚动条时内容不横向抖动 */
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: var(--text-soft) transparent;
padding: 8px 6px 20px 0;
/* 关键:侧栏本身是 sticky,会生成独立层叠上下文。
若不提升层级,内部弹出的主题菜单(z-index:50)会被锁在该上下文里,
而内容区卡片图标带 z-index:1,反而会画到菜单之上。提到 20 即可压过内容区。 */
z-index: 20;
}
/* 滚动条要看得见,否则内容被截断时用户不知道还能往下滚 */
.side-nav::-webkit-scrollbar { width: 6px; }
.side-nav::-webkit-scrollbar-track { background: transparent; }
.side-nav::-webkit-scrollbar-thumb {
background: var(--text-soft);
border-radius: 3px;
}
.side-nav::-webkit-scrollbar-thumb:hover { background: var(--brand); }
.side-block {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: var(--shadow);
padding: 13px 10px;
margin-bottom: 14px;
}
.side-title {
display: flex; align-items: center; gap: 6px;
font-size: 12px; font-weight: 600;
color: var(--text-muted);
letter-spacing: 0.5px;
padding: 0 8px 8px;
margin-bottom: 4px;
border-bottom: 1px solid var(--border);
}
.side-title i { color: var(--brand); font-size: 14px; }
.side-list { list-style: none; margin: 0; padding: 4px 0 0; }
.side-list li a {
display: flex; align-items: center; gap: 9px;
padding: 8px 10px;
border-radius: 8px;
font-size: 14px;
color: var(--text);
text-decoration: none;
transition: background 0.15s, color 0.15s;
}
.side-list li a:hover { background: rgba(22,186,170,0.10); color: var(--brand); }
.side-list li a.active {
background: rgba(22,186,170,0.16);
color: var(--brand);
font-weight: 600;
box-shadow: inset 3px 0 0 var(--brand);
}
.side-list li a .side-ico {
color: var(--brand); font-size: 16px;
width: 18px; text-align: center; flex-shrink: 0;
}
.side-list li a .side-txt {
flex: 1; min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.side-list li a .side-count {
font-size: 11px; color: var(--text-soft); flex-shrink: 0;
}
/* 搜索过滤:分类无命中时整项隐藏 */
.side-list li.hidden-by-search { display: none; }
/* 侧栏特殊项:我常访问(紫)/ 热门(橙) */
.side-list a.side-mine, .side-list a.side-mine .side-ico { color: #8b5cf6; }
.side-list a.side-mine:hover, .side-list a.side-mine.active {
background: rgba(139,92,246,0.14); box-shadow: inset 3px 0 0 #8b5cf6;
}
.side-list a.side-hot, .side-list a.side-hot .side-ico { color: #ff6b35; }
.side-list a.side-hot:hover, .side-list a.side-hot.active {
background: rgba(255,107,53,0.13); box-shadow: inset 3px 0 0 #ff6b35;
}
/* 桌面(≥1025px):用左侧侧栏替代顶部横条 */
@media (min-width: 1025px) {
.cat-nav { display: none; }
}
/* 平板/手机(≤1024px):侧栏收起,回到顶部横条 + 抽屉 */
@media (max-width: 1024px) {
.layout { display: block; }
.side-nav { display: none; }
/* 顶部横条是 sticky 的,锚点跳转要留出它的高度 */
.section { scroll-margin-top: 64px; }
}
/* ===== Section ===== */
.section { margin: 32px 0; scroll-margin-top: 24px; }
.section-header {
display: flex;
align-items: baseline;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
border-left: 4px solid var(--brand);
padding-left: 12px;
}
.section-header h2 { font-size: 20px; margin: 0; font-weight: 600; }
.section-header .meta { font-size: 13px; color: var(--text-muted); }
/* ===== Grid ===== */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--card-min), 1fr));
gap: 16px;
}
.card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 16px;
text-decoration: none;
color: inherit;
box-shadow: var(--shadow);
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
display: flex; align-items: center; gap: 12px;
min-width: 0;
}
.card:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-hover);
border-color: var(--brand);
}
.card:active { transform: translateY(0); }
/* ===== 热门网址卡片:与普通卡片区分 ===== */
.section-hot { margin-top: 8px; }
.section-hot .section-header h2 i { color: #ff6b35; margin-right: 2px; }
.card-top {
border-color: rgba(22, 186, 170, 0.35);
background: linear-gradient(135deg, var(--card-bg) 0%, rgba(22, 186, 170, 0.05) 100%);
position: relative;
}
.card-top::before {
content: '';
position: absolute; top: 0; left: 0;
width: 3px; height: 100%;
background: var(--brand);
border-radius: 10px 0 0 10px;
}
.card-top:hover {
border-color: var(--brand);
box-shadow: 0 6px 20px rgba(22, 186, 170, 0.18);
}
.cat-nav-inner a.cat-nav-hot {
color: #ff6b35;
font-weight: 600;
}
/* ===== 我常访问(个人榜):紫色系,与热门的橙色区分 ===== */
.section-mine { margin-top: 8px; }
.section-mine .section-header h2 i { color: #8b5cf6; margin-right: 2px; }
.section-mine .section-header .meta a { color: var(--text-muted); text-decoration: underline; }
.section-mine .section-header .meta a:hover { color: #8b5cf6; }
.card-mine {
border-color: rgba(139, 92, 246, 0.35);
background: linear-gradient(135deg, var(--card-bg) 0%, rgba(139, 92, 246, 0.06) 100%);
position: relative;
}
.card-mine::before {
content: '';
position: absolute; top: 0; left: 0;
width: 3px; height: 100%;
background: #8b5cf6;
border-radius: 10px 0 0 10px;
}
.card-mine:hover {
border-color: #8b5cf6;
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.18);
}
.card-mine .visit-count { color: #8b5cf6; }
.cat-nav-inner a.cat-nav-mine { color: #8b5cf6; font-weight: 600; }
.cat-drawer-list a.cat-drawer-mine,
.cat-drawer-list a.cat-drawer-mine .cat-icon { color: #8b5cf6; font-weight: 600; }
.cat-drawer-list a.cat-drawer-hot,
.cat-drawer-list a.cat-drawer-hot .cat-icon { color: #ff6b35; font-weight: 600; }
.card .icon-wrap {
position: relative;
width: 42px; height: 42px;
border-radius: 10px;
overflow: hidden;
flex-shrink: 0;
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.05);
background: var(--card-bg);
}
.card .nav-letter {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
color: #fff; font-weight: 700; font-size: 18px;
text-transform: uppercase;
user-select: none;
letter-spacing: 0.5px;
}
.card .nav-favicon {
position: absolute; inset: 0;
width: 100%; height: 100%;
object-fit: contain;
background: #fff;
z-index: 1;
}
.card-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.card .title-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
.card .title {
font-size: 15px; font-weight: 600; color: var(--text);
flex: 1; min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.card .ext-tag {
flex-shrink: 0;
font-size: 12px; color: var(--brand-2);
background: rgba(46, 141, 232, 0.08);
padding: 1px 6px;
border-radius: 4px;
}
.card .desc {
font-size: 13px; color: var(--text-muted);
line-height: 1.5;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
min-height: 20px;
}
/* 描述与网址共用同一行:默认显示描述,悬停时交叉淡出为网址。
两者用 grid 叠在同一格,卡片高度不再被两行文字撑开。 */
.card .meta-line {
display: grid;
min-height: 20px;
margin-top: 2px;
}
.card .meta-line > .desc,
.card .meta-line > .url {
grid-area: 1 / 1;
min-width: 0;
align-self: center;
transition: opacity 0.18s ease;
}
.card .meta-line > .url {
opacity: 0;
pointer-events: none;
}
.card:hover .meta-line > .desc,
.card:focus-visible .meta-line > .desc { opacity: 0; }
.card:hover .meta-line > .url,
.card:focus-visible .meta-line > .url { opacity: 1; }
.card .url {
font-size: 12px; color: var(--brand-2);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
direction: rtl;
text-align: left;
}
/* 触屏设备没有 hover,直接展示网址,避免描述永远盖住它 */
@media (hover: none) {
.card .meta-line > .desc { opacity: 0; }
.card .meta-line > .url { opacity: 1; }
}
.card .visit-count {
margin-top: auto;
font-size: 11px;
color: var(--text-muted);
text-align: right;
letter-spacing: 0.3px;
opacity: 0.75;
padding-top: 2px;
}
.card-arrow {
flex-shrink: 0;
color: var(--text-soft);
font-size: 14px;
display: flex; align-items: center;
}
.empty {
text-align: center; padding: 48px 0; color: var(--text-muted);
}
/* ===== 关于页样式 ===== */
.card-section {
background: var(--card-bg);
border-radius: 12px;
padding: 28px 32px;
box-shadow: var(--shadow);
margin-bottom: 20px;
}
.about-header h1 {
margin: 0 0 8px;
font-size: 24px;
color: var(--text);
}
.icp-line {
font-size: 13px;
color: var(--text-muted);
}
.icp-line a {
color: var(--text-muted);
text-decoration: none;
}
.icp-line a:hover { color: var(--brand); }
.beian-shield { margin-right: 4px; }
.about-content { line-height: 1.8; color: var(--text); margin-top: 12px; }
.about-content h1,
.about-content h2,
.about-content h3 {
margin: 18px 0 10px;
color: var(--text);
font-weight: 600;
}
.about-content h1 { font-size: 22px; }
.about-content h2 { font-size: 19px; }
.about-content h3 { font-size: 16px; }
.about-content p { margin: 8px 0; }
.about-content code {
background: rgba(22,186,170,0.08);
padding: 1px 6px;
border-radius: 3px;
font-family: Consolas, monospace;
font-size: 13px;
color: var(--brand);
}
.about-content pre {
background: rgba(0,0,0,0.04);
padding: 12px 16px;
border-radius: 6px;
overflow-x: auto;
}
html[data-theme="dark"] .about-content pre { background: rgba(255,255,255,0.06); }
.about-content blockquote {
border-left: 4px solid var(--brand);
margin: 12px 0;
padding: 4px 16px;
background: rgba(22,186,170,0.06);
color: var(--text-muted);
}
.about-content ul, .about-content ol { padding-left: 24px; }
.about-content table {
border-collapse: collapse;
width: 100%;
margin: 12px 0;
}
.about-content table th,
.about-content table td {
border: 1px solid var(--border);
padding: 8px 12px;
}
.contact-grid h2,
.donate-block h2 {
font-size: 17px;
margin: 16px 0 12px;
color: var(--text);
border-left: 3px solid var(--brand);
padding-left: 10px;
}
.contact-grid ul {
list-style: none;
padding: 0;
margin: 0;
}
.contact-grid ul li {
padding: 6px 0;
font-size: 14px;
color: var(--text);
border-bottom: 1px dashed var(--border);
}
.contact-grid ul li:last-child { border-bottom: none; }
.contact-grid ul li b { color: var(--brand); margin-right: 6px; }
.donate-block { text-align: center; padding-bottom: 12px; }
.donate-block .donate-img {
max-width: 220px;
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px;
background: #fff;
}
.donate-block .layui-btn-warm { background: #ff7043; }
.about-footer-meta {
margin-top: 20px;
padding-top: 12px;
border-top: 1px solid var(--border);
font-size: 12px;
color: var(--text-soft);
text-align: center;
}
footer.site-footer {
margin-top: 48px; padding: 24px 0;
text-align: center; color: var(--text-muted); font-size: 13px;
border-top: 1px solid var(--border);
}
footer.site-footer a { color: var(--brand); text-decoration: none; }
footer.site-footer .footer-privacy { font-size: 11px; color: var(--text-muted); margin-top: 4px; opacity: 0.8; }
/* ===== 回到顶部 ===== */
.to-top {
position: fixed;
right: 20px; bottom: 20px;
width: 42px; height: 42px;
background: var(--brand);
color: #fff;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
box-shadow: var(--shadow-hover);
opacity: 0;
transform: translateY(20px);
transition: all 0.2s;
pointer-events: none;
z-index: 20;
border: none;
font-size: 18px;
}
.to-top.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
/* ===== 响应式:平板 ===== */
@media (max-width: 900px) {
.grid { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
.topbar h1 { font-size: 30px; }
.site-search { max-width: 100%; }
}
/* ===== 响应式:手机(OneNav 风格:顶部图标入口 + 左侧分类抽屉 + 分组列表) ===== */
@media (max-width: 600px) {
body { font-size: 14px; }
.container { padding: 0 14px; }
.topbar { padding: 10px 0 12px; }
.topbar-inner {
padding: 0 14px;
display: flex;
align-items: center;
gap: 8px;
}
/* 手机顶栏:第一行 = 分类 + 搜索 + 主题;第二行 = 站点名 + 副标题。
注意 flex 的换行发生在收缩之前,所以 .topbar-actions 必须用
flex-basis: 0 + min-width: 0;否则它会先按搜索框的固有宽度(约 280px)
判定"放不下"而整块换行,之后再缩小——表现就是三个控件挤成两行。 */
.cat-trigger-mobile {
display: inline-flex;
width: 38px; height: 38px;
font-size: 18px;
order: 1;
}
.topbar-actions { flex: 1 1 0; min-width: 0; order: 2; gap: 8px; }
.topbar-brand {
display: flex;
align-items: baseline;
flex-wrap: nowrap;
gap: 8px;
flex: 1 1 100%;
min-width: 0;
order: 3;
margin-top: 8px;
}
.topbar h1 {
font-size: 17px; margin: 0; line-height: 1.3;
white-space: nowrap; flex-shrink: 0;
}
.topbar .sub {
font-size: 12px; opacity: 0.9; line-height: 1.3;
min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 顶部搜索框更紧凑,占满剩余空间 */
.site-search {
flex: 1 1 auto;
min-width: 0;
margin-top: 0;
max-width: none;
padding: 2px 2px 2px 12px;
}
.site-search i { font-size: 16px; margin-right: 8px; cursor: pointer; }
.site-search input[type="search"] { font-size: 14px; padding: 8px 0; width: 100%; }
/* 手机端靠键盘的「搜索」键或左侧放大镜提交,把横向空间全留给输入框 */
.site-search .search-btn { display: none; }
.top-theme-trigger { flex-shrink: 0; }
.top-theme-toggle { width: 38px; height: 38px; font-size: 17px; }
/* 手机端:横向分类 Tab 隐藏(用抽屉替代) */
.cat-nav { display: none; }
.section { margin: 18px 0; scroll-margin-top: 16px; }
.section-header { padding-left: 10px; margin-bottom: 12px; }
.section-header h2 { font-size: 16px; }
.section-header .meta { font-size: 12px; }
/* 手机端:卡片改成两列网格 */
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.card {
padding: 11px 12px;
border-radius: 10px;
gap: 9px;
align-items: flex-start;
}
.card .icon-wrap { width: 32px; height: 32px; border-radius: 8px; flex-shrink: 0; }
.card .nav-letter { font-size: 14px; }
.card-body { gap: 2px; min-width: 0; }
.card .title { font-size: 13px; line-height: 1.3; }
.card .desc {
font-size: 11px;
min-height: 0;
-webkit-line-clamp: 2;
line-height: 1.35;
}
.card .url { font-size: 10px; }
.card .visit-count { font-size: 10px; padding-top: 1px; }
.card-arrow { display: none; } /* 二列空间紧,隐藏右箭头 */
.ext-tag { display: none; } /* 隐藏外链标记,让标题空间更大 */
/* 手机端:分类抽屉替代 FAB */
.cat-fab { display: none !important; }
.to-top { right: 12px; bottom: 12px; width: 38px; height: 38px; }
/* 分类抽屉样式 */
.cat-drawer-panel {
left: 0; right: auto; top: 0; bottom: 0;
border-radius: 0 16px 16px 0;
/* 必须给显式宽度:只写 max-width 时面板会被内容撑成一条窄条(实测 173px)。 */
width: min(280px, 84vw);
max-width: none;
max-height: 100vh;
overflow-y: auto;
overscroll-behavior: contain;
transform: translateX(-100%);
padding: 20px 16px 28px;
}
.cat-drawer.open .cat-drawer-panel { transform: translateX(0); }
.cat-drawer-list li a {
justify-content: flex-start !important;
gap: 12px;
font-size: 15px;
padding: 14px 10px;
}
.cat-drawer-list li a .cat-icon {
display: inline-block;
color: var(--brand);
font-size: 18px;
width: 24px;
text-align: center;
flex-shrink: 0;
}
.cat-drawer-list li a .arrow { margin-left: auto; }
/* 抽屉宽 280px,主题菜单(min-width 160px)作为 fixed 浮层无论贴左还是贴右
都会被挤出可视区,所以手机端改成在「切换主题」父项下方内联展开。 */
.cat-drawer .theme-menu {
position: static !important;
top: auto !important;
left: auto !important;
width: auto;
min-width: 0;
max-width: none;
margin: 4px 10px 10px;
background: var(--bg);
box-shadow: none;
}
/* 抽屉分区 */
.cat-drawer-section { margin-bottom: 18px; }
.cat-drawer-title {
display: flex; align-items: center; gap: 6px;
font-size: 12px; font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 0 4px 8px;
border-bottom: 1px solid var(--border);
margin-bottom: 6px;
}
.cat-drawer-title i { color: var(--brand); font-size: 14px; }
}
</style>
{% block extra_head %}{% endblock %}
</head>
<body>
<header class="topbar">
<div class="container topbar-inner">
{# 手机端分类入口按钮(OneNav 风格) #}
<button type="button" class="cat-trigger-mobile" id="cat-trigger-mobile" aria-label="打开分类" title="分类">
<i class="layui-icon layui-icon-spread-left"></i>
</button>
<div class="topbar-brand">
<h1>{{ site_name|default('旺珂 · 导航') }}</h1>
<div class="sub">{% block subtitle %}精选优质站点,按分类聚合展示 · 共 {{ total|default(0) }} 个链接{% endblock %}</div>
</div>
<div class="topbar-actions">
{# 全局搜索框(OneNav 风格) #}
<form class="site-search" id="site-search" action="{{ url_for('public_nav.index') }}" method="get" role="search">
{# 手机端会隐藏右侧「搜索」按钮,改由这个放大镜图标提交 #}
<i class="layui-icon layui-icon-search" id="search-icon" role="button" tabindex="0" aria-label="提交搜索"></i>
<input type="search" name="q" id="search-input" placeholder="输入书签关键词进行搜索" autocomplete="off" enterkeyhint="search" aria-label="搜索书签">
<button type="submit" class="search-btn" aria-label="搜索">搜索</button>
</form>
{# 顶栏主题切换:把原本藏在侧栏/抽屉的入口放到顶栏右侧,填充空白 #}
<div class="theme-picker-trigger top-theme-trigger">
<button type="button" class="top-theme-toggle" id="top-theme-toggle" aria-label="切换主题" title="切换主题">
<i class="layui-icon layui-icon-light"></i>
</button>
<ul class="theme-menu" id="top-theme-menu">
<li data-theme="default">蓝绿</li>
<li data-theme="sunset">暮色</li>
<li data-theme="forest">森林</li>
<li data-theme="dark">暗色</li>
<li data-theme="lavender">紫韵</li>
<li data-theme="ocean">深海</li>
<li data-theme="sky">青空</li>
<li data-theme="rose">玫红</li>
<li data-theme="auto">跟随系统</li>
</ul>
</div>
</div>
</div>
</header>
{% block cat_nav %}{% endblock %}
{# 首页时锚点直接指向本页 section;其他页面(关于/友链/分类详情)跳回首页对应位置 #}
{% set is_home = request.endpoint in ['public_nav.index', 'index.index'] %}
{% set home_url = url_for('public_nav.index') %}
{% set anchor_base = '#' if is_home else home_url ~ '#' %}
<div class="container layout">
{# ===== 左侧分类侧栏:桌面常驻(≥1025px);平板/手机隐藏,改用顶部横条 + 抽屉 ===== #}
<aside class="side-nav" id="side-nav" aria-label="分类导航"{% if is_home %} data-home="1"{% endif %}>
{# 「我常访问」与「热门网址」合并为同一大类,右侧内容区仍保持两个独立分区 #}
{% if (my_navs is defined and my_navs) or (top_navs is defined and top_navs) %}
<div class="side-block side-mine-block side-hot-block">
<div class="side-title">
<i class="layui-icon {% if my_navs is defined and my_navs %}layui-icon-star-fill{% else %}layui-icon-fire{% endif %}"></i>
{% if (my_navs is defined and my_navs) and (top_navs is defined and top_navs) -%}
常用 &amp; 热门
{%- elif my_navs is defined and my_navs -%}
我的
{%- else -%}
热门
{%- endif %}
</div>
<ul class="side-list">
{% if my_navs is defined and my_navs %}
<li><a href="{{ anchor_base }}cat-mine" class="side-mine" data-side-anchor="cat-mine">
<i class="layui-icon side-ico layui-icon-star-fill"></i>
<span class="side-txt">我常访问</span>
</a></li>
{% endif %}
{% if top_navs is defined and top_navs %}
<li><a href="{{ anchor_base }}cat-top" class="side-hot" data-side-anchor="cat-top">
<i class="layui-icon side-ico layui-icon-fire"></i>
<span class="side-txt">热门网址</span>
</a></li>
{% endif %}
</ul>
</div>
{% endif %}
<div class="side-block" id="side-cat-block">
<div class="side-title"><i class="layui-icon layui-icon-list"></i> 书签分类</div>
<ul class="side-list" id="side-cat-list">
{% for c in nav_categories() %}
<li>
<a href="{{ anchor_base }}{{ c.anchor }}" data-side-anchor="{{ c.anchor }}" title="{{ c.name }}"{% if category is defined and category == c.name %} class="active"{% endif %}>
<i class="layui-icon side-ico {{ c.icon }}"></i>
<span class="side-txt">{{ c.name }}</span>
<span class="side-count"></span>
</a>
</li>
{% endfor %}
</ul>
</div>
<div class="side-block">
<div class="side-title"><i class="layui-icon layui-icon-link"></i> 站点页面</div>
<ul class="side-list">
<li><a href="{{ home_url }}">
<i class="layui-icon side-ico layui-icon-home"></i>
<span class="side-txt">首页</span></a></li>
<li><a href="{{ url_for('public_about.about') }}">
<i class="layui-icon side-ico layui-icon-about"></i>
<span class="side-txt">关于本站</span></a></li>
<li><a href="{{ url_for('public_friend.friend_index') }}">
<i class="layui-icon side-ico layui-icon-website"></i>
<span class="side-txt">友情链接</span></a></li>
<li><a href="/system/passport/login">
<i class="layui-icon side-ico layui-icon-password"></i>
<span class="side-txt">登录后台</span></a></li>
<li class="theme-picker-trigger">
<a href="javascript:void(0)" id="side-theme-toggle">
<i class="layui-icon side-ico layui-icon-light"></i>
<span class="side-txt">切换主题</span></a>
<ul class="theme-menu" id="side-theme-menu">
<li data-theme="default">蓝绿</li>
<li data-theme="sunset">暮色</li>
<li data-theme="forest">森林</li>
<li data-theme="dark">暗色</li>
<li data-theme="lavender">紫韵</li>
<li data-theme="ocean">深海</li>
<li data-theme="sky">青空</li>
<li data-theme="rose">玫红</li>
<li data-theme="auto">跟随系统</li>
</ul>
</li>
</ul>
</div>
</aside>
<main class="content">
<div id="search-empty" class="search-empty" hidden></div>
{% block content %}{% endblock %}
</main>
</div>
<footer class="site-footer">
<div>© 2026 旺珂 · 基于 <a href="https://gitea.bwhome.top/bwadmin/pear-admin-flask">Pear Admin Flask</a> 构建</div>
<div class="footer-privacy">本站使用匿名统计:会生成一个随机匿名 ID 用于记住你的常用网址,不关联任何个人信息,可在「我常访问」里一键清除。</div>
</footer>
<button class="to-top" id="to-top" aria-label="回到顶部" title="回到顶部"></button>
{# 移动端分类 FAB + 抽屉(桌面被 CSS 隐藏,无需各页面写) #}
<button type="button" class="cat-fab" id="cat-fab" aria-haspopup="dialog" aria-controls="cat-drawer">
📂 分类 (<span id="cat-fab-count">0</span>)
</button>
<div class="cat-drawer" id="cat-drawer" role="dialog" aria-modal="true" aria-label="导航抽屉">
<div class="cat-drawer-panel">
<div class="cat-drawer-head">
<span>导航菜单</span>
<button type="button" class="close" id="cat-drawer-close" aria-label="关闭">×</button>
</div>
{# 「我常访问」+「热门网址」合并为同一分区,与左侧侧栏保持一致 #}
{% if (my_navs is defined and my_navs) or (top_navs is defined and top_navs) %}
<div class="cat-drawer-section cat-drawer-featured">
<div class="cat-drawer-title">
<i class="layui-icon {% if my_navs is defined and my_navs %}layui-icon-star-fill{% else %}layui-icon-fire{% endif %}"></i>
{% if (my_navs is defined and my_navs) and (top_navs is defined and top_navs) -%}
常用 &amp; 热门
{%- elif my_navs is defined and my_navs -%}
我的
{%- else -%}
热门
{%- endif %}
</div>
<ul class="cat-drawer-list">
{% if my_navs is defined and my_navs %}
<li>
<a href="{{ anchor_base }}cat-mine" class="cat-drawer-mine">
<i class="layui-icon cat-icon layui-icon-star-fill"></i>
<span>我常访问</span>
<span class="arrow"></span>
</a>
</li>
{% endif %}
{% if top_navs is defined and top_navs %}
<li>
<a href="{{ anchor_base }}cat-top" class="cat-drawer-hot">
<i class="layui-icon cat-icon layui-icon-fire"></i>
<span>热门网址</span>
<span class="arrow"></span>
</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
{# 分类区 #}
<div class="cat-drawer-section">
<div class="cat-drawer-title"><i class="layui-icon layui-icon-list"></i> 书签分类</div>
<ul class="cat-drawer-list" id="cat-drawer-list">
{% for c in nav_categories() %}
<li>
<a href="{{ anchor_base }}{{ c.anchor }}" data-side-anchor="{{ c.anchor }}">
<i class="layui-icon cat-icon {{ c.icon }}"></i>
<span>{{ c.name }}</span>
<span class="arrow"></span>
</a>
</li>
{% endfor %}
</ul>
</div>
{# 其他页面入口(关于 / 友链 / 后台 / 主题) #}
<div class="cat-drawer-section">
<div class="cat-drawer-title"><i class="layui-icon layui-icon-link"></i> 站点页面</div>
<ul class="cat-drawer-list cat-drawer-extras">
<li><a href="{{ url_for('public_nav.index') }}">
<i class="layui-icon cat-icon layui-icon-home"></i>
<span>首页</span>
<span class="arrow"></span></a></li>
<li><a href="{{ url_for('public_about.about') }}">
<i class="layui-icon cat-icon layui-icon-about"></i>
<span>关于本站</span>
<span class="arrow"></span></a></li>
<li><a href="{{ url_for('public_friend.friend_index') }}">
<i class="layui-icon cat-icon layui-icon-website"></i>
<span>友情链接</span>
<span class="arrow"></span></a></li>
<li><a href="/system/passport/login">
<i class="layui-icon cat-icon layui-icon-password"></i>
<span>登录后台</span>
<span class="arrow"></span></a></li>
<li class="theme-picker-trigger">
<a href="javascript:void(0)" id="drawer-theme-toggle">
<i class="layui-icon cat-icon layui-icon-light"></i>
<span>切换主题</span>
<span class="arrow"></span></a>
<ul class="theme-menu" id="drawer-theme-menu">
<li data-theme="default">蓝绿</li>
<li data-theme="sunset">暮色</li>
<li data-theme="forest">森林</li>
<li data-theme="dark">暗色</li>
<li data-theme="lavender">紫韵</li>
<li data-theme="ocean">深海</li>
<li data-theme="sky">青空</li>
<li data-theme="rose">玫红</li>
<li data-theme="auto">跟随系统</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
{% block extra_js %}{% endblock %}
<script>
// ===== 主题切换:多主题选择 + localStorage 持久化 + 跟随系统 =====
(function(){
var THEME_KEY = 'bw-theme';
var FIXED_THEMES = ['default', 'sunset', 'forest', 'dark', 'lavender', 'ocean', 'sky', 'rose'];
var ALL = FIXED_THEMES.concat(['auto']);
function isSystemDark(){
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
// 把"auto"解析为实际生效的主题
function resolveTheme(name){
if (name === 'auto') return isSystemDark() ? 'dark' : 'default';
return ALL.indexOf(name) !== -1 ? name : 'default';
}
// 应用主题(persist=true 表示记住用户选择)
function applyTheme(name, persist){
if (ALL.indexOf(name) === -1) name = 'default';
var resolved = resolveTheme(name);
document.documentElement.setAttribute('data-theme', resolved);
// 顶部 meta 颜色随主题变化(暗色用深色,其他用对应品牌色)
var themeColor = (resolved === 'dark') ? '#0f1419' :
(resolved === 'lavender') ? '#8b5cf6' :
(resolved === 'ocean') ? '#0ea5e9' :
(resolved === 'sky') ? '#38bdf8' :
(resolved === 'rose') ? '#f43f5e' : '#16baaa';
var meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute('content', themeColor);
// 高亮菜单项
document.querySelectorAll('.theme-menu li').forEach(function(li){
if (li.getAttribute('data-theme') === name) li.classList.add('active');
else li.classList.remove('active');
});
if (persist) localStorage.setItem(THEME_KEY, name);
}
// 加载:localStorage 优先;未保存则跟随系统;若无系统信息则 default
var saved = localStorage.getItem(THEME_KEY);
if (saved && ALL.indexOf(saved) !== -1) {
applyTheme(saved, false);
} else if (isSystemDark()) {
applyTheme('auto', false); // 等价于 'dark',但记录 auto 让用户看到"跟随系统"高亮
localStorage.setItem(THEME_KEY, 'auto'); // 顺手持久化
} else {
applyTheme('default', false);
}
// 注:主题切换入口在侧栏(#side-theme-toggle)与抽屉(#drawer-theme-toggle),下方单独绑定
// 跟随系统:监听系统暗色偏好变化,自动重应用
if (window.matchMedia) {
var mq = window.matchMedia('(prefers-color-scheme: dark)');
var listener = function(){
var cur = localStorage.getItem(THEME_KEY) || 'default';
if (cur === 'auto') applyTheme('auto', false);
};
if (mq.addEventListener) mq.addEventListener('change', listener);
else if (mq.addListener) mq.addListener(listener); // 旧 Safari 兼容
}
// 回到顶部
var top = document.getElementById('to-top');
if (top) {
window.addEventListener('scroll', function(){
if (window.scrollY > 400) top.classList.add('visible');
else top.classList.remove('visible');
});
top.addEventListener('click', function(){
window.scrollTo({top: 0, behavior: 'smooth'});
});
}
// 目录高亮当前 section:左侧侧栏(桌面)/ 顶部横条(平板)同步高亮
(function(){
var allLinks = [].slice.call(
document.querySelectorAll('.cat-nav a[href^="#"], .side-nav a[href^="#"]')
);
if (!allLinks.length) return;
var map = {}, sections = [];
allLinks.forEach(function(a){
var id = (a.getAttribute('href') || '').split('#').pop();
var el = id ? document.getElementById(id) : null;
if (!el) return;
if (!map[id]) { map[id] = []; sections.push({id: id, el: el}); }
map[id].push(a);
});
if (!sections.length) return;
function onScroll(){
var y = window.scrollY + 120;
var activeId = sections[0].id;
sections.forEach(function(s){
// 用 rect 计算,兼容 section 位于 flex 子容器内的情况
var top = s.el.getBoundingClientRect().top + window.scrollY;
if (top <= y) activeId = s.id;
});
allLinks.forEach(function(a){ a.classList.remove('active'); });
(map[activeId] || []).forEach(function(a){ a.classList.add('active'); });
// 侧栏自身可滚动时,保证激活项可见
var activeLink = (map[activeId] || [])[0];
if (activeLink && activeLink.closest) {
var side = activeLink.closest('.side-nav');
if (side && side.scrollHeight > side.clientHeight) {
var lr = activeLink.getBoundingClientRect();
var sr = side.getBoundingClientRect();
if (lr.top < sr.top) side.scrollTop -= (sr.top - lr.top) + 8;
else if (lr.bottom > sr.bottom) side.scrollTop += (lr.bottom - sr.bottom) + 8;
}
}
}
window.addEventListener('scroll', onScroll, {passive: true});
window.addEventListener('resize', onScroll);
onScroll();
})();
// ===== 移动端分类抽屉 =====
(function(){
var drawer = document.getElementById('cat-drawer');
var list = document.getElementById('cat-drawer-list');
var closeBtn = document.getElementById('cat-drawer-close');
var triggerMobile = document.getElementById('cat-trigger-mobile');
var fab = document.getElementById('cat-fab');
var countEl = document.getElementById('cat-fab-count');
if (!drawer || !list) return;
function openDrawer(){
drawer.classList.add('open');
document.body.classList.add('drawer-open'); // 锁背景滚动
}
function closeDrawer(){
drawer.classList.remove('open');
document.body.classList.remove('drawer-open');
}
if (triggerMobile) triggerMobile.addEventListener('click', openDrawer);
if (fab) fab.addEventListener('click', openDrawer);
if (closeBtn) closeBtn.addEventListener('click', closeDrawer);
// 点遮罩空白处关闭(不在 panel 内即可)
drawer.addEventListener('click', function(e){
if (e.target === drawer) closeDrawer();
});
// 如果列表已由模板渲染(带 cat_meta),只需绑定关闭事件
var existingItems = list.querySelectorAll('li a[href^="#"]');
existingItems.forEach(function(link){
link.addEventListener('click', closeDrawer);
});
// 兜底:没有模板渲染时,从 .cat-nav 同步
if (!existingItems.length) {
var navLinks = document.querySelectorAll('.cat-nav a[href^="#"]');
if (countEl) countEl.textContent = navLinks.length;
navLinks.forEach(function(a){
var li = document.createElement('li');
var link = document.createElement('a');
link.href = a.getAttribute('href');
link.textContent = a.textContent;
var arrow = document.createElement('span');
arrow.className = 'arrow';
arrow.textContent = '→';
link.appendChild(arrow);
li.appendChild(link);
list.appendChild(li);
link.addEventListener('click', closeDrawer);
});
}
})();
// ===== 导航卡片点击埋点:sendBeacon,不影响原 href 跳转 =====
(function(){
function bindNavClick(card){
if (!card || card.__navBound) return;
card.__navBound = true;
card.addEventListener('click', function(){
var navId = card.getAttribute('data-nav-id');
if (!navId) return;
// 本机 localStorage 立即 +1,「我常访问」下次进页面就生效
try { if (window.__wkMineBump) window.__wkMineBump(navId); } catch(e) {}
try {
if (navigator.sendBeacon) {
navigator.sendBeacon('/site/nav/' + navId + '/click');
} else {
// 兜底:fetch keepalive POST
fetch('/site/nav/' + navId + '/click', {method:'POST', keepalive:true, credentials:'same-origin'});
}
} catch(e) { /* 静默失败,不影响跳转 */ }
});
}
function bindAll(){
document.querySelectorAll('.card[data-nav-id]').forEach(bindNavClick);
}
window.__wkBindNavClick = bindNavClick;
window.__wkBindNavAll = bindAll;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bindAll);
} else {
bindAll();
}
})();
// 卡片图标增强:统一走本地缓存接口 /site/icon/<domain>。
// 服务端负责拉取并落盘到 data/icon/,拉取失败的自动返回「站点名称首字母」SVG,
// 因此既有真实 favicon、也不会再出现空白图标,且后续不再重复拉取。
window.enhanceFavicons = function(root){
var scope = root || document;
var imgs = scope.querySelectorAll('img.nav-favicon');
for (var i = 0; i < imgs.length; i++){
var img = imgs[i];
if (img.getAttribute('data-fav-wired')) continue;
img.setAttribute('data-fav-wired', '1');
var domain = img.getAttribute('data-domain');
if (!domain) continue;
var letter = img.getAttribute('data-letter') || '';
var url = '/site/icon/' + encodeURIComponent(domain);
if (letter) url += '?letter=' + encodeURIComponent(letter);
// 接口始终返回图片:成功为 favicon,失败为字母头像 SVG
img.onerror = function(){ img.style.display = 'none'; };
img.src = url;
img.style.display = 'block';
}
};
// ===== 「我常访问」:localStorage 本地累计 + 服务端匿名榜合并 =====
(function(){
var KEY = 'wk_mine_clicks';
var MIN_ITEMS = 2; // 冷启动:至少点过 2 个不同网址才展示
var MAX_ITEMS = 8;
function load(){
try { return JSON.parse(localStorage.getItem(KEY) || '{}') || {}; }
catch(e) { return {}; }
}
function save(m){
try { localStorage.setItem(KEY, JSON.stringify(m)); } catch(e) {}
}
window.__wkMineBump = function(navId){
if (!navId) return;
var m = load();
m[navId] = (m[navId] || 0) + 1;
save(m);
};
function findSourceCard(id){
var all = document.querySelectorAll('.card[data-nav-id="' + id + '"]');
for (var i = 0; i < all.length; i++) {
if (!all[i].closest('#cat-mine')) return all[i];
}
return null;
}
function addCatNavEntry(){
var inner = document.getElementById('cat-nav-inner');
if (!inner || inner.querySelector('.cat-nav-mine')) return;
var a = document.createElement('a');
a.href = '#cat-mine';
a.className = 'cat-nav-mine';
a.textContent = '★ 我常访问';
inner.insertBefore(a, inner.firstChild);
}
// 「我常访问」是 JS 事后补的:服务端渲染那一刻 my_navs 可能为空,
// 标题只会写成「热门」。补/删条目后要把标题和图标跟着纠正。
function retitleFeatured(el, text, icon){
if (!el) return;
var ico = el.querySelector('i');
el.textContent = text;
if (!ico) return;
ico.className = 'layui-icon ' + icon;
el.insertBefore(ico, el.firstChild);
}
function addDrawerEntry(){
var panel = document.querySelector('.cat-drawer-panel');
if (!panel || panel.querySelector('.cat-drawer-mine')) return;
// 「常用 & 热门」合并分区:往里插一条,别重建 section(会顶掉热门入口)
var host = panel.querySelector('.cat-drawer-featured .cat-drawer-list');
if (host) {
var href = '#cat-mine';
var sample = host.querySelector('a[href]');
if (sample) href = (sample.getAttribute('href') || '#cat-mine').replace(/cat-top$/, 'cat-mine');
var li = document.createElement('li');
li.innerHTML =
'<a href="' + href + '" class="cat-drawer-mine">' +
'<i class="layui-icon cat-icon layui-icon-star-fill"></i>' +
'<span>我常访问</span><span class="arrow">→</span></a>';
host.insertBefore(li, host.firstChild);
var dsec = host.closest('.cat-drawer-section');
retitleFeatured(dsec && dsec.querySelector('.cat-drawer-title'),
'常用 & 热门', 'layui-icon-star-fill');
return;
}
// 兜底:连合并分区都没有时才新建
var sec = document.createElement('div');
sec.className = 'cat-drawer-section cat-drawer-featured';
sec.innerHTML =
'<div class="cat-drawer-title"><i class="layui-icon layui-icon-star-fill"></i> 常用 &amp; 热门</div>' +
'<ul class="cat-drawer-list"><li>' +
'<a href="#cat-mine" class="cat-drawer-mine">' +
'<i class="layui-icon cat-icon layui-icon-star-fill"></i>' +
'<span>我常访问</span><span class="arrow">→</span></a></li></ul>';
panel.insertBefore(sec, panel.children[1] || null);
}
// 左侧侧栏里补一个「我常访问」入口(桌面端)
function addSideNavEntry(){
var nav = document.getElementById('side-nav');
if (!nav || nav.querySelector('.side-mine')) return;
// 「常用 & 热门」合并块:优先往里插一条 li,不要重建整个块(会顶掉热门入口)
var block = nav.querySelector('.side-hot-block');
if (block) {
var ul = block.querySelector('.side-list');
if (ul) {
// 复用同块里已有链接的 href 前缀,非首页时才会带上 /site/ 前缀
var href = '#cat-mine';
var sample = ul.querySelector('a[data-side-anchor]');
if (sample) href = (sample.getAttribute('href') || '#cat-mine').replace(/cat-top$/, 'cat-mine');
var li = document.createElement('li');
li.innerHTML =
'<a href="' + href + '" class="side-mine" data-side-anchor="cat-mine">' +
'<i class="layui-icon side-ico layui-icon-star-fill"></i>' +
'<span class="side-txt">我常访问</span></a>';
ul.insertBefore(li, ul.firstChild);
retitleFeatured(block.querySelector('.side-title'),
'常用 & 热门', 'layui-icon-star-fill');
return;
}
}
// 兜底:连合并块都没有(既没个人也没热门)时才新建独立块
var b = document.createElement('div');
b.className = 'side-block side-mine-block';
b.innerHTML =
'<div class="side-title"><i class="layui-icon layui-icon-star-fill"></i> 常用 &amp; 热门</div>' +
'<ul class="side-list"><li>' +
'<a href="#cat-mine" class="side-mine" data-side-anchor="cat-mine">' +
'<i class="layui-icon side-ico layui-icon-star-fill"></i>' +
'<span class="side-txt">我常访问</span></a></li></ul>';
nav.insertBefore(b, nav.firstChild);
}
function removeEntries(){
var n = document.querySelector('.cat-nav-mine');
if (n && n.parentNode) n.parentNode.removeChild(n);
var d = document.querySelector('.cat-drawer-mine');
if (d) {
// 抽屉里也是「常用 & 热门」合并分区:只摘这一条 li,
// 整段删会把热门入口一起干掉。分区空了才移除分区本身。
var dli = d.closest('li');
var dsec = d.closest('.cat-drawer-section');
if (dli && dli.parentNode) dli.parentNode.removeChild(dli);
if (dsec && !dsec.querySelector('.cat-drawer-list li') && dsec.parentNode) {
dsec.parentNode.removeChild(dsec);
} else if (dsec && dsec.querySelector('.cat-drawer-hot')) {
// 只剩热门了,标题从「常用 & 热门」改回「热门」
retitleFeatured(dsec.querySelector('.cat-drawer-title'), '热门', 'layui-icon-fire');
}
}
// 侧栏是「常用 & 热门」合并块,只摘掉「我常访问」这一条 li;
// 整块删会把热门入口一起干掉。块空了才移除块本身。
var sm = document.querySelector('.side-mine');
if (sm) {
var sli = sm.closest('li');
var sblk = sm.closest('.side-block');
if (sli && sli.parentNode) sli.parentNode.removeChild(sli);
if (sblk && !sblk.querySelector('.side-list li') && sblk.parentNode) {
sblk.parentNode.removeChild(sblk);
} else if (sblk && sblk.querySelector('.side-hot')) {
// 只剩热门了,标题从「常用 & 热门」改回「热门」
retitleFeatured(sblk.querySelector('.side-title'), '热门', 'layui-icon-fire');
}
}
}
function render(){
var main = document.querySelector('main.content') || document.querySelector('main');
if (!main) return;
var local = load();
var score = {};
// 1) 服务端已渲染的「我常访问」卡片(带 data-mine 加权分)
var sec = document.getElementById('cat-mine');
if (sec) {
sec.querySelectorAll('.card[data-nav-id]').forEach(function(c){
var id = c.getAttribute('data-nav-id');
var v = parseFloat(c.getAttribute('data-mine') || '0') || 0;
if (v > (score[id] || 0)) score[id] = v;
});
}
// 2) 本机 localStorage(含服务端还没同步到的点击)
Object.keys(local).forEach(function(id){
var v = local[id] | 0;
if (v > (score[id] || 0)) score[id] = v;
});
var ranked = Object.keys(score)
.filter(function(id){ return (score[id] || 0) > 0; })
.sort(function(a, b){ return (score[b] - score[a]) || (a - b); })
.slice(0, MAX_ITEMS);
if (ranked.length < MIN_ITEMS) {
// 个人榜不足:整个「我常访问」分区隐藏,热门分区不受影响
if (sec) sec.style.display = 'none';
removeEntries();
return;
}
if (!sec) {
sec = document.createElement('section');
sec.className = 'section section-mine';
sec.id = 'cat-mine';
sec.innerHTML =
'<div class="section-header">' +
'<h2><i class="layui-icon layui-icon-star-fill"></i> 我常访问</h2>' +
'<div class="meta">只在这台设备上统计 · <a href="javascript:;" id="clear-mine">清除我的记录</a></div>' +
'</div>' +
'<div class="grid" id="mine-grid"></div>';
main.insertBefore(sec, main.firstChild);
addCatNavEntry();
addDrawerEntry();
addSideNavEntry();
}
sec.style.display = '';
var grid = sec.querySelector('#mine-grid') || sec.querySelector('.grid');
if (!grid) {
grid = document.createElement('div');
grid.className = 'grid';
grid.id = 'mine-grid';
sec.appendChild(grid);
}
grid.innerHTML = '';
ranked.forEach(function(id){
var src = findSourceCard(id);
if (!src) return;
var clone = src.cloneNode(true);
clone.classList.add('card-mine');
clone.classList.remove('card-top');
clone.setAttribute('data-mine', score[id]);
var vc = clone.querySelector('.visit-count');
if (vc) vc.textContent = '我点过 ' + Math.round(score[id]) + ' 次';
grid.appendChild(clone);
if (window.__wkBindNavClick) window.__wkBindNavClick(clone);
});
}
// 清除我的记录:清 localStorage + 通知服务端删匿名点击
document.addEventListener('click', function(e){
var t = e.target;
if (!t) return;
if (t.id !== 'clear-mine' && !(t.closest && t.closest('#clear-mine'))) return;
e.preventDefault();
try { localStorage.removeItem(KEY); } catch(err) {}
try {
if (navigator.sendBeacon) navigator.sendBeacon('/site/api/my-clear');
else fetch('/site/api/my-clear', {method:'POST', keepalive:true, credentials:'same-origin'});
} catch(err) {}
removeEntries();
// 只移除「我常访问」分区,热门分区(#cat-top)保持原样
var sec = document.getElementById('cat-mine');
if (sec && sec.parentNode) sec.parentNode.removeChild(sec);
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', render);
} else {
render();
}
// 首屏渲染后,为所有卡片加载 favicon(失败的自动回退到字母头像)
if (window.enhanceFavicons) window.enhanceFavicons(document);
})();
// ===== 全局搜索:前端实时过滤卡片,并高亮分类 Tab =====
(function(){
var form = document.getElementById('site-search');
var input = document.getElementById('search-input');
if (!input) return;
// 手机端右侧「搜索」按钮被 CSS 隐藏,改由左侧放大镜提交;
// 同时把冗长的 placeholder 缩短,避免窄屏被截成半句话。
var isPhone = window.matchMedia('(max-width: 600px)');
function applyPhoneTweaks(){
if (isPhone.matches) {
input.placeholder = '搜索书签';
} else {
input.placeholder = '输入书签关键词进行搜索';
}
}
applyPhoneTweaks();
if (isPhone.addEventListener) isPhone.addEventListener('change', applyPhoneTweaks);
else if (isPhone.addListener) isPhone.addListener(applyPhoneTweaks);
var searchIcon = document.getElementById('search-icon');
function submitByIcon(){
if (input.value.trim() && form) form.submit();
else input.focus();
}
if (searchIcon) {
searchIcon.addEventListener('click', submitByIcon);
searchIcon.addEventListener('keydown', function(e){
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); submitByIcon(); }
});
}
function filter(keyword){
keyword = (keyword || '').trim().toLowerCase();
var cards = document.querySelectorAll('.grid .card');
var sections = document.querySelectorAll('section.section');
cards.forEach(function(card){
var text = (card.textContent || '').toLowerCase();
if (!keyword || text.indexOf(keyword) !== -1) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
// 隐藏整节没有可见卡片的分类
sections.forEach(function(sec){
var visible = sec.querySelectorAll('.card:not([style*="display: none"])').length;
sec.style.display = visible ? '' : 'none';
});
// 同步左侧侧栏 / 抽屉:刷新计数,并隐藏没有命中的分类
document.querySelectorAll('a[data-side-anchor]').forEach(function(a){
var id = a.getAttribute('data-side-anchor');
var sec = document.getElementById(id);
var li = a.parentNode;
if (!sec || !li) return; // 本页无对应 section(空分类 / 非首页)保持原样
var n = sec.querySelectorAll('.card:not([style*="display: none"])').length;
var cnt = a.querySelector('.side-count');
if (cnt) cnt.textContent = n ? String(n) : '';
li.style.display = n ? '' : 'none';
});
// 无结果空态
var empty = document.getElementById('search-empty');
if (empty) {
var totalVisible = document.querySelectorAll('.grid .card:not([style*="display: none"])').length;
if (keyword && totalVisible === 0) {
empty.innerHTML = '<span class="ico">🔍</span>未找到与「' + keyword + '」匹配的网址';
empty.hidden = false;
} else {
empty.hidden = true;
}
}
}
// 输入防抖,避免每次按键都遍历全部卡片
var t;
input.addEventListener('input', function(){
clearTimeout(t);
var v = input.value;
t = setTimeout(function(){ filter(v); }, 120);
});
form.addEventListener('submit', function(e){
// 如果当前在首页直接本地过滤,否则提交到首页
var isHome = window.location.pathname === '/' || window.location.pathname === '/site/';
if (isHome) {
e.preventDefault();
filter(input.value);
}
});
// 首页:隐藏没有对应 section 的「空分类」项(侧栏 + 抽屉)
var sideNav = document.getElementById('side-nav');
if (sideNav && sideNav.getAttribute('data-home') === '1') {
document.querySelectorAll('a[data-side-anchor]').forEach(function(a){
var sec = document.getElementById(a.getAttribute('data-side-anchor'));
if (!sec && a.parentNode) a.parentNode.style.display = 'none';
});
}
// 初始化:填充侧栏计数;同时读取 URL 的 ?q=xxx 自动过滤
var params = new URLSearchParams(window.location.search);
var q = params.get('q');
if (q) input.value = q;
filter(q || '');
})();
// 将主题菜单定位到触发元素右侧(fixed 浮层,避免被侧栏 overflow 裁剪)
function positionThemeMenu(trigger, menu){
var rect = trigger.getBoundingClientRect();
var prevDisplay = menu.style.display;
menu.style.visibility = 'hidden';
menu.style.display = 'block';
var mw = menu.offsetWidth, mh = menu.offsetHeight;
menu.style.display = prevDisplay;
menu.style.visibility = '';
var margin = 8;
var vw = window.innerWidth, vh = window.innerHeight;
var left = rect.right + 8; // 首选:触发元素右侧
if (left + mw > vw - margin) left = rect.left - mw - 8; // 右侧放不下:改左侧
// 左侧也放不下(窄屏 / 抽屉里父项几乎占满整宽):贴可视区右边缘,保证完整可见
if (left < margin) left = Math.max(margin, vw - mw - margin);
var top = rect.top;
if (top + mh > vh - margin) top = Math.max(margin, vh - mh - margin);
menu.style.top = top + 'px';
menu.style.left = left + 'px';
}
// ===== 侧栏「切换主题」:弹出下拉菜单 =====
(function(){
var btn = document.getElementById('side-theme-toggle');
var menu = document.getElementById('side-theme-menu');
if (!btn || !menu) return;
function closeAll(){
document.querySelectorAll('.theme-menu.open').forEach(function(m){ m.classList.remove('open'); });
}
btn.addEventListener('click', function(e){
e.preventDefault();
e.stopPropagation();
var wasOpen = menu.classList.contains('open');
closeAll();
if (!wasOpen) { positionThemeMenu(btn, menu); menu.classList.add('open'); }
});
menu.addEventListener('click', function(e){
var li = e.target.closest('li[data-theme]');
if (!li) return;
e.stopPropagation();
applyTheme(li.getAttribute('data-theme'), true);
closeAll();
});
document.addEventListener('click', closeAll);
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeAll(); });
})();
// ===== 顶栏「切换主题」:弹出下拉菜单(与侧栏逻辑一致) =====
(function(){
var btn = document.getElementById('top-theme-toggle');
var menu = document.getElementById('top-theme-menu');
if (!btn || !menu) return;
function closeAll(){
document.querySelectorAll('.theme-menu.open').forEach(function(m){ m.classList.remove('open'); });
}
btn.addEventListener('click', function(e){
e.preventDefault();
e.stopPropagation();
var wasOpen = menu.classList.contains('open');
closeAll();
if (!wasOpen) { positionThemeMenu(btn, menu); menu.classList.add('open'); }
});
menu.addEventListener('click', function(e){
var li = e.target.closest('li[data-theme]');
if (!li) return;
e.stopPropagation();
applyTheme(li.getAttribute('data-theme'), true);
closeAll();
});
document.addEventListener('click', closeAll);
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeAll(); });
})();
// ===== 抽屉内「切换主题」:弹出下拉菜单 =====
(function(){
var btn = document.getElementById('drawer-theme-toggle');
var menu = document.getElementById('drawer-theme-menu');
if (!btn || !menu) return;
function closeAll(){
document.querySelectorAll('.theme-menu.open').forEach(function(m){ m.classList.remove('open'); });
}
btn.addEventListener('click', function(e){
e.preventDefault();
e.stopPropagation();
var wasOpen = menu.classList.contains('open');
closeAll();
if (!wasOpen) {
positionThemeMenu(btn, menu);
menu.classList.add('open');
// 手机端是内联展开,9 个主题项会顶出屏幕底部,展开后滚进可视区
if (menu.scrollIntoView) menu.scrollIntoView({ block: 'nearest' });
}
});
menu.addEventListener('click', function(e){
var li = e.target.closest('li[data-theme]');
if (!li) return;
e.stopPropagation();
applyTheme(li.getAttribute('data-theme'), true);
closeAll();
// 关闭抽屉
var drawer = document.getElementById('cat-drawer');
if (drawer) drawer.classList.remove('open');
});
document.addEventListener('click', closeAll);
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeAll(); });
})();
})();
</script>
</body>
</html>