feat: 友链/书库/移动端导航/排行榜等多模块功能

新增模块:
- 友链(friendlink)后端处理 + 前端管理页与友链板
- 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot
- 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉
- 排行榜 service 测试、站点页面测试、时间线发布(timeline_release)

其它改动:
- 后端 handlers/services 全量小幅调整
- 前端组件、库函数、URL/品牌/站点 URL 工具更新
- Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
This commit is contained in:
2026-09-30 16:30:07 +08:00
parent e487bc2e92
commit 4f3f3a265c
105 changed files with 12371 additions and 852 deletions

View File

@@ -215,6 +215,9 @@
}
html {
/* 恒定保留滚动条槽位:短页面(如书库)无滚动条时视口宽度不再比长页面宽 10px,
避免 Header 居中容器与右侧按钮组在切页时横移 */
scrollbar-gutter: stable;
scrollbar-color: var(--line-2) transparent;
background-color: var(--bg); /* 橡皮筋 overscroll 与画布同色 */
--j13-bg-site-image: none;
@@ -356,6 +359,23 @@ html[data-lv-fx="off"] .lv-text {
-webkit-user-select: none;
}
/* 无封面文本封面:颜色由书名派生的稳定色相 --cover-h 决定(lib/coverColor.ts),
同一本书任何页面/刷新同色;浅色=淡彩底深字,深色=暗底亮字 */
.j13-text-cover {
--cover-h: 210;
color: hsl(var(--cover-h) 45% 34%);
background:
radial-gradient(120% 140% at 20% -10%, hsl(var(--cover-h) 85% 55% / 0.2), transparent 60%),
hsl(var(--cover-h) 60% 95.5%);
}
.dark .j13-text-cover {
color: hsl(var(--cover-h) 65% 78%);
background:
radial-gradient(120% 140% at 20% -10%, hsl(var(--cover-h) 70% 62% / 0.22), transparent 60%),
hsl(var(--cover-h) 28% 16%);
}
/* 全站悬停即显提示气泡(配合 TipHost + data-tip,替代原生 title 延迟) */
.j13-tip-bubble {
position: fixed;
@@ -1967,6 +1987,10 @@ html.dark .j13-board-trigger.is-invalid .j13-board-trigger-hint {
border-radius: 10px;
color: var(--ink-2);
transition: background 0.12s ease, color 0.12s ease;
/* 作为 flex 子元素时保持固定宽度,避免被压缩或按内容计算,
否则窄屏下直接挂在 .j13-md-tool-groups 内的按钮会塌缩成图标宽度(如 15px),
而被 .j13-md-popover-anchor 包裹的按钮保持 34px,导致整行按钮宽度不一致 */
flex: 0 0 auto;
}
.j13-md-tool:hover:not(:disabled) {
background: var(--accent-soft);
@@ -2397,6 +2421,38 @@ html.dark .j13-board-trigger.is-invalid .j13-board-trigger-hint {
height: 32px;
padding: 0 8px;
}
/* 窄屏(手机端)编辑工具栏:收成单行核心工具 + 「更多」菜单(评论栏与正文编辑器共用) */
.j13-md-editor.is-narrow .j13-md-toolbar {
flex-wrap: nowrap;
gap: 2px;
padding: 2px 0 8px;
}
.j13-md-editor.is-narrow .j13-md-tool-groups {
flex-wrap: nowrap;
}
.j13-md-editor.is-narrow .j13-md-tool {
width: 30px;
height: 30px;
}
.j13-md-editor.is-narrow .j13-md-sep {
/* 窄屏下 1px 分隔符视觉几乎不可见却占 5px 横向空间,
反而打断按钮间均匀间距;改由 gap-0.5 统一控制间距 */
display: none;
}
.j13-md-editor.is-narrow .j13-md-mode button {
height: 28px;
padding: 0 7px;
font-size: 12px;
}
/* 「更多」内嵌的代码块/表情/隐藏内容弹层:相对更多按钮居中,避免贴边溢出屏幕 */
.j13-md-popover-center {
left: 50%;
transform: translateX(-50%);
}
/* 窄屏左锚点表单弹层(链接等)宽度收敛,保证完整落在屏内 */
.j13-md-editor.is-narrow .j13-md-popover-form {
min-width: 232px;
}
/* 正文内嵌隐藏块 */
.j13-hide-block {
@@ -2857,9 +2913,10 @@ body:has([data-compose-viewport]) > footer {
min-width: 0;
}
/* <1024px(移动/平板,底部公共菜单显示区间):三区堆叠、统一左对齐连成整体,
避免「左中右各自断开」的残缺观感;底部避让公共菜单 */
避免「左中右各自断开」的残缺观感;底栏避让统一由 footer 承担(:has 判定),
#main 只留常规小 padding-bottom,无底栏页面(如全部通知)底部也有间隙 */
@media (max-width: 1023px) {
.site-footer {
body:has([data-j13-tabbar]) .site-footer {
padding-bottom: calc(4.75rem + env(safe-area-inset-bottom));
}
.site-footer-inner {
@@ -2881,6 +2938,15 @@ body:has([data-compose-viewport]) > footer {
}
}
/* 通知下拉(移动端底部面板)避让底部公共菜单:面板在 sticky header 的层叠上下文内,
z-index 抬不过底栏,只能在底栏存在时整体抬高(与 footer 的避让值一致),
露出被遮住的「查看全部通知」入口 */
@media (max-width: 1023.98px) {
body:has([data-j13-tabbar]) .j13-notif-sheet {
bottom: calc(4.75rem + env(safe-area-inset-bottom));
}
}
body:has([data-compose-viewport]) #main {
flex: 1 1 0;
min-height: 0;
@@ -3103,7 +3169,8 @@ body:has([data-compose-viewport]) #main > [data-compose-viewport] {
.j13-compose-typemodal-body {
max-height: min(78vh, 42rem);
overflow-y: auto;
padding-right: 2px;
/* 滚动容器会裁剪 box-shadow 聚焦圈(2px),四周各留 2px */
padding: 2px;
}
.j13-compose-typemodal-body .j13-compose-typepanel {
padding: 0;
@@ -3197,6 +3264,13 @@ body:has([data-chat-viewport]) #main {
overflow: hidden;
}
/* 消息列表页显示底栏时,锁视口布局底部避让(/chat/[id] 无底栏不受影响) */
@media (max-width: 1023.98px) {
body:has([data-j13-tabbar]) #main:has([data-chat-viewport]) {
padding-bottom: calc(4.75rem + env(safe-area-inset-bottom));
}
}
/* ── 管理后台独立壳:全高侧栏 + 顶栏 + 可滚主区(不依赖公开 Header/Footer) ── */
body:has([data-admin-viewport]) {
height: 100dvh;
@@ -3809,6 +3883,19 @@ a.j13-mention-token:hover {
border-radius: var(--r);
box-shadow: var(--shadow-sm);
}
/* 阅读型页面(公文壳 / 书库读页)手机端中栏去块化:
直接子级 panel 贴边铺满(-mx-4 抵消 #main 移动端 px-4),与帖子详情页移动端同款;
仅作用于直接子级,正文内的条目卡等嵌套 panel 不受影响 */
@media (max-width: 639.98px) {
.j13-doc-bleed > .panel {
margin-left: -1rem;
margin-right: -1rem;
border-radius: 0;
border-left-width: 0;
border-right-width: 0;
box-shadow: none;
}
}
.hover-lift {
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
@@ -4444,6 +4531,30 @@ a.j13-mention-token:hover {
align-items: center;
gap: 0.3rem;
}
/* 品牌图固定对外地址:点击复制,友链引用用 */
.admin-brand-alias {
display: flex;
align-items: center;
gap: 0.3rem;
min-width: 0;
padding: 0.2rem 0.45rem;
border-radius: 8px;
box-shadow: inset 0 0 0 1px var(--line);
font-size: 11.5px;
color: var(--ink-2);
text-align: left;
}
.admin-brand-alias:hover {
color: var(--ink);
box-shadow: inset 0 0 0 1px var(--accent, var(--line));
}
.admin-brand-alias-url {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-jetbrains), ui-monospace, monospace;
}
.admin-settings-row {
display: grid;
grid-template-columns: 10.75rem minmax(0, 1fr);
@@ -4601,13 +4712,17 @@ a.j13-mention-token:hover {
#settings-identity .admin-settings-actions,
#settings-brand .admin-settings-actions,
#settings-url-style .admin-settings-actions,
#settings-footer .admin-settings-actions {
#settings-footer .admin-settings-actions,
#settings-header-nav .admin-settings-actions,
#settings-mobile-tabbar .admin-settings-actions {
grid-template-columns: minmax(0, 1fr);
}
#settings-identity .admin-settings-actions-inner,
#settings-brand .admin-settings-actions-inner,
#settings-url-style .admin-settings-actions-inner,
#settings-footer .admin-settings-actions-inner {
#settings-footer .admin-settings-actions-inner,
#settings-header-nav .admin-settings-actions-inner,
#settings-mobile-tabbar .admin-settings-actions-inner {
grid-column: 1;
}
.admin-settings-keywords {
@@ -5942,6 +6057,31 @@ a.j13-mention-token:hover {
height: auto;
}
}
/* 书库管理:左列表右编辑;桌面整页不滚(复用 admin-content-page 的 canvas 锁定),
两栏各自内部滚动;窄屏按 data-view 切换单栏 */
.admin-library-split {
display: grid;
gap: 0.85rem;
align-items: stretch;
min-height: 18rem;
}
@media (min-width: 1024px) {
.admin-library-split {
grid-template-columns: minmax(15rem, 21rem) minmax(0, 1fr);
gap: 1rem;
flex: 1 1 auto;
min-height: 0;
height: auto;
}
}
@media (max-width: 1023.98px) {
.admin-library-split[data-view="editor"] .admin-library-list {
display: none;
}
.admin-library-split[data-view="list"] .admin-library-editor {
display: none;
}
}
.admin-messages-panel {
flex: 1 1 auto;
min-height: 0;
@@ -6679,7 +6819,7 @@ a.j13-mention-token:hover {
display: inline-flex;
align-items: center;
gap: 0.45rem;
margin: 0 0 0.65rem -0.1rem;
margin: 0;
padding: 0.32rem 0.75rem 0.32rem 0.6rem;
border-radius: 999px;
background: var(--ok-soft);
@@ -6691,6 +6831,7 @@ a.j13-mention-token:hover {
line-height: 1.35;
position: relative;
z-index: 1;
flex-shrink: 0;
}
.j13-timeline-day-label time {
color: var(--ok);
@@ -6748,19 +6889,29 @@ a.j13-mention-token:hover {
}
.j13-timeline-head {
display: flex;
align-items: flex-start;
align-items: center;
flex-wrap: wrap;
gap: 0.45rem;
}
.j13-timeline-summary {
margin: 0;
flex: 1;
flex: 1 1 auto;
min-width: 0;
font-size: 13.5px;
font-weight: 500;
line-height: 1.5;
font-size: 15px;
font-weight: 800;
line-height: 1.4;
color: var(--ink);
letter-spacing: 0.01em;
word-break: break-word;
}
/* 当天非首条:版本号紧贴左侧,与首条版本号对齐(不重复日期) */
.j13-timeline-item:not(.is-first-in-day) .j13-timeline-summary {
margin-left: 0;
}
/* 首条目卡片上方留一点呼吸,与日期胶囊拉开层次 */
.j13-timeline-item.is-first-in-day .j13-timeline-card {
padding-top: 0.15rem;
}
.j13-timeline-chip {
display: inline-flex;
align-items: center;
@@ -7925,7 +8076,7 @@ a.j13-mention-token:hover {
/* 移动端横向板块条(滚动条隐藏,保留触控惯性) */
.j13-chips {
display: flex;
gap: 8px;
gap: 5px;
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
@@ -7943,7 +8094,7 @@ a.j13-mention-token:hover {
/* 无框 chip:透明边框占位,选中/悬停切底色时尺寸不跳 */
border: 1px solid transparent;
background: transparent;
border-radius: 999px;
border-radius: 10px;
font-size: 13px;
font-weight: 500;
color: var(--ink-2);
@@ -7979,7 +8130,7 @@ a.j13-mention-token:hover {
opacity: 1;
}
.j13-chip-fade-left {
left: -1rem;
left: 0;
border-radius: 1rem 0 0 1rem;
background: linear-gradient(
to right,
@@ -7988,7 +8139,7 @@ a.j13-mention-token:hover {
);
}
.j13-chip-fade-right {
right: -1rem;
right: 0;
border-radius: 0 1rem 1rem 0;
background: linear-gradient(
to left,
@@ -8068,10 +8219,11 @@ a.j13-mention-token:hover {
}
.prose-content .j13-timeline-summary {
margin: 0;
font-size: 13.5px;
font-weight: 500;
line-height: 1.5;
font-size: 15px;
font-weight: 800;
line-height: 1.4;
color: var(--ink);
letter-spacing: 0.01em;
}
.prose-content .j13-timeline-body,
.prose-content .j13-timeline-body p {
@@ -8079,6 +8231,29 @@ a.j13-mention-token:hover {
line-height: 1.55;
color: var(--ink-3);
}
/* 条目卡嵌在 prose 内:压住字段值/摘要的段落与列表外边距(未分层规则才能胜过
.prose-content p 的 0.9rem 外边距),否则字段内容被推低、与左侧标签错位 */
.prose-content .j13-entry-card dd p,
.prose-content .j13-entry-card .j13-entry-summary p {
margin: 0;
}
.prose-content .j13-entry-card dd ul,
.prose-content .j13-entry-card dd ol {
margin: 0.35rem 0;
}
.prose-content .j13-entry-card dd li {
margin: 0.15rem 0;
}
/* 条目卡标题字号:移动端收小(约 h3 水平),sm 起与 prose h2 同大;
卡内 h2 用 inline style 引用该变量,避免被上面的 .prose-content h2 压过 */
.j13-entry-card h2 {
--entry-title-size: 1.18rem;
}
@media (min-width: 640px) {
.j13-entry-card h2 {
--entry-title-size: 1.4rem;
}
}
.prose-content h1 { font-size: 1.75rem; font-weight: 800; margin: 1.6rem 0 0.8rem; }
.prose-content h2 {
font-size: 1.4rem;
@@ -8619,8 +8794,8 @@ html.dark .prose-content pre[data-theme] span {
前台 main 纸面 + 首页三栏「白纸一体」
main 铺 --main-bg(浅色纯白 / 暗色比画布略亮一档),左右外缘
1px --line 描边与页头下边线围合成框;站长配置了自定义站点
背景图时让位(保持透出画布)。左栏与窄屏下沉面板无框化,
内容直接坐在纸面上;右栏保留卡片板块感(j13-module 原样)。
背景图时让位(保持透出画布)。左栏无框化,内容直接坐在纸面上;
右栏保留卡片板块感(j13-module 原样)。
============================================================ */
html:not(.j13-has-site-bg) #main {
background: var(--main-bg);
@@ -8637,8 +8812,7 @@ html:not(.j13-has-site-bg) #main {
padding: 0;
}
/* 左栏小节分隔:任意块相邻时画发丝线(含站长自定义工具块) */
.j13-rail-left > * + *,
.j13-fluid-home .j13-flat-rail > * + * {
.j13-rail-left > * + * {
margin-top: 1.25rem;
padding-top: 1.25rem;
border-top: 1px solid var(--line);
@@ -8654,8 +8828,7 @@ html:not(.j13-has-site-bg) #main {
height: 14px;
}
/* 栏内图标无底色:只留彩色字形本身(收成图标实际大小,避免隐形占位拉大间距) */
.j13-rail-left .j13-bcard-ico,
.j13-fluid-home .j13-flat-rail .j13-bcard-ico {
.j13-rail-left .j13-bcard-ico {
background: transparent !important; /* 覆盖「全部讨论/管理面板」图标的内联底色 */
border-radius: 0;
width: 20px;
@@ -8696,25 +8869,16 @@ html:not(.j13-has-site-bg) #main {
}
}
/* 左栏分组标题:小号灰字,纸面风格的段落断点 */
.j13-rail-left .j13-rail-group,
.j13-fluid-home .j13-flat-rail .j13-rail-group {
.j13-rail-left .j13-rail-group {
padding: 10px 8px 4px;
font-size: 11px;
font-weight: 500;
color: var(--ink-3);
letter-spacing: 0.02em;
}
.j13-rail-left .j13-rail-group:first-child,
.j13-fluid-home .j13-flat-rail .j13-rail-group:first-child {
.j13-rail-left .j13-rail-group:first-child {
padding-top: 2px;
}
/* 手机端下沉面板:推广占位放开到整栏宽(248 定宽是桌面右栏尺寸,
窄屏居中两侧大片空白显得稀疏);/ads/buy 购买页预览保持真实尺寸不受影响 */
@media (max-width: 1023px) {
.j13-fluid-home .j13-flat-rail .j13-ad-promo {
width: 100%;
}
}
/* 左栏板块导航:当前项 accent 竖条 + 浅色底 */
.j13-rail-left .j13-module .j13-bcard {
position: relative;