feat(public): 首页卡片/主题菜单/侧栏/移动端多项优化

卡片
- 描述与网址共用一行(.meta-line + grid 同格),默认显示描述、悬停交叉淡出为网址;
  触屏 (hover:none) 直接显示网址,卡片不再被两行文字撑高

主题菜单
- 修复「暗色」项与其他项不一致:主题变量块原用裸属性选择器 [data-theme="dark"],
  会连菜单里的 <li data-theme="dark"> 一起命中导致颜色被局部覆盖;
  全部改为 html[data-theme="…"](8 个主题 + 关于页 dark 特例)
- 每项加统一色块标识 + 选中对勾,宽高与对齐方式一致

左侧菜单栏
- 「我常访问」「热门网址」合并为一个「常用 & 热门」分区(侧栏 + 移动端抽屉),
  标题随数据动态切换;右侧内容区两个 section 保持独立不变
- addSideNavEntry/addDrawerEntry 改为往合并分区插 li,removeEntries 只摘单条,
  避免整块删除把热门入口一起干掉
- 新增 retitleFeatured():JS 补入/移除个人榜后同步标题与图标

PC 侧栏
- 修复滚轮无响应:overscroll-behavior 由 contain 改为 auto(contain 会阻断滚动链接,
  内容未溢出时侧栏上滚轮完全没反应)
- 滚动条加宽到 6px 并使用 --text-soft,加 scrollbar-gutter: stable

移动端顶栏
- 修复 360px 小屏三个控件挤成两行:.topbar-actions 改 flex: 1 1 0 + min-width: 0
  (flex 换行先于收缩,basis auto 会先按固有宽度判定放不下而换行)
- 恢复显示站点名与副标题作为第二行,消除顶部空白
- 手机端隐藏「搜索」按钮,改由左侧放大镜提交,input 加 enterkeyhint
- placeholder 按视口自适应:手机「搜索书签」

抽屉
- 主题菜单在窄屏改为内联展开(280px 抽屉放不下 160px fixed 浮层,
  原逻辑会算出负数 left 飞出屏幕)
