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:
+400
-71
@@ -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;
|
||||
/* 不用 contain:contain 会阻断滚动链接,侧栏滚到底(或内容没超出)时
|
||||
鼠标在侧栏上滚轮会完全没反应,表现为"上下滚动没有呼应"。
|
||||
保持默认的 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>
|
||||
|
||||
<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"></i>
|
||||
<input type="search" name="q" id="search-input" placeholder="输入书签关键词进行搜索" autocomplete="off" aria-label="搜索书签">
|
||||
{# 手机端会隐藏右侧「搜索」按钮,改由这个放大镜图标提交 #}
|
||||
<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) -%}
|
||||
常用 & 热门
|
||||
{%- 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">
|
||||
<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) -%}
|
||||
常用 & 热门
|
||||
{%- 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">
|
||||
<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> 常用 & 热门</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> 常用 & 热门</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]');
|
||||
|
||||
@@ -43,8 +43,11 @@
|
||||
<span class="ext-tag" title="外部链接"><i class="layui-icon layui-icon-link"></i></span>
|
||||
{% endif %}
|
||||
</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) }}
|
||||
|
||||
@@ -23,8 +23,11 @@
|
||||
<span class="ext-tag" title="外部链接"><i class="layui-icon layui-icon-link"></i></span>
|
||||
{% endif %}
|
||||
</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 %}
|
||||
|
||||
Reference in New Issue
Block a user