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

@@ -0,0 +1,78 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
DEFAULT_HEADER_NAV,
HEADER_NAV_BUILTIN_KEYS,
isHeaderNavCustom,
normalizeHeaderNav,
type HeaderNavItem,
} from "./headerNav.ts";
describe("normalizeHeaderNav", () => {
it("缺行/非法输入回落内置默认三项", () => {
assert.deepEqual(normalizeHeaderNav(undefined), DEFAULT_HEADER_NAV);
assert.deepEqual(normalizeHeaderNav(null), DEFAULT_HEADER_NAV);
assert.deepEqual(normalizeHeaderNav("home"), DEFAULT_HEADER_NAV);
});
it("默认三项顺序与注册表一致", () => {
assert.equal(DEFAULT_HEADER_NAV.length, 3);
assert.deepEqual(
DEFAULT_HEADER_NAV.map((i) => (i.type === "builtin" ? i.key : "")),
[...HEADER_NAV_BUILTIN_KEYS],
);
});
it("显式空数组表示全部隐藏,不回落默认", () => {
assert.deepEqual(normalizeHeaderNav([]), []);
});
it("自定义顺序与内容原样保留", () => {
const items: HeaderNavItem[] = [
{ type: "builtin", key: "library" },
{ type: "custom", label: "关于", url: "/p/about", new_tab: false },
{ type: "builtin", key: "home" },
{ type: "custom", label: "官网", url: "https://example.com", new_tab: true },
];
assert.deepEqual(normalizeHeaderNav(items), items);
});
it("内置 key 去空白/小写容错、重复去重", () => {
assert.deepEqual(
normalizeHeaderNav([
{ type: "builtin", key: " HOME " },
{ type: "builtin", key: "home" },
{ type: "builtin", key: "library" },
]),
[
{ type: "builtin", key: "home" },
{ type: "builtin", key: "library" },
],
);
});
it("未知内置 key 与非法自定义项被剔除", () => {
assert.deepEqual(
normalizeHeaderNav([
{ type: "builtin", key: "nope" },
{ type: "custom", label: "", url: "/x" },
{ type: "custom", label: "坏地址", url: "javascript:alert(1)" },
{ type: "custom", label: "相对路径", url: "relative" },
{ type: "weird", key: "home" },
{ type: "custom", label: "合法", url: "/links" },
]),
[{ type: "custom", label: "合法", url: "/links", new_tab: false }],
);
});
it("自定义外链 new_tab 缺省为 true,站内为 false", () => {
const got = normalizeHeaderNav([
{ type: "custom", label: "外链", url: "https://example.com" },
{ type: "custom", label: "站内", url: "/p/about" },
]);
assert.equal(got.length, 2);
assert.ok(got.every(isHeaderNavCustom));
assert.equal((got[0] as { new_tab: boolean }).new_tab, true);
assert.equal((got[1] as { new_tab: boolean }).new_tab, false);
});
});