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_*)
1102 lines
45 KiB
HTML
1102 lines
45 KiB
HTML
<!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> |