feat(public): 首页分类移至左侧侧栏 + 大屏自适应布局
- 分类导航由顶部横条改为左侧 sticky 侧栏(≥1025px),含快速导航/我常访问/热门/书签分类/站点页面 - 首页左右去留白:.container 与 .topbar-inner 改为 max-width:100%,布局铺满视口 - 卡片网格按屏宽自适应(minmax(var(--card-min),1fr)),1920 固定 6 列 - 移除顶部 .actions 菜单(与侧栏站点页面重复),主题切换改由侧栏按钮循环切换 - 新增 get_nav_categories() 统一锚点(侧栏/顶部/抽屉/section id 四者同源),修复空分类错位;后台分类增删改/启停后失效前台缓存
This commit is contained in:
@@ -15,3 +15,12 @@ def init_template_directives(app):
|
||||
def csrf_input():
|
||||
return f'<input type="hidden" name="csrf_token" value="{generate_csrf()}">'
|
||||
|
||||
@app.template_global()
|
||||
def nav_categories():
|
||||
"""前台左侧侧栏 / 移动端抽屉共用的分类列表(带 anchor 锚点)。
|
||||
|
||||
以函数形式提供而非 context_processor,后台模板不调用就不会查库。
|
||||
"""
|
||||
from applications.view.public.nav import get_nav_categories
|
||||
return get_nav_categories()
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ URL 路径(挂在根路径下,便于匿名访问):
|
||||
- 使用 url_prefix='/site' 避免与后台 / 路由冲突(后台 / 是登录后的工作台)。
|
||||
"""
|
||||
from collections import OrderedDict
|
||||
import time
|
||||
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
|
||||
@@ -22,6 +23,70 @@ MINE_MIN_ITEMS = 2
|
||||
|
||||
bp = Blueprint('public_nav', __name__, url_prefix='/site')
|
||||
|
||||
# 分类列表缓存(左侧侧栏 / 移动端抽屉共用,避免每个请求都查库)
|
||||
_NAV_CATS_CACHE = {'ts': 0.0, 'data': []}
|
||||
_NAV_CATS_TTL = 30.0
|
||||
|
||||
|
||||
def get_nav_categories() -> "list[dict]":
|
||||
"""返回前台导航用的分类列表(按后台 sort 排序,带稳定锚点)。
|
||||
|
||||
返回形如::
|
||||
[{'name': '开发工具', 'icon': 'layui-icon-list',
|
||||
'description': '', 'anchor': 'cat-1'}, ...]
|
||||
|
||||
锚点 ``anchor`` 基于「启用分类」的排序索引生成(从 1 开始),与首页
|
||||
各 ``<section>`` 的 id 严格一致——即使某个分类下暂时没有导航(空分组
|
||||
被过滤)也不会错位。
|
||||
|
||||
带 30s 进程内缓存:后台新增/调整分类后最多 30s 生效。
|
||||
"""
|
||||
now = time.time()
|
||||
cached = _NAV_CATS_CACHE.get('data') or []
|
||||
if cached and (now - _NAV_CATS_CACHE.get('ts', 0.0)) < _NAV_CATS_TTL:
|
||||
return cached
|
||||
try:
|
||||
cats = (
|
||||
NavCategory.query
|
||||
.filter(NavCategory.enable == 1)
|
||||
.order_by(NavCategory.sort.desc(), NavCategory.id.asc())
|
||||
.all()
|
||||
)
|
||||
names = [c.name for c in cats]
|
||||
meta = {c.name: (c.icon, c.description) for c in cats}
|
||||
|
||||
# 兜底:只在 Nav 表里出现、未登记到 NavCategory 的历史分类,
|
||||
# 追加到末尾(与 _grouped_navs 的兜底顺序一致:按分类名升序),
|
||||
# 保证侧栏锚点与首页 section 一一对应。
|
||||
used = Nav.query.filter(Nav.enable == 1).with_entities(Nav.category).distinct().all()
|
||||
names.extend(sorted(n for (n,) in used if n and n not in meta))
|
||||
|
||||
data = []
|
||||
for idx, name in enumerate(names):
|
||||
icon, desc = meta.get(name, (None, None))
|
||||
data.append({
|
||||
'name': name,
|
||||
'icon': icon or 'layui-icon-list',
|
||||
'description': desc or '',
|
||||
'anchor': 'cat-%d' % (idx + 1),
|
||||
})
|
||||
except Exception: # 数据库尚未初始化(如迁移前)时不影响页面渲染
|
||||
data = []
|
||||
_NAV_CATS_CACHE['ts'] = now
|
||||
_NAV_CATS_CACHE['data'] = data
|
||||
return data
|
||||
|
||||
|
||||
def get_cat_anchor_map() -> "dict[str, str]":
|
||||
"""分类名 -> 锚点 id 的映射,供首页渲染 section id 使用。"""
|
||||
return {c['name']: c['anchor'] for c in get_nav_categories()}
|
||||
|
||||
|
||||
def clear_nav_categories_cache():
|
||||
"""后台改动分类后调用:立即失效缓存。"""
|
||||
_NAV_CATS_CACHE['ts'] = 0.0
|
||||
_NAV_CATS_CACHE['data'] = []
|
||||
|
||||
|
||||
@bp.get('/')
|
||||
def index():
|
||||
@@ -89,6 +154,7 @@ def _render_index():
|
||||
total=total,
|
||||
site_name='旺珂 · 导航',
|
||||
cat_meta=_category_meta(),
|
||||
cat_anchors=get_cat_anchor_map(),
|
||||
top_navs=_top_navs(12),
|
||||
my_navs=mine if len(mine) >= MINE_MIN_ITEMS else [],
|
||||
)
|
||||
|
||||
@@ -26,6 +26,15 @@ bp = Blueprint(
|
||||
)
|
||||
|
||||
|
||||
def _invalidate_nav_cache():
|
||||
"""分类变更后立即失效前台侧栏分类缓存(延迟导入避免循环依赖)。"""
|
||||
try:
|
||||
from applications.view.public.nav import clear_nav_categories_cache
|
||||
clear_nav_categories_cache()
|
||||
except Exception: # 缓存失效失败不影响主流程
|
||||
pass
|
||||
|
||||
|
||||
@bp.get('/')
|
||||
@authorize("system:nav-category:main")
|
||||
def main():
|
||||
@@ -118,6 +127,7 @@ def update():
|
||||
cat.sort = int(req_json.get('sort') or 0)
|
||||
cat.enable = int(req_json.get('enable') or 1)
|
||||
db.session.commit()
|
||||
_invalidate_nav_cache()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return fail_api(msg=f'更新失败:{e}')
|
||||
@@ -147,6 +157,7 @@ def enable():
|
||||
res = curd.enable_status(NavCategory, cat_id)
|
||||
if not res:
|
||||
return fail_api(msg='操作失败')
|
||||
_invalidate_nav_cache()
|
||||
return success_api(msg='已启用')
|
||||
|
||||
|
||||
@@ -160,4 +171,5 @@ def dis_enable():
|
||||
res = curd.disable_status(NavCategory, cat_id)
|
||||
if not res:
|
||||
return fail_api(msg='操作失败')
|
||||
_invalidate_nav_cache()
|
||||
return success_api(msg='已禁用')
|
||||
|
||||
+313
-67
@@ -107,8 +107,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 布局变量:容器宽度 / 侧栏宽度 / 卡片最小宽度 =====
|
||||
* 默认 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%; }
|
||||
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
@@ -126,9 +149,20 @@
|
||||
padding: 56px 0 40px;
|
||||
position: relative;
|
||||
}
|
||||
.topbar-inner { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
|
||||
.topbar-inner { max-width: 100%; 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; }
|
||||
/* 宽屏:标题与内边距同步放大,避免头重脚轻 */
|
||||
@media (min-width: 1700px) {
|
||||
.topbar { padding: 68px 0 52px; }
|
||||
.topbar h1 { font-size: 44px; }
|
||||
.topbar .sub { font-size: 17px; }
|
||||
.site-search { max-width: 680px; }
|
||||
}
|
||||
@media (min-width: 2560px) {
|
||||
.topbar { padding: 84px 0 64px; }
|
||||
.topbar h1 { font-size: 52px; }
|
||||
}
|
||||
.topbar .actions { margin-top: 18px; display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.topbar .actions a, .topbar .actions button {
|
||||
display: inline-flex;
|
||||
@@ -260,7 +294,7 @@
|
||||
}
|
||||
.site-search .search-btn:hover { opacity: 0.9; }
|
||||
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
|
||||
.container { max-width: 100%; margin: 0 auto; padding: 0 24px; }
|
||||
|
||||
/* ===== 分类目录条(横向滚动) ===== */
|
||||
.cat-nav {
|
||||
@@ -357,8 +391,100 @@
|
||||
.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;
|
||||
overscroll-behavior: contain;
|
||||
padding: 8px 0 20px;
|
||||
}
|
||||
.side-nav::-webkit-scrollbar { width: 4px; }
|
||||
.side-nav::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
|
||||
.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: 70px; }
|
||||
.section { margin: 32px 0; scroll-margin-top: 24px; }
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
@@ -375,7 +501,7 @@
|
||||
/* ===== Grid ===== */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--card-min), 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.card {
|
||||
@@ -676,7 +802,7 @@
|
||||
/* 手机端:横向分类 Tab 隐藏(用抽屉替代) */
|
||||
.cat-nav { display: none; }
|
||||
|
||||
.section { margin: 18px 0; scroll-margin-top: 56px; }
|
||||
.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; }
|
||||
@@ -773,35 +899,84 @@
|
||||
<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>
|
||||
{# 首页时锚点直接指向本页 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 %}
|
||||
<div class="side-block side-mine-block">
|
||||
<div class="side-title"><i class="layui-icon layui-icon-star-fill"></i> 我的</div>
|
||||
<ul class="side-list">
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if top_navs is defined and top_navs %}
|
||||
<div class="side-block side-hot-block">
|
||||
<div class="side-title"><i class="layui-icon layui-icon-fire"></i> 热门</div>
|
||||
<ul class="side-list">
|
||||
<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>
|
||||
</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><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></li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
{% 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>
|
||||
@@ -827,7 +1002,7 @@
|
||||
<div class="cat-drawer-title"><i class="layui-icon layui-icon-star-fill"></i> 我的</div>
|
||||
<ul class="cat-drawer-list">
|
||||
<li>
|
||||
<a href="#cat-mine" class="cat-drawer-mine">
|
||||
<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>
|
||||
@@ -843,7 +1018,7 @@
|
||||
<div class="cat-drawer-title"><i class="layui-icon layui-icon-fire"></i> 热门</div>
|
||||
<ul class="cat-drawer-list">
|
||||
<li>
|
||||
<a href="#cat-top" class="cat-drawer-hot">
|
||||
<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>
|
||||
@@ -854,30 +1029,20 @@
|
||||
{% endif %}
|
||||
|
||||
{# 分类区 #}
|
||||
{% 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, {}) %}
|
||||
{% for c in nav_categories() %}
|
||||
<li>
|
||||
<a href="#cat-{{ loop.index }}">
|
||||
<i class="layui-icon cat-icon {{ meta.icon|default('layui-icon-list') }}"></i>
|
||||
<span>{{ category }}</span>
|
||||
<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>
|
||||
{% 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">
|
||||
@@ -1006,27 +1171,49 @@
|
||||
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});
|
||||
// 目录高亮当前 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 + 100;
|
||||
var active = sections[0];
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
if (sections[i].el.offsetTop <= y) active = sections[i];
|
||||
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;
|
||||
}
|
||||
}
|
||||
navLinks.forEach(function(a){ a.classList.remove('active'); });
|
||||
if (active) active.link.classList.add('active');
|
||||
}
|
||||
window.addEventListener('scroll', onScroll, {passive: true});
|
||||
window.addEventListener('resize', onScroll);
|
||||
onScroll();
|
||||
}
|
||||
})();
|
||||
|
||||
// ===== 移动端分类抽屉 =====
|
||||
(function(){
|
||||
@@ -1188,6 +1375,21 @@
|
||||
panel.insertBefore(sec, panel.children[1] || null);
|
||||
}
|
||||
|
||||
// 左侧侧栏里补一个「我常访问」入口(桌面端)
|
||||
function addSideNavEntry(){
|
||||
var nav = document.getElementById('side-nav');
|
||||
if (!nav || nav.querySelector('.side-mine')) return;
|
||||
var block = document.createElement('div');
|
||||
block.className = 'side-block side-mine-block';
|
||||
block.innerHTML =
|
||||
'<div class="side-title"><i class="layui-icon layui-icon-star-fill"></i> 我的</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(block, nav.firstChild);
|
||||
}
|
||||
|
||||
function removeEntries(){
|
||||
var n = document.querySelector('.cat-nav-mine');
|
||||
if (n && n.parentNode) n.parentNode.removeChild(n);
|
||||
@@ -1196,10 +1398,12 @@
|
||||
var s = d.closest('.cat-drawer-section');
|
||||
if (s && s.parentNode) s.parentNode.removeChild(s);
|
||||
}
|
||||
var sb = document.querySelector('.side-mine-block');
|
||||
if (sb && sb.parentNode) sb.parentNode.removeChild(sb);
|
||||
}
|
||||
|
||||
function render(){
|
||||
var main = document.querySelector('main.container');
|
||||
var main = document.querySelector('main.content') || document.querySelector('main');
|
||||
if (!main) return;
|
||||
|
||||
var local = load();
|
||||
@@ -1242,6 +1446,7 @@
|
||||
main.insertBefore(sec, main.firstChild);
|
||||
addCatNavEntry();
|
||||
addDrawerEntry();
|
||||
addSideNavEntry();
|
||||
}
|
||||
sec.style.display = '';
|
||||
|
||||
@@ -1309,6 +1514,18 @@
|
||||
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';
|
||||
});
|
||||
}
|
||||
|
||||
input.addEventListener('input', function(){
|
||||
@@ -1324,13 +1541,42 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载时读取 URL 的 ?q=xxx 自动过滤
|
||||
// 首页:隐藏没有对应 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);
|
||||
}
|
||||
if (q) input.value = q;
|
||||
filter(q || '');
|
||||
})();
|
||||
|
||||
// ===== 侧栏「切换主题」:循环切换到下一个主题 =====
|
||||
(function(){
|
||||
var btn = document.getElementById('side-theme-toggle');
|
||||
if (!btn) return;
|
||||
btn.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 next = VALID[(VALID.indexOf(cur) + 1) % VALID.length];
|
||||
var isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
var resolved = (next === 'auto') ? (isDark ? 'dark' : 'default') : next;
|
||||
document.documentElement.setAttribute('data-theme', resolved);
|
||||
var label = document.getElementById('theme-toggle-label');
|
||||
if (label) label.textContent = LABEL_MAP[next];
|
||||
localStorage.setItem('bw-theme', next);
|
||||
document.querySelectorAll('.theme-menu li').forEach(function(li){
|
||||
li.classList.toggle('active', li.getAttribute('data-theme') === next);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -34,15 +34,16 @@
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
{# 分类目录条(sticky 顶部导航) #}
|
||||
{# 分类目录条:仅平板(601–1024px)可见,桌面走左侧侧栏、手机走抽屉 #}
|
||||
{% block cat_nav %}
|
||||
{% if groups or top_navs or my_navs %}
|
||||
{% set anchors = cat_anchors or {} %}
|
||||
<nav class="cat-nav" aria-label="分类导航">
|
||||
<div class="cat-nav-inner" id="cat-nav-inner">
|
||||
{% if my_navs %}<a href="#cat-mine" class="cat-nav-mine">★ 我常访问</a>{% endif %}
|
||||
{% if top_navs %}<a href="#cat-top" class="cat-nav-hot">🔥 热门网址</a>{% endif %}
|
||||
{% for category in groups.keys() %}
|
||||
<a href="#cat-{{ loop.index }}">{{ category }}</a>
|
||||
<a href="#{{ anchors.get(category) or ('cat-' ~ loop.index) }}">{{ category }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</nav>
|
||||
@@ -88,8 +89,9 @@
|
||||
{% endif %}
|
||||
|
||||
{% for category, items in groups.items() %}
|
||||
{% set cat_id = 'cat-' ~ loop.index %}
|
||||
<section class="section" id="{{ cat_id }}">
|
||||
{# 锚点与左侧侧栏、顶部横条、移动端抽屉共用同一套 id #}
|
||||
{% set cat_id = (cat_anchors or {}).get(category) or ('cat-' ~ loop.index) %}
|
||||
<section class="section" id="{{ cat_id }}" data-cat="{{ category }}">
|
||||
<div class="section-header">
|
||||
<h2>{{ category }}</h2>
|
||||
<div class="meta">
|
||||
|
||||
Reference in New Issue
Block a user