新增模块: - 友链(friendlink)后端处理 + 前端管理页与友链板 - 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot - 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉 - 排行榜 service 测试、站点页面测试、时间线发布(timeline_release) 其它改动: - 后端 handlers/services 全量小幅调整 - 前端组件、库函数、URL/品牌/站点 URL 工具更新 - Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
179 lines
5.9 KiB
TypeScript
179 lines
5.9 KiB
TypeScript
import type { Metadata } from "next";
|
||
import type { CSSProperties } from "react";
|
||
import Link from "next/link";
|
||
import { BookOpen, FileDown } from "lucide-react";
|
||
import { fetchLibraryList, type LibraryDocListItem } from "@/lib/api";
|
||
import { coverHue } from "@/lib/coverColor";
|
||
import { getPublicSettingsCached } from "@/lib/serverData";
|
||
import { requestCanonical } from "@/lib/canonicalMeta";
|
||
import { libraryPath, libraryDocPath } from "@/lib/urlStyle";
|
||
|
||
function formatSize(n: number): string {
|
||
if (!Number.isFinite(n) || n <= 0) return "0 B";
|
||
if (n < 1024) return `${n} B`;
|
||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
||
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
||
}
|
||
|
||
/** 文字封面:无图时用完整书名 + 书名派生稳定色相洗底(lib/coverColor.ts) */
|
||
function TextCover({ title }: { title: string }) {
|
||
const name = title.trim() || "书";
|
||
return (
|
||
<div
|
||
className="j13-text-cover w-full h-full flex items-center justify-center px-5 select-none"
|
||
style={{ "--cover-h": coverHue(name) } as CSSProperties}
|
||
aria-hidden
|
||
>
|
||
<span
|
||
className="text-[22px] font-extrabold leading-snug text-center line-clamp-3 break-all"
|
||
>
|
||
{name}
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ExtChip({ ext }: { ext: string }) {
|
||
return (
|
||
<span
|
||
className="text-[10.5px] font-semibold px-1.5 py-0.5 rounded-md uppercase tracking-wide"
|
||
style={{
|
||
color: "var(--accent)",
|
||
background: "var(--accent-soft)",
|
||
border: "1px solid color-mix(in srgb, var(--accent) 30%, transparent)",
|
||
}}
|
||
>
|
||
{ext}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function DocCard({
|
||
doc,
|
||
href,
|
||
}: {
|
||
doc: LibraryDocListItem;
|
||
href: string;
|
||
}) {
|
||
const exts = Array.from(new Set(doc.files.map((f) => f.ext))).slice(0, 4);
|
||
const totalSize = doc.files.reduce((acc, f) => acc + f.size, 0);
|
||
return (
|
||
<Link
|
||
href={href}
|
||
className="group flex flex-col overflow-hidden rounded-[20px] transition-transform hover:-translate-y-0.5"
|
||
style={{
|
||
background: "var(--panel)",
|
||
border: "1px solid var(--line)",
|
||
}}
|
||
>
|
||
<div className="h-36 overflow-hidden" style={{ borderBottom: "1px solid var(--line)" }}>
|
||
{doc.cover_url ? (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img
|
||
src={doc.cover_url}
|
||
alt={doc.title}
|
||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
|
||
loading="lazy"
|
||
decoding="async"
|
||
draggable={false}
|
||
/>
|
||
) : (
|
||
<TextCover title={doc.title} />
|
||
)}
|
||
</div>
|
||
<div className="p-4 flex flex-col gap-2 flex-1">
|
||
<h2
|
||
className="text-[15px] font-bold leading-snug line-clamp-2"
|
||
style={{ color: "var(--ink)" }}
|
||
>
|
||
{doc.title}
|
||
</h2>
|
||
<div className="mt-auto flex flex-wrap items-center gap-1.5">
|
||
{exts.map((ext) => (
|
||
<ExtChip key={ext} ext={ext} />
|
||
))}
|
||
<span className="meta text-[11px] ml-auto inline-flex items-center gap-1">
|
||
<FileDown size={11} />
|
||
{doc.file_count > 0 ? `${doc.file_count} 个文件 · ${formatSize(totalSize)}` : "暂无文件"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</Link>
|
||
);
|
||
}
|
||
|
||
export async function generateMetadata(): Promise<Metadata> {
|
||
const settings = await getPublicSettingsCached();
|
||
return {
|
||
title: "书库",
|
||
description: settings.site_description,
|
||
...(await requestCanonical(libraryPath(settings.url_style))),
|
||
};
|
||
}
|
||
|
||
export default async function LibraryPage() {
|
||
const settings = await getPublicSettingsCached();
|
||
const urlStyle = settings.url_style;
|
||
|
||
// 接口失败降级为空态,不让页面 500
|
||
let docs: LibraryDocListItem[] = [];
|
||
try {
|
||
const res = await fetchLibraryList();
|
||
docs = res.docs ?? [];
|
||
} catch {
|
||
/* 降级空态 */
|
||
}
|
||
|
||
return (
|
||
<div className="min-w-0">
|
||
{/* Hero:渐变洗底(与排行榜同语言) */}
|
||
<section
|
||
className="relative overflow-hidden rounded-[28px] px-6 py-8 sm:px-10 sm:py-10 text-center"
|
||
style={{
|
||
background:
|
||
"radial-gradient(120% 160% at 12% -10%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 55%)," +
|
||
"radial-gradient(90% 140% at 90% -20%, color-mix(in srgb, var(--accent) 8%, transparent), transparent 50%)",
|
||
border: "1px solid var(--line)",
|
||
}}
|
||
>
|
||
<div
|
||
aria-hidden
|
||
className="absolute -top-16 -right-10 w-56 h-56 rounded-full pointer-events-none"
|
||
style={{
|
||
background: "color-mix(in srgb, var(--accent) 14%, transparent)",
|
||
filter: "blur(48px)",
|
||
}}
|
||
/>
|
||
<div className="relative">
|
||
<h1
|
||
className="text-[26px] sm:text-[32px] leading-tight font-extrabold tracking-tight inline-flex items-center gap-2.5"
|
||
style={{ color: "var(--ink)" }}
|
||
>
|
||
<BookOpen size={26} style={{ color: "var(--accent)" }} />
|
||
书库
|
||
</h1>
|
||
<p className="meta mt-2 text-[13.5px]">书籍、文档与页面作品,在线阅读或下载</p>
|
||
</div>
|
||
</section>
|
||
|
||
{docs.length === 0 ? (
|
||
<div
|
||
className="mt-6 rounded-[20px] px-6 py-14 text-center"
|
||
style={{ background: "var(--panel)", border: "1px solid var(--line)" }}
|
||
>
|
||
<p className="text-[14px] font-medium" style={{ color: "var(--ink-2)" }}>
|
||
书库还没有内容
|
||
</p>
|
||
<p className="meta text-[12.5px] mt-1.5">管理员可在后台「书库管理」发布条目</p>
|
||
</div>
|
||
) : (
|
||
<div className="mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||
{docs.map((d) => (
|
||
<DocCard key={d.id} doc={d} href={libraryDocPath(urlStyle, d.slug)} />
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|