style(public): 前台导航页响应式适配 + 暗色模式 + 锚点目录

主要改动
- templates/public/base.html
  - 增加 3 套响应式断点:≤900px(平板)、≤600px(手机)、≤360px(小屏)
  - topbar 在小屏下压缩 padding/字号,actions 自适应换行
  - grid 卡片在手机端最小 160px,小屏单列
  - 卡片字号、间距在手机端缩放
  - 增加 sticky 分类目录条占位
  - 增加回到顶部按钮(滚动后显示)
  - 增加暗色模式 CSS(prefers-color-scheme + data-theme 手动覆盖)和主题切换按钮
  - 补充 theme-color、viewport 等移动端 meta
- templates/public/index.html
  - 输出 5 个分类的 sticky 目录导航(点击锚点跳转)
  - 每个 section 设置 id=cat-N 锚点
  - 卡片 title/url 增加 title 提示
- templates/public/category.html
  - 目录条加返回首页 + 当前分类名
  - section 锚点统一为 cat-detail

验证
- curl 抓取 /site/ 响应 200,HTML 中 10 项响应式/交互特性检查全部通过
- 服务器本地运行无异常
This commit is contained in:
bwstudio
2026-09-05 15:33:45 +08:00
parent 115585c00d
commit 9ba96bcc3e
3 changed files with 251 additions and 38 deletions
+220 -28
View File
@@ -2,68 +2,146 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<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('BWStudio · 导航') }}{% endblock %}</title>
<meta name="description" content="BWStudio 公开导航聚合页:精选开发工具、AI、设计资源、效率工具等优质站点。">
<meta property="og:title" content="{{ site_name|default('BWStudio · 导航') }}">
<meta property="og:description" content="精选优质站点,按分类聚合展示。">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/layui@2.9.7/dist/css/layui.css">
<style>
:root {
--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="dark"] {
--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);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--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, #16baaa 0%, #2e8de8 100%);
color: #fff;
padding: 56px 0 48px;
background: linear-gradient(135deg, var(--brand) 0%, var(--brand-2) 100%);
color: var(--topbar-text);
padding: 56px 0 40px;
position: relative;
}
.topbar .container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
.topbar h1 { font-size: 36px; margin: 0 0 8px; font-weight: 600; }
.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; }
.topbar .actions a {
display: inline-block;
.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;
margin-right: 8px;
background: rgba(255,255,255,0.18);
border: 1px solid rgba(255,255,255,0.35);
color: #fff;
color: var(--topbar-text);
border-radius: 999px;
font-size: 13px;
text-decoration: none;
transition: all 0.2s;
cursor: pointer;
transition: background 0.2s;
font-family: inherit;
}
.topbar .actions a:hover { background: rgba(255,255,255,0.32); }
.topbar .actions a:hover, .topbar .actions button:hover { background: rgba(255,255,255,0.32); }
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
.section { margin: 32px 0; }
/* ===== 分类目录条(横向滚动) ===== */
.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; }
/* ===== 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 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));
@@ -77,19 +155,17 @@
text-decoration: none;
color: inherit;
box-shadow: var(--shadow);
transition: all 0.25s ease;
display: flex;
flex-direction: column;
gap: 6px;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
display: flex; flex-direction: column; gap: 6px;
min-width: 0;
}
.card:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-hover);
border-color: var(--brand);
}
.card .title-row {
display: flex; align-items: center; gap: 10px;
}
.card:active { transform: translateY(0); } /* 移动端点击反馈 */
.card .title-row { display: flex; align-items: center; gap: 10px; min-width: 0; }
.card .icon-wrap {
width: 36px; height: 36px;
background: rgba(22, 186, 170, 0.1);
@@ -101,7 +177,8 @@
}
.card .title {
font-size: 15px; font-weight: 600; color: var(--text);
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
flex: 1; min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.card .desc {
font-size: 13px; color: var(--text-muted);
@@ -113,8 +190,10 @@
min-height: 38px;
}
.card .url {
font-size: 12px; color: #b0bac3;
font-size: 12px; color: var(--text-soft);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
direction: rtl;
text-align: left;
}
.empty {
text-align: center; padding: 48px 0; color: var(--text-muted);
@@ -125,6 +204,62 @@
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; }
/* ===== 回到顶部 ===== */
.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(220px, 1fr)); }
.topbar h1 { font-size: 30px; }
}
/* ===== 响应式:手机 ===== */
@media (max-width: 600px) {
body { font-size: 14px; }
.container { padding: 0 14px; }
.topbar { padding: 32px 0 28px; }
.topbar .container { padding: 0 14px; }
.topbar h1 { font-size: 24px; }
.topbar .sub { font-size: 13px; }
.topbar .actions a, .topbar .actions button { padding: 5px 11px; font-size: 12px; }
.cat-nav-inner { padding: 6px 14px; }
.cat-nav a { padding: 5px 11px; font-size: 12px; }
.section { margin: 22px 0; scroll-margin-top: 60px; }
.section-header h2 { font-size: 17px; }
.section-header .meta { font-size: 12px; }
.grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 10px;
}
.card { padding: 12px; border-radius: 8px; }
.card .title { font-size: 14px; }
.card .desc { font-size: 12px; min-height: 32px; }
.card .icon-wrap { width: 30px; height: 30px; font-size: 16px; }
.to-top { right: 14px; bottom: 14px; width: 38px; height: 38px; }
}
@media (max-width: 360px) {
.grid { grid-template-columns: 1fr; }
}
</style>
{% block extra_head %}{% endblock %}
</head>
@@ -136,18 +271,75 @@
<div class="actions">
<a href="{{ url_for('public_nav.index') }}">🏠 首页</a>
<a href="/admin/login">🔐 登录后台</a>
<button type="button" id="theme-toggle" title="切换主题" aria-label="切换主题">🌗 主题</button>
</div>
</div>
</header>
{% block cat_nav %}{% endblock %}
<main class="container">
{% block content %}{% endblock %}
</main>
<footer class="site-footer">
© 2026 BWStudio · 基于 <a href="https://gitea.bwhome.top/bwadmin/pear-admin-flask" style="color:var(--brand);">Pear Admin Flask</a> 构建
© 2026 BWStudio · 基于 <a href="https://gitea.bwhome.top/bwadmin/pear-admin-flask">Pear Admin Flask</a> 构建
</footer>
<button class="to-top" id="to-top" aria-label="回到顶部" title="回到顶部"></button>
{% block extra_js %}{% endblock %}
<script>
// 暗色模式:localStorage > data-theme > 系统
(function(){
var saved = localStorage.getItem('bw-theme');
if (saved === 'dark' || saved === 'light') {
document.documentElement.setAttribute('data-theme', saved);
}
var btn = document.getElementById('theme-toggle');
if (btn) {
btn.addEventListener('click', function(){
var cur = document.documentElement.getAttribute('data-theme');
var sysDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
var isDark = cur ? (cur === 'dark') : sysDark;
var next = isDark ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('bw-theme', next);
});
}
// 回到顶部
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();
}
})();
</script>
</body>
</html>
+14 -7
View File
@@ -2,14 +2,21 @@
{% block title %}{{ category }} - {{ site_name }}{% endblock %}
{# 分类详情页只有一个分类,目录条也指向自己即可(避免空目录条) #}
{% block cat_nav %}
<nav class="cat-nav" aria-label="分类导航">
<div class="cat-nav-inner">
<a href="{{ url_for('public_nav.index') }}" class="active">← 返回首页</a>
<a href="#cat-detail" class="active">{{ category }}</a>
</div>
</nav>
{% endblock %}
{% block content %}
<section class="section">
<section class="section" id="cat-detail">
<div class="section-header">
<h2>{{ category }}</h2>
<div class="meta">
共 {{ items|length }} 个 ·
<a href="{{ url_for('public_nav.index') }}">← 返回首页</a>
</div>
<div class="meta">共 {{ items|length }} 个</div>
</div>
{% if not items %}
@@ -24,10 +31,10 @@
<div class="icon-wrap">
<i class="layui-icon {{ item.icon }}"></i>
</div>
<div class="title">{{ item.title }}</div>
<div class="title" title="{{ item.title }}">{{ item.title }}</div>
</div>
<div class="desc">{{ item.description or '暂无描述' }}</div>
<div class="url">{{ item.url }}</div>
<div class="url" title="{{ item.url }}">{{ item.url }}</div>
</a>
{% endfor %}
</div>
+17 -3
View File
@@ -1,5 +1,18 @@
{% extends 'public/base.html' %}
{# 分类目录条(sticky 顶部导航) #}
{% block cat_nav %}
{% if groups %}
<nav class="cat-nav" aria-label="分类导航">
<div class="cat-nav-inner">
{% for category in groups.keys() %}
<a href="#cat-{{ loop.index }}">{{ category }}</a>
{% endfor %}
</div>
</nav>
{% endif %}
{% endblock %}
{% block content %}
{% if not groups %}
<div class="empty">
@@ -8,7 +21,8 @@
</div>
{% else %}
{% for category, items in groups.items() %}
<section class="section">
{% set cat_id = 'cat-' ~ loop.index %}
<section class="section" id="{{ cat_id }}">
<div class="section-header">
<h2>{{ category }}</h2>
<div class="meta">
@@ -25,10 +39,10 @@
<div class="icon-wrap">
<i class="layui-icon {{ item.icon }}"></i>
</div>
<div class="title">{{ item.title }}</div>
<div class="title" title="{{ item.title }}">{{ item.title }}</div>
</div>
<div class="desc">{{ item.description or '暂无描述' }}</div>
<div class="url">{{ item.url }}</div>
<div class="url" title="{{ item.url }}">{{ item.url }}</div>
</a>
{% endfor %}
</div>