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

其它改动:
- 后端 handlers/services 全量小幅调整
- 前端组件、库函数、URL/品牌/站点 URL 工具更新
- Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
2026-09-30 16:30:07 +08:00

89 lines
3.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 桌面端 Header 主导航配置的单一事实源:
* 与后端 service.HeaderNavBuiltinKeys 保持一致;Header 渲染、后台管理面板、公共设置 normalize 共用。
*
* 配置为有序数组,数组顺序即展示顺序:
* - builtin 只存 key,文案/图标/地址/高亮由前端注册表派生(地址随伪静态风格变化);
* - custom 存文案 + 地址(站内路径或 http(s) 外链)。
* 缺行/非法输入回落内置默认三项;显式空数组表示全部隐藏(前台不渲染导航)。
* 后续新增内置页面:在本注册表与后端白名单各加一项,后台自动出现可选项。
*/
import { footerURLAllowed, isExternalFooterURL, runeCount } from "./brand.ts";
export const HEADER_NAV_BUILTIN_KEYS = ["home", "leaderboard", "library"] as const;
export type HeaderNavBuiltinKey = (typeof HEADER_NAV_BUILTIN_KEYS)[number];
/** 内置菜单默认文案;后台面板用同一文案 */
export const HEADER_NAV_BUILTIN_LABELS: Record<HeaderNavBuiltinKey, string> = {
home: "首页",
leaderboard: "排行榜",
library: "书库",
};
export type HeaderNavBuiltinItem = { type: "builtin"; key: HeaderNavBuiltinKey };
export type HeaderNavCustomItem = {
type: "custom";
label: string;
url: string;
new_tab: boolean;
};
export type HeaderNavItem = HeaderNavBuiltinItem | HeaderNavCustomItem;
export const HEADER_NAV_LIMITS = {
items: 8,
label: 16,
url: 200,
} as const;
/** 内置默认三项(缺行时回落) */
export const DEFAULT_HEADER_NAV: HeaderNavItem[] = HEADER_NAV_BUILTIN_KEYS.map((key) => ({
type: "builtin" as const,
key,
}));
export function isHeaderNavCustom(item: HeaderNavItem): item is HeaderNavCustomItem {
return item.type === "custom";
}
/**
* 宽容解析服务端配置:未知内置 key/重复内置/非法自定义项跳过;
* 入参非数组回落默认;空数组原样返回(全部隐藏)。
*/
export function normalizeHeaderNav(raw: unknown): HeaderNavItem[] {
if (!Array.isArray(raw)) return DEFAULT_HEADER_NAV.map((item) => ({ ...item }));
const out: HeaderNavItem[] = [];
const seenBuiltin = new Set<string>();
for (const entry of raw) {
if (!entry || typeof entry !== "object") continue;
const rec = entry as Record<string, unknown>;
const type = String(rec.type ?? "").trim();
if (type === "builtin") {
const key = String(rec.key ?? "")
.trim()
.toLowerCase();
if (!(HEADER_NAV_BUILTIN_KEYS as readonly string[]).includes(key) || seenBuiltin.has(key)) {
continue;
}
seenBuiltin.add(key);
out.push({ type: "builtin", key: key as HeaderNavBuiltinKey });
} else if (type === "custom") {
const label = String(rec.label ?? "").trim();
const url = String(rec.url ?? "").trim();
if (!label || runeCount(label) > HEADER_NAV_LIMITS.label) continue;
if (!footerURLAllowed(url)) continue;
const omitted = rec.new_tab === undefined || rec.new_tab === null;
const newTab = omitted ? isExternalFooterURL(url) : rec.new_tab !== false;
out.push({ type: "custom", label, url, new_tab: newTab });
}
if (out.length >= HEADER_NAV_LIMITS.items) break;
}
return out;
}
/** 自定义菜单是否为外链(外链渲染 target=_blank) */
export function isHeaderNavExternal(url: string): boolean {
return isExternalFooterURL(url.trim());
}