- positionThemeMenu 增加边界兜底:两侧都放不下时贴可视区右边缘
- 面板补 width: min(280px,84vw) + overflow-y:auto(原被内容撑成 173px)
This commit is contained in:
bwstudio
2026-09-07 20:38:06 +08:00
parent 279217746a
commit 4ece453f00
3 changed files with 419 additions and 84 deletions
+408 -79
View File
@@ -36,7 +36,7 @@
* forest 墨绿 + 青绿
* dark 深色背景
*/
:root, [data-theme="default"] {
:root, html[data-theme="default"] {
--brand: #16baaa;
--brand-2: #2e8de8;
--bg: #f5f7fa;
@@ -50,7 +50,7 @@
--shadow-hover: 0 8px 24px rgba(22, 186, 170, 0.18);
}
/* 暮色橙(暖色系) */
[data-theme="sunset"] {
html[data-theme="sunset"] {
--brand: #ff7043;
--brand-2: #e91e63;
--bg: #fff8f3;
@@ -64,7 +64,7 @@
--shadow-hover: 0 8px 24px rgba(255,112,67,0.25);
}
/* 森林绿(自然系) */
[data-theme="forest"] {
html[data-theme="forest"] {
--brand: #2e7d32;
--brand-2: #00897b;
--bg: #f3f8f4;
@@ -78,7 +78,7 @@
--shadow-hover: 0 8px 24px rgba(46,125,50,0.22);
}
/* 暗色(深色背景) */
[data-theme="dark"] {
html[data-theme="dark"] {
--brand: #16baaa;
--brand-2: #2e8de8;
--bg: #0f1419;
@@ -92,7 +92,7 @@
--shadow-hover: 0 8px 24px rgba(22, 186, 170, 0.25);
}
/* 紫韵(优雅紫色) */
[data-theme="lavender"] {
html[data-theme="lavender"] {
--brand: #8b5cf6;
--brand-2: #ec4899;
--bg: #faf5ff;
@@ -106,7 +106,7 @@
--shadow-hover: 0 8px 24px rgba(139,92,246,0.25);
}
/* 深海(深蓝 navy */
[data-theme="ocean"] {
html[data-theme="ocean"] {
--brand: #0ea5e9;
--brand-2: #06b6d4;
--bg: #f0f9ff;
@@ -120,7 +120,7 @@
--shadow-hover: 0 8px 24px rgba(14,165,233,0.28);
}
/* 青空(明亮天蓝) */
[data-theme="sky"] {
html[data-theme="sky"] {
--brand: #38bdf8;
--brand-2: #3b82f6;
--bg: #f0f9ff;
@@ -134,7 +134,7 @@
--shadow-hover: 0 8px 24px rgba(59,130,246,0.28);
}
/* 玫红(热情玫红) */
[data-theme="rose"] {
html[data-theme="rose"] {
--brand: #f43f5e;
--brand-2: #ec4899;
--bg: #fff1f2;
@@ -205,7 +205,38 @@
padding: 56px 0 40px;
position: relative;
}
.topbar-inner { max-width: 100%; margin: 0 auto; padding: 0 24px; }
.topbar-inner {
max-width: 100%;
margin: 0 auto;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
flex-wrap: wrap;
}
.topbar-brand { flex: 1 1 auto; min-width: 220px; }
.topbar-actions {
display: flex;
align-items: center;
gap: 14px;
flex: 0 1 auto;
justify-content: flex-end;
}
/* 顶栏右侧主题切换按钮(让原本藏在侧栏的入口在顶栏也可见,填充右侧空白) */
.top-theme-trigger { position: relative; }
.top-theme-toggle {
width: 44px; height: 44px;
border-radius: 50%;
border: 1px solid rgba(255,255,255,0.4);
background: rgba(255,255,255,0.18);
color: #fff; font-size: 20px;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
transition: background .2s;
}
.top-theme-toggle:hover { background: rgba(255,255,255,0.30); }
.topbar h1 { font-size: 36px; margin: 0 0 8px; font-weight: 600; line-height: 1.2; }
.topbar .sub { font-size: 15px; opacity: 0.92; }
/* 宽屏:标题与内边距同步放大,避免头重脚轻 */
@@ -213,7 +244,7 @@
.topbar { padding: 68px 0 52px; }
.topbar h1 { font-size: 44px; }
.topbar .sub { font-size: 17px; }
.site-search { max-width: 760px; }
.site-search { max-width: 560px; }
/* 卡片描述放宽到 2 行,提升信息密度 */
.card .desc { -webkit-line-clamp: 2; min-height: 40px; }
}
@@ -256,7 +287,9 @@
}
.theme-menu.open { display: block; }
.theme-menu li {
display: block;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
font-size: 14px;
border-radius: 6px;
@@ -264,14 +297,39 @@
transition: background 0.12s;
color: var(--text);
}
/* 每项都带一枚同规格色块,保证「暗色」这类深底项不会视觉上落单 */
.theme-menu li::before {
content: '';
width: 12px; height: 12px;
border-radius: 50%;
flex-shrink: 0;
background: var(--sw, var(--brand));
box-shadow: 0 0 0 1px rgba(0,0,0,0.12) inset;
}
.theme-menu li[data-theme="default"] { --sw: #16baaa; }
.theme-menu li[data-theme="sunset"] { --sw: #ff7043; }
.theme-menu li[data-theme="forest"] { --sw: #2e7d32; }
.theme-menu li[data-theme="dark"] { --sw: #1a1f29; }
.theme-menu li[data-theme="lavender"] { --sw: #8b5cf6; }
.theme-menu li[data-theme="ocean"] { --sw: #0ea5e9; }
.theme-menu li[data-theme="sky"] { --sw: #38bdf8; }
.theme-menu li[data-theme="rose"] { --sw: #f43f5e; }
.theme-menu li[data-theme="auto"] { --sw: linear-gradient(135deg, #f5f7fa 50%, #1a1f29 50%); }
.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.active::after {
content: '✓';
margin-left: auto;
color: var(--brand);
font-weight: 700;
}
/* ===== 顶部全局搜索 ===== */
.site-search {
position: relative;
max-width: 560px;
margin-top: 22px;
max-width: 460px;
flex: 1 1 320px;
margin-top: 0;
display: flex;
align-items: center;
background: rgba(255,255,255,0.95);
@@ -433,11 +491,28 @@
top: 20px;
max-height: calc(100vh - 40px);
overflow-y: auto;
overscroll-behavior: contain;
padding: 8px 0 20px;
/* 不用 containcontain 会阻断滚动链接,侧栏滚到底(或内容没超出)时
鼠标在侧栏上滚轮会完全没反应,表现为"上下滚动没有呼应"。
保持默认的 auto,滚到底后自然接力给页面滚动。 */
overscroll-behavior: auto;
/* 预留滚动条位置,出现滚动条时内容不横向抖动 */
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: var(--text-soft) transparent;
padding: 8px 6px 20px 0;
/* 关键:侧栏本身是 sticky,会生成独立层叠上下文。
若不提升层级,内部弹出的主题菜单(z-index:50)会被锁在该上下文里,
而内容区卡片图标带 z-index:1,反而会画到菜单之上。提到 20 即可压过内容区。 */
z-index: 20;
}
.side-nav::-webkit-scrollbar { width: 4px; }
.side-nav::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* 滚动条要看得见,否则内容被截断时用户不知道还能往下滚 */
.side-nav::-webkit-scrollbar { width: 6px; }
.side-nav::-webkit-scrollbar-track { background: transparent; }
.side-nav::-webkit-scrollbar-thumb {
background: var(--text-soft);
border-radius: 3px;
}
.side-nav::-webkit-scrollbar-thumb:hover { background: var(--brand); }
.side-block {
background: var(--card-bg);
@@ -597,8 +672,12 @@
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.18);
}
.card-mine .visit-count { color: #8b5cf6; }
.cat-nav-inner a.cat-nav-mine { color: #8b5cf6; font-weight: 600; }
.cat-drawer-list a.cat-drawer-mine { color: #8b5cf6; font-weight: 600; }
.cat-drawer-list a.cat-drawer-mine,
.cat-drawer-list a.cat-drawer-mine .cat-icon { color: #8b5cf6; font-weight: 600; }
.cat-drawer-list a.cat-drawer-hot,
.cat-drawer-list a.cat-drawer-hot .cat-icon { color: #ff6b35; font-weight: 600; }
.card .icon-wrap {
position: relative;
@@ -647,12 +726,39 @@
-webkit-box-orient: vertical;
min-height: 20px;
}
/* 描述与网址共用同一行:默认显示描述,悬停时交叉淡出为网址。
两者用 grid 叠在同一格,卡片高度不再被两行文字撑开。 */
.card .meta-line {
display: grid;
min-height: 20px;
margin-top: 2px;
}
.card .meta-line > .desc,
.card .meta-line > .url {
grid-area: 1 / 1;
min-width: 0;
align-self: center;
transition: opacity 0.18s ease;
}
.card .meta-line > .url {
opacity: 0;
pointer-events: none;
}
.card:hover .meta-line > .desc,
.card:focus-visible .meta-line > .desc { opacity: 0; }
.card:hover .meta-line > .url,
.card:focus-visible .meta-line > .url { opacity: 1; }
.card .url {
font-size: 12px; color: var(--text-muted);
font-size: 12px; color: var(--brand-2);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
direction: rtl;
text-align: left;
}
/* 触屏设备没有 hover,直接展示网址,避免描述永远盖住它 */
@media (hover: none) {
.card .meta-line > .desc { opacity: 0; }
.card .meta-line > .url { opacity: 1; }
}
.card .visit-count {
margin-top: auto;
font-size: 11px;
@@ -721,7 +827,7 @@
border-radius: 6px;
overflow-x: auto;
}
[data-theme="dark"] .about-content pre { background: rgba(255,255,255,0.06); }
html[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;
@@ -819,26 +925,58 @@
@media (max-width: 600px) {
body { font-size: 14px; }
.container { padding: 0 14px; }
.topbar { padding: 12px 0; }
.topbar { padding: 10px 0 12px; }
.topbar-inner {
padding: 0 14px;
display: flex;
align-items: center;
gap: 10px;
gap: 8px;
}
/* 手机顶栏:第一行 = 分类 + 搜索 + 主题;第二行 = 站点名 + 副标题。
注意 flex 的换行发生在收缩之前,所以 .topbar-actions 必须用
flex-basis: 0 + min-width: 0;否则它会先按搜索框的固有宽度(约 280px)
判定"放不下"而整块换行,之后再缩小——表现就是三个控件挤成两行。 */
.cat-trigger-mobile {
display: inline-flex;
width: 38px; height: 38px;
font-size: 18px;
order: 1;
}
.topbar-actions { flex: 1 1 0; min-width: 0; order: 2; gap: 8px; }
.topbar-brand {
display: flex;
align-items: baseline;
flex-wrap: nowrap;
gap: 8px;
flex: 1 1 100%;
min-width: 0;
order: 3;
margin-top: 8px;
}
.topbar h1 {
font-size: 17px; margin: 0; line-height: 1.3;
white-space: nowrap; flex-shrink: 0;
}
.topbar .sub {
font-size: 12px; opacity: 0.9; line-height: 1.3;
min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.topbar-brand { display: none; } /* 手机顶部只保留分类按钮+搜索 */
.cat-trigger-mobile { display: inline-flex; }
/* 顶部搜索框更紧凑,占满剩余空间 */
.site-search {
flex: 1;
flex: 1 1 auto;
min-width: 0;
margin-top: 0;
max-width: none;
padding: 3px 4px 3px 12px;
padding: 2px 2px 2px 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; }
.site-search i { font-size: 16px; margin-right: 8px; cursor: pointer; }
.site-search input[type="search"] { font-size: 14px; padding: 8px 0; width: 100%; }
/* 手机端靠键盘的「搜索」键或左侧放大镜提交,把横向空间全留给输入框 */
.site-search .search-btn { display: none; }
.top-theme-trigger { flex-shrink: 0; }
.top-theme-toggle { width: 38px; height: 38px; font-size: 17px; }
/* 手机端:横向分类 Tab 隐藏(用抽屉替代) */
.cat-nav { display: none; }
@@ -883,8 +1021,12 @@
.cat-drawer-panel {
left: 0; right: auto; top: 0; bottom: 0;
border-radius: 0 16px 16px 0;
/* 必须给显式宽度:只写 max-width 时面板会被内容撑成一条窄条(实测 173px)。 */
width: min(280px, 84vw);
max-width: none;
max-height: 100vh;
max-width: 280px;
overflow-y: auto;
overscroll-behavior: contain;
transform: translateX(-100%);
padding: 20px 16px 28px;
}
@@ -905,6 +1047,20 @@
}
.cat-drawer-list li a .arrow { margin-left: auto; }
/* 抽屉宽 280px,主题菜单(min-width 160px)作为 fixed 浮层无论贴左还是贴右
都会被挤出可视区,所以手机端改成在「切换主题」父项下方内联展开。 */
.cat-drawer .theme-menu {
position: static !important;
top: auto !important;
left: auto !important;
width: auto;
min-width: 0;
max-width: none;
margin: 4px 10px 10px;
background: var(--bg);
box-shadow: none;
}
/* 抽屉分区 */
.cat-drawer-section { margin-bottom: 18px; }
.cat-drawer-title {
@@ -935,12 +1091,33 @@
<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="topbar-actions">
{# 全局搜索框(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" id="search-icon" role="button" tabindex="0" aria-label="提交搜索"></i>
<input type="search" name="q" id="search-input" placeholder="输入书签关键词进行搜索" autocomplete="off" enterkeyhint="search" aria-label="搜索书签">
<button type="submit" class="search-btn" aria-label="搜索">搜索</button>
</form>
{# 顶栏主题切换:把原本藏在侧栏/抽屉的入口放到顶栏右侧,填充空白 #}
<div class="theme-picker-trigger top-theme-trigger">
<button type="button" class="top-theme-toggle" id="top-theme-toggle" aria-label="切换主题" title="切换主题">
<i class="layui-icon layui-icon-light"></i>
</button>
<ul class="theme-menu" id="top-theme-menu">
<li data-theme="default">蓝绿</li>
<li data-theme="sunset">暮色</li>
<li data-theme="forest">森林</li>
<li data-theme="dark">暗色</li>
<li data-theme="lavender">紫韵</li>
<li data-theme="ocean">深海</li>
<li data-theme="sky">青空</li>
<li data-theme="rose">玫红</li>
<li data-theme="auto">跟随系统</li>
</ul>
</div>
</div>
</div>
</header>
@@ -954,26 +1131,32 @@
<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>
{# 「我常访问」与「热门网址」合并为同一大类,右侧内容区仍保持两个独立分区 #}
{% if (my_navs is defined and my_navs) or (top_navs is defined and top_navs) %}
<div class="side-block side-mine-block side-hot-block">
<div class="side-title">
<i class="layui-icon {% if my_navs is defined and my_navs %}layui-icon-star-fill{% else %}layui-icon-fire{% endif %}"></i>
{% if (my_navs is defined and my_navs) and (top_navs is defined and top_navs) -%}
常用 &amp; 热门
{%- elif my_navs is defined and my_navs -%}
我的
{%- else -%}
热门
{%- endif %}
</div>
<ul class="side-list">
{% if my_navs is defined and my_navs %}
<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">
{% endif %}
{% if top_navs is defined and top_navs %}
<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>
{% endif %}
</ul>
</div>
{% endif %}
@@ -1052,11 +1235,21 @@
<button type="button" class="close" id="cat-drawer-close" aria-label="关闭">×</button>
</div>
{# 我常访问入口(仅首页,且必须传了 my_navs 才渲染)#}
{% if my_navs is defined and my_navs %}
<div class="cat-drawer-section">
<div class="cat-drawer-title"><i class="layui-icon layui-icon-star-fill"></i> 我的</div>
{# 我常访问」+「热门网址」合并为同一分区,与左侧侧栏保持一致 #}
{% if (my_navs is defined and my_navs) or (top_navs is defined and top_navs) %}
<div class="cat-drawer-section cat-drawer-featured">
<div class="cat-drawer-title">
<i class="layui-icon {% if my_navs is defined and my_navs %}layui-icon-star-fill{% else %}layui-icon-fire{% endif %}"></i>
{% if (my_navs is defined and my_navs) and (top_navs is defined and top_navs) -%}
常用 &amp; 热门
{%- elif my_navs is defined and my_navs -%}
我的
{%- else -%}
热门
{%- endif %}
</div>
<ul class="cat-drawer-list">
{% if my_navs is defined and my_navs %}
<li>
<a href="{{ anchor_base }}cat-mine" class="cat-drawer-mine">
<i class="layui-icon cat-icon layui-icon-star-fill"></i>
@@ -1064,15 +1257,8 @@
<span class="arrow"></span>
</a>
</li>
</ul>
</div>
{% endif %}
{# 热门网址入口(仅首页,且首页必须传 top_navs 才渲染)#}
{% if top_navs is defined and top_navs %}
<div class="cat-drawer-section">
<div class="cat-drawer-title"><i class="layui-icon layui-icon-fire"></i> 热门</div>
<ul class="cat-drawer-list">
{% endif %}
{% if top_navs is defined and top_navs %}
<li>
<a href="{{ anchor_base }}cat-top" class="cat-drawer-hot">
<i class="layui-icon cat-icon layui-icon-fire"></i>
@@ -1080,6 +1266,7 @@
<span class="arrow"></span>
</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
@@ -1403,13 +1590,42 @@
inner.insertBefore(a, inner.firstChild);
}
// 「我常访问」是 JS 事后补的:服务端渲染那一刻 my_navs 可能为空,
// 标题只会写成「热门」。补/删条目后要把标题和图标跟着纠正。
function retitleFeatured(el, text, icon){
if (!el) return;
var ico = el.querySelector('i');
el.textContent = text;
if (!ico) return;
ico.className = 'layui-icon ' + icon;
el.insertBefore(ico, el.firstChild);
}
function addDrawerEntry(){
var panel = document.querySelector('.cat-drawer-panel');
if (!panel || panel.querySelector('.cat-drawer-mine')) return;
// 「常用 & 热门」合并分区:往里插一条,别重建 section(会顶掉热门入口)
var host = panel.querySelector('.cat-drawer-featured .cat-drawer-list');
if (host) {
var href = '#cat-mine';
var sample = host.querySelector('a[href]');
if (sample) href = (sample.getAttribute('href') || '#cat-mine').replace(/cat-top$/, 'cat-mine');
var li = document.createElement('li');
li.innerHTML =
'<a href="' + href + '" class="cat-drawer-mine">' +
'<i class="layui-icon cat-icon layui-icon-star-fill"></i>' +
'<span>我常访问</span><span class="arrow">→</span></a>';
host.insertBefore(li, host.firstChild);
var dsec = host.closest('.cat-drawer-section');
retitleFeatured(dsec && dsec.querySelector('.cat-drawer-title'),
'常用 & 热门', 'layui-icon-star-fill');
return;
}
// 兜底:连合并分区都没有时才新建
var sec = document.createElement('div');
sec.className = 'cat-drawer-section';
sec.className = 'cat-drawer-section cat-drawer-featured';
sec.innerHTML =
'<div class="cat-drawer-title"><i class="layui-icon layui-icon-star-fill"></i> 我的</div>' +
'<div class="cat-drawer-title"><i class="layui-icon layui-icon-star-fill"></i> 常用 &amp; 热门</div>' +
'<ul class="cat-drawer-list"><li>' +
'<a href="#cat-mine" class="cat-drawer-mine">' +
'<i class="layui-icon cat-icon layui-icon-star-fill"></i>' +
@@ -1421,15 +1637,36 @@
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>' +
// 「常用 & 热门」合并块:优先往里插一条 li,不要重建整个块(会顶掉热门入口)
var block = nav.querySelector('.side-hot-block');
if (block) {
var ul = block.querySelector('.side-list');
if (ul) {
// 复用同块里已有链接的 href 前缀,非首页时才会带上 /site/ 前缀
var href = '#cat-mine';
var sample = ul.querySelector('a[data-side-anchor]');
if (sample) href = (sample.getAttribute('href') || '#cat-mine').replace(/cat-top$/, 'cat-mine');
var li = document.createElement('li');
li.innerHTML =
'<a href="' + href + '" 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>';
ul.insertBefore(li, ul.firstChild);
retitleFeatured(block.querySelector('.side-title'),
'常用 & 热门', 'layui-icon-star-fill');
return;
}
}
// 兜底:连合并块都没有(既没个人也没热门)时才新建独立块
var b = document.createElement('div');
b.className = 'side-block side-mine-block';
b.innerHTML =
'<div class="side-title"><i class="layui-icon layui-icon-star-fill"></i> 常用 &amp; 热门</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);
nav.insertBefore(b, nav.firstChild);
}
function removeEntries(){
@@ -1437,11 +1674,32 @@
if (n && n.parentNode) n.parentNode.removeChild(n);
var d = document.querySelector('.cat-drawer-mine');
if (d) {
var s = d.closest('.cat-drawer-section');
if (s && s.parentNode) s.parentNode.removeChild(s);
// 抽屉里也是「常用 & 热门」合并分区:只摘这一条 li,
// 整段删会把热门入口一起干掉。分区空了才移除分区本身。
var dli = d.closest('li');
var dsec = d.closest('.cat-drawer-section');
if (dli && dli.parentNode) dli.parentNode.removeChild(dli);
if (dsec && !dsec.querySelector('.cat-drawer-list li') && dsec.parentNode) {
dsec.parentNode.removeChild(dsec);
} else if (dsec && dsec.querySelector('.cat-drawer-hot')) {
// 只剩热门了,标题从「常用 & 热门」改回「热门」
retitleFeatured(dsec.querySelector('.cat-drawer-title'), '热门', 'layui-icon-fire');
}
}
// 侧栏是「常用 & 热门」合并块,只摘掉「我常访问」这一条 li;
// 整块删会把热门入口一起干掉。块空了才移除块本身。
var sm = document.querySelector('.side-mine');
if (sm) {
var sli = sm.closest('li');
var sblk = sm.closest('.side-block');
if (sli && sli.parentNode) sli.parentNode.removeChild(sli);
if (sblk && !sblk.querySelector('.side-list li') && sblk.parentNode) {
sblk.parentNode.removeChild(sblk);
} else if (sblk && sblk.querySelector('.side-hot')) {
// 只剩热门了,标题从「常用 & 热门」改回「热门」
retitleFeatured(sblk.querySelector('.side-title'), '热门', 'layui-icon-fire');
}
}
var sb = document.querySelector('.side-mine-block');
if (sb && sb.parentNode) sb.parentNode.removeChild(sb);
}
function render(){
@@ -1451,7 +1709,7 @@
var local = load();
var score = {};
// 1) 服务端已渲染出来的个人卡片(带 data-mine 加权分)
// 1) 服务端已渲染的「我常访问」卡片(带 data-mine 加权分)
var sec = document.getElementById('cat-mine');
if (sec) {
sec.querySelectorAll('.card[data-nav-id]').forEach(function(c){
@@ -1472,7 +1730,9 @@
.slice(0, MAX_ITEMS);
if (ranked.length < MIN_ITEMS) {
// 个人榜不足:整个「我常访问」分区隐藏,热门分区不受影响
if (sec) sec.style.display = 'none';
removeEntries();
return;
}
@@ -1483,8 +1743,9 @@
sec.innerHTML =
'<div class="section-header">' +
'<h2><i class="layui-icon layui-icon-star-fill"></i> 我常访问</h2>' +
'<div class="meta">只在这台设备上统计 · <a href="javascript:;" id="clear-mine">清除</a></div>' +
'</div><div class="grid" id="mine-grid"></div>';
'<div class="meta">只在这台设备上统计 · <a href="javascript:;" id="clear-mine">清除我的记录</a></div>' +
'</div>' +
'<div class="grid" id="mine-grid"></div>';
main.insertBefore(sec, main.firstChild);
addCatNavEntry();
addDrawerEntry();
@@ -1492,7 +1753,13 @@
}
sec.style.display = '';
var grid = sec.querySelector('.grid');
var grid = sec.querySelector('#mine-grid') || sec.querySelector('.grid');
if (!grid) {
grid = document.createElement('div');
grid.className = 'grid';
grid.id = 'mine-grid';
sec.appendChild(grid);
}
grid.innerHTML = '';
ranked.forEach(function(id){
var src = findSourceCard(id);
@@ -1520,6 +1787,7 @@
else fetch('/site/api/my-clear', {method:'POST', keepalive:true, credentials:'same-origin'});
} catch(err) {}
removeEntries();
// 只移除「我常访问」分区,热门分区(#cat-top)保持原样
var sec = document.getElementById('cat-mine');
if (sec && sec.parentNode) sec.parentNode.removeChild(sec);
});
@@ -1539,6 +1807,32 @@
var input = document.getElementById('search-input');
if (!input) return;
// 手机端右侧「搜索」按钮被 CSS 隐藏,改由左侧放大镜提交;
// 同时把冗长的 placeholder 缩短,避免窄屏被截成半句话。
var isPhone = window.matchMedia('(max-width: 600px)');
function applyPhoneTweaks(){
if (isPhone.matches) {
input.placeholder = '搜索书签';
} else {
input.placeholder = '输入书签关键词进行搜索';
}
}
applyPhoneTweaks();
if (isPhone.addEventListener) isPhone.addEventListener('change', applyPhoneTweaks);
else if (isPhone.addListener) isPhone.addListener(applyPhoneTweaks);
var searchIcon = document.getElementById('search-icon');
function submitByIcon(){
if (input.value.trim() && form) form.submit();
else input.focus();
}
if (searchIcon) {
searchIcon.addEventListener('click', submitByIcon);
searchIcon.addEventListener('keydown', function(e){
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); submitByIcon(); }
});
}
function filter(keyword){
keyword = (keyword || '').trim().toLowerCase();
var cards = document.querySelectorAll('.grid .card');
@@ -1626,10 +1920,14 @@
var mw = menu.offsetWidth, mh = menu.offsetHeight;
menu.style.display = prevDisplay;
menu.style.visibility = '';
var left = rect.right + 8;
if (left + mw > window.innerWidth - 8) left = rect.left - mw - 8;
var margin = 8;
var vw = window.innerWidth, vh = window.innerHeight;
var left = rect.right + 8; // 首选:触发元素右侧
if (left + mw > vw - margin) left = rect.left - mw - 8; // 右侧放不下:改左侧
// 左侧也放不下(窄屏 / 抽屉里父项几乎占满整宽):贴可视区右边缘,保证完整可见
if (left < margin) left = Math.max(margin, vw - mw - margin);
var top = rect.top;
if (top + mh > window.innerHeight - 8) top = Math.max(8, window.innerHeight - mh - 8);
if (top + mh > vh - margin) top = Math.max(margin, vh - mh - margin);
menu.style.top = top + 'px';
menu.style.left = left + 'px';
}
@@ -1660,6 +1958,32 @@
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeAll(); });
})();
// ===== 顶栏「切换主题」:弹出下拉菜单(与侧栏逻辑一致) =====
(function(){
var btn = document.getElementById('top-theme-toggle');
var menu = document.getElementById('top-theme-menu');
if (!btn || !menu) return;
function closeAll(){
document.querySelectorAll('.theme-menu.open').forEach(function(m){ m.classList.remove('open'); });
}
btn.addEventListener('click', function(e){
e.preventDefault();
e.stopPropagation();
var wasOpen = menu.classList.contains('open');
closeAll();
if (!wasOpen) { positionThemeMenu(btn, menu); menu.classList.add('open'); }
});
menu.addEventListener('click', function(e){
var li = e.target.closest('li[data-theme]');
if (!li) return;
e.stopPropagation();
applyTheme(li.getAttribute('data-theme'), true);
closeAll();
});
document.addEventListener('click', closeAll);
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeAll(); });
})();
// ===== 抽屉内「切换主题」:弹出下拉菜单 =====
(function(){
var btn = document.getElementById('drawer-theme-toggle');
@@ -1673,7 +1997,12 @@
e.stopPropagation();
var wasOpen = menu.classList.contains('open');
closeAll();
if (!wasOpen) { positionThemeMenu(btn, menu); menu.classList.add('open'); }
if (!wasOpen) {
positionThemeMenu(btn, menu);
menu.classList.add('open');
// 手机端是内联展开,9 个主题项会顶出屏幕底部,展开后滚进可视区
if (menu.scrollIntoView) menu.scrollIntoView({ block: 'nearest' });
}
});
menu.addEventListener('click', function(e){
var li = e.target.closest('li[data-theme]');
+5 -2
View File
@@ -43,8 +43,11 @@
<span class="ext-tag" title="外部链接"><i class="layui-icon layui-icon-link"></i></span>
{% endif %}
</div>
<div class="desc">{{ item.description or '暂无描述' }}</div>
<div class="url" title="{{ item.url }}">{{ item.url }}</div>
{# 描述与网址共用同一行:默认显示描述,悬停时交叉淡出为网址 #}
<div class="meta-line">
<div class="desc">{{ item.description or '暂无描述' }}</div>
<div class="url" title="{{ item.url }}">{{ item.url }}</div>
</div>
<div class="visit-count" data-nav-visit="{{ item.visit_count or 0 }}">
{%- set vc = item.visit_count or 0 -%}
{%- if vc >= 3 -%}访问 {{ '{:,}'.format(vc) }}
+6 -3
View File
@@ -23,8 +23,11 @@
<span class="ext-tag" title="外部链接"><i class="layui-icon layui-icon-link"></i></span>
{% endif %}
</div>
<div class="desc">{{ item.description or '暂无描述' }}</div>
<div class="url" title="{{ item.url }}">{{ item.url }}</div>
{# 描述与网址共用同一行:默认显示描述,悬停时交叉淡出为网址,避免两行同时占用卡片空间 #}
<div class="meta-line">
<div class="desc">{{ item.description or '暂无描述' }}</div>
<div class="url" title="{{ item.url }}">{{ item.url }}</div>
</div>
<div class="visit-count" data-nav-visit="{{ item.visit_count or 0 }}">
{%- if mine is not none -%}
我点过 {{ item.mine_count or 0 }} 次
@@ -69,7 +72,7 @@
<section class="section section-mine" id="cat-mine">
<div class="section-header">
<h2><i class="layui-icon layui-icon-star-fill"></i> 我常访问</h2>
<div class="meta">只在这台设备上统计 · <a href="javascript:;" id="clear-mine">清除</a></div>
<div class="meta">只在这台设备上统计 · <a href="javascript:;" id="clear-mine">清除我的记录</a></div>
</div>
<div class="grid" id="mine-grid">
{% for item in my_navs %}