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:
+220
-28
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user