Files
pear-admin-flask/templates/public/base.html
T
bwstudio 0ffc103e46 feat(plugins): 4 个业务插件代码 + 前台公开页面改造
plugins/navManager:导航 CRUD + 导航分类 CRUD(独立 blueprint)
plugins/friendManager:友情链接 CRUD
plugins/aboutManager:关于本站 CRUD
plugins/siteStats:访问统计主页 + JSON 接口 + /site/* GET 埋点 + nav 卡片点击埋点

applications/view/public/nav.py:导航聚合页 / 分类详情 / JSON 接口,支持 visit_count 注入
applications/view/public/about.py:关于本站公开页
applications/view/public/friend.py:友情链接公开页

templates/public/base.html:响应式首页(搜索框 / 分类抽屉 / 5 主题切换 / footer 隐私小字)
templates/public/index.html / category.html:卡片右下角访问次数
templates/public/about.html / friend.html:关于 + 友链前台页

docs/plugins-development.md:插件开发完整指南(生命周期 + 4 个示例 + FAQ + framework 迁移)

scripts/:探针与端到端验证脚本(probe_*/verify_*/test_*)
2026-09-06 18:18:06 +08:00

1102 lines
45 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, [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);
}
/* 暮色橙(暖色系) */
[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);
}
/* 森林绿(自然系) */
[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);
}
/* 暗色(深色背景) */
[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);
}
/* 系统暗色兜底:未设置 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);
}
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
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: 1200px; margin: 0 auto; padding: 0 24px; }
.topbar h1 { font-size: 36px; margin: 0 0 8px; font-weight: 600; line-height: 1.2; }
.topbar .sub { font-size: 15px; opacity: 0.92; }
.topbar .actions { margin-top: 18px; display: flex; flex-wrap: wrap; gap: 8px; }
.topbar .actions a, .topbar .actions button {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 14px;
background: rgba(255,255,255,0.18);
border: 1px solid rgba(255,255,255,0.35);
color: var(--topbar-text);
border-radius: 999px;
font-size: 13px;
text-decoration: none;
cursor: pointer;
transition: background 0.2s;
font-family: inherit;
}
.topbar .actions a:hover, .topbar .actions button:hover { background: rgba(255,255,255,0.32); }
/* ===== 主题切换下拉 ===== */
.theme-picker { position: relative; display: inline-block; }
.theme-picker > button {
display: inline-flex; align-items: center; gap: 4px;
}
.theme-menu {
position: absolute;
top: calc(100% + 6px);
right: 0;
min-width: 180px;
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;
opacity: 0;
transform: translateY(-6px);
pointer-events: none;
transition: opacity 0.15s, transform 0.15s;
}
.theme-menu.open {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.theme-menu li {
display: flex; align-items: center; gap: 10px;
padding: 8px 12px;
font-size: 14px;
border-radius: 6px;
cursor: pointer;
transition: background 0.12s;
color: var(--text);
}
.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 .dot {
display: inline-block;
width: 18px; height: 18px;
border-radius: 50%;
border: 2px solid var(--border);
flex-shrink: 0;
}
.dot-default { background: linear-gradient(135deg, #16baaa 0%, #2e8de8 100%); }
.dot-sunset { background: linear-gradient(135deg, #ff7043 0%, #e91e63 100%); }
.dot-forest { background: linear-gradient(135deg, #2e7d32 0%, #00897b 100%); }
.dot-dark { background: linear-gradient(135deg, #0f1419 0%, #16baaa 100%); }
.dot-auto {
background: linear-gradient(90deg, #f5f7fa 0%, #f5f7fa 50%, #0f1419 50%, #0f1419 100%);
border-color: var(--text-muted);
}
/* 手机端分类入口按钮:默认桌面隐藏 */
.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); }
/* ===== 顶部全局搜索 ===== */
.site-search {
position: relative;
max-width: 560px;
margin-top: 22px;
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: 1200px; 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: 1200px;
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; }
.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; }
/* ===== Section ===== */
.section { margin: 32px 0; scroll-margin-top: 70px; }
.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(260px, 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); }
.card .icon-wrap {
width: 42px; height: 42px;
background: rgba(22, 186, 170, 0.1);
color: var(--brand);
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.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;
}
.card .url {
font-size: 12px; color: var(--text-soft);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
direction: rtl;
text-align: left;
}
.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;
}
[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: 12px 0; }
.topbar-inner {
padding: 0 14px;
display: flex;
align-items: center;
gap: 10px;
}
.topbar-brand { display: none; } /* 手机顶部只保留分类按钮+搜索 */
.topbar .actions { display: none; }
.cat-trigger-mobile { display: inline-flex; }
/* 顶部搜索框更紧凑,占满剩余空间 */
.site-search {
flex: 1;
margin-top: 0;
max-width: none;
padding: 3px 4px 3px 12px;
}
.site-search i { font-size: 16px; margin-right: 8px; }
.site-search input[type="search"] { font-size: 14px; padding: 8px 0; }
.site-search .search-btn { padding: 7px 14px; font-size: 13px; }
/* 手机端:横向分类 Tab 隐藏(用抽屉替代) */
.cat-nav { display: none; }
.section { margin: 18px 0; scroll-margin-top: 56px; }
.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; font-size: 16px; border-radius: 8px; flex-shrink: 0; }
.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-height: 100vh;
max-width: 280px;
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; }
/* 抽屉分区 */
.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>
{# 全局搜索框(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"></i>
<input type="search" name="q" id="search-input" placeholder="输入书签关键词进行搜索" autocomplete="off" aria-label="搜索书签">
<button type="submit" class="search-btn" aria-label="搜索">搜索</button>
</form>
<div class="actions">
<a href="{{ url_for('public_nav.index') }}">🏠 首页</a>
<a href="{{ url_for('public_about.about') }}">📖 关于</a>
<a href="{{ url_for('public_friend.friend_index') }}">🔗 友链</a>
<a href="/system/passport/login">🔐 登录后台</a>
{# 主题切换下拉 #}
<div class="theme-picker">
<button type="button" id="theme-toggle" title="切换主题" aria-haspopup="listbox" aria-controls="theme-menu">
<span id="theme-toggle-label">🎨 主题</span>
<i class="layui-icon layui-icon-down" style="font-size:12px;"></i>
</button>
<ul class="theme-menu" id="theme-menu" role="listbox">
<li role="option" data-theme="default"><span class="dot dot-default"></span>蓝绿(默认)</li>
<li role="option" data-theme="sunset"><span class="dot dot-sunset"></span>暮色橙</li>
<li role="option" data-theme="forest"><span class="dot dot-forest"></span>森林绿</li>
<li role="option" data-theme="dark"><span class="dot dot-dark"></span>暗色</li>
<li role="option" data-theme="auto"><span class="dot dot-auto"></span>🔄 跟随系统</li>
</ul>
</div>
</div>
</div>
</header>
{% block cat_nav %}{% endblock %}
<main class="container">
{% block content %}{% endblock %}
</main>
<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">本站使用匿名访问统计,仅记录点击次数,不关联个人身份。</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 cat_meta is defined %}
<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 category in groups.keys() %}
{% set meta = cat_meta.get(category, {}) %}
<li>
<a href="#cat-{{ loop.index }}">
<i class="layui-icon cat-icon {{ meta.icon|default('layui-icon-list') }}"></i>
<span>{{ category }}</span>
<span class="arrow"></span>
</a>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<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">
{# 由 JS 从 .cat-nav a 同步过来(无 cat_meta 时兜底) #}
</ul>
</div>
{% endif %}
{# 其他页面入口(关于 / 友链 / 后台 / 主题) #}
<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><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></li>
</ul>
</div>
</div>
</div>
{% block extra_js %}{% endblock %}
<script>
// ===== 主题切换:多主题选择 + localStorage 持久化 + 跟随系统 =====
(function(){
var THEME_KEY = 'bw-theme';
var FIXED_THEMES = ['default', 'sunset', 'forest', 'dark'];
var ALL = FIXED_THEMES.concat(['auto']);
var LABEL_MAP = {
'default': '🎨 蓝绿',
'sunset': '🎨 暮色',
'forest': '🎨 森林',
'dark': '🎨 暗色',
'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 FIXED_THEMES.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' : '#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');
});
// 更新顶部按钮文字
var label = document.getElementById('theme-toggle-label');
if (label) label.textContent = LABEL_MAP[name] || '🎨 主题';
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);
}
// 顶部按钮:展开/收起菜单
var btn = document.getElementById('theme-toggle');
var menu = document.getElementById('theme-menu');
if (btn && menu) {
btn.addEventListener('click', function(e){
e.stopPropagation();
menu.classList.toggle('open');
});
menu.querySelectorAll('li').forEach(function(li){
li.addEventListener('click', function(e){
e.stopPropagation();
applyTheme(this.getAttribute('data-theme'), true);
menu.classList.remove('open');
});
});
document.addEventListener('click', function(){
menu.classList.remove('open');
});
}
// 跟随系统:监听系统暗色偏好变化,自动重应用
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
var navLinks = document.querySelectorAll('.cat-nav a[href^="#"]');
if (navLinks.length) {
var sections = [];
navLinks.forEach(function(a){
var id = a.getAttribute('href').slice(1);
var el = document.getElementById(id);
if (el) sections.push({link: a, el: el});
});
function onScroll(){
var y = window.scrollY + 100;
var active = sections[0];
for (var i = 0; i < sections.length; i++) {
if (sections[i].el.offsetTop <= y) active = sections[i];
}
navLinks.forEach(function(a){ a.classList.remove('active'); });
if (active) active.link.classList.add('active');
}
window.addEventListener('scroll', onScroll, {passive: true});
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');
}
function closeDrawer(){
drawer.classList.remove('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);
});
}
// 抽屉内"切换主题"按钮:循环到下一个主题(含 auto)
var drawerThemeBtn = document.getElementById('drawer-theme-toggle');
if (drawerThemeBtn) {
drawerThemeBtn.addEventListener('click', function(e){
e.preventDefault();
var VALID = ['default', 'sunset', 'forest', 'dark', 'auto'];
var LABEL_MAP = {default:'🎨 蓝绿', sunset:'🎨 暮色', forest:'🎨 森林', dark:'🎨 暗色', auto:'🎨 跟随系统'};
var cur = localStorage.getItem('bw-theme') || 'default';
var idx = VALID.indexOf(cur);
var next = VALID[(idx + 1) % VALID.length];
// 解析 next 为实际生效值
var isSystemDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
var resolved = (next === 'auto') ? (isSystemDark ? 'dark' : 'default') : next;
document.documentElement.setAttribute('data-theme', resolved);
var labelEl = document.getElementById('theme-toggle-label');
if (labelEl) labelEl.textContent = LABEL_MAP[next];
localStorage.setItem('bw-theme', next);
// 同步菜单高亮
document.querySelectorAll('.theme-menu li').forEach(function(li){
if (li.getAttribute('data-theme') === next) li.classList.add('active');
else li.classList.remove('active');
});
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;
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);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bindAll);
} else {
bindAll();
}
})();
// ===== 全局搜索:前端实时过滤卡片,并高亮分类 Tab =====
(function(){
var form = document.getElementById('site-search');
var input = document.getElementById('search-input');
if (!input) return;
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';
});
}
input.addEventListener('input', function(){
filter(input.value);
});
form.addEventListener('submit', function(e){
// 如果当前在首页直接本地过滤,否则提交到首页
var isHome = window.location.pathname === '/' || window.location.pathname === '/site/';
if (isHome) {
e.preventDefault();
filter(input.value);
}
});
// 页面加载时读取 URL 的 ?q=xxx 自动过滤
var params = new URLSearchParams(window.location.search);
var q = params.get('q');
if (q) {
input.value = q;
filter(q);
}
})();
</script>
</body>
</html>