feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等

新增:
- 书库导入导出(library_import/library_export)及测试
- 图片变体生成(image_variants)与响应式图片(responsiveImage)
- 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件
- 小组件运行时(widgetRuntime)与静态检查(widgetLint)
- 上传缓存中间件(upload_cache)与字体 CSS 提取脚本

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
This commit is contained in:
2026-10-01 03:06:05 +08:00
parent 7fbd6ba699
commit ff2ab286fb
141 changed files with 7527 additions and 1407 deletions

View File

@@ -46,6 +46,7 @@ import Modal from "@/components/Modal";
import { AdText } from "@/components/AdText";
import { formatRelative } from "@/lib/format";
import { toast } from "@/lib/toast";
import { densitySrcSet, isOwnImage, pickVariantWidth, variantURL } from "@/lib/responsiveImage";
type StatusFilter = "" | "pending" | "active" | "rejected" | "expired";
type SectionId = "overview" | "queue" | "promote" | "sponsors";
@@ -882,8 +883,14 @@ function AdCard({
{/* 预览缩略 */}
<div className="j13-ads-thumb shrink-0" aria-hidden>
{ad.kind === "image" && ad.image_url ? (
// 88×44 缩略位:本站图走 96/160 密度变体;外链图保持原地址
// eslint-disable-next-line @next/next/no-img-element
<img src={ad.image_url} alt="" className="h-full w-full object-contain" />
<img
src={isOwnImage(ad.image_url) ? variantURL(ad.image_url, pickVariantWidth(88)) : ad.image_url}
srcSet={isOwnImage(ad.image_url) ? densitySrcSet(ad.image_url, 88) : undefined}
alt=""
className="h-full w-full object-contain"
/>
) : ad.kind === "text" && ad.title ? (
<div className="flex h-full w-full items-center justify-center p-1.5">
<AdText title={ad.title} textColor={ad.text_color} bgColor={ad.bg_color} />
@@ -990,8 +997,14 @@ function PaymentEditor({
{/* 二维码:点击即上传 */}
<label className="j13-ads-qrbox shrink-0 cursor-pointer" title="点击上传/更换收款码">
{payment.qr_url ? (
// 96×96 收款码位:本站图走 96/160 变体
// eslint-disable-next-line @next/next/no-img-element
<img src={payment.qr_url} alt="" className="h-full w-full object-contain" />
<img
src={isOwnImage(payment.qr_url) ? variantURL(payment.qr_url, pickVariantWidth(96)) : payment.qr_url}
srcSet={isOwnImage(payment.qr_url) ? densitySrcSet(payment.qr_url, 96) : undefined}
alt=""
className="h-full w-full object-contain"
/>
) : (
<span className="flex flex-col items-center gap-1 text-[11px]" style={{ color: "var(--ink-3)" }}>
<Upload size={16} />
@@ -1084,8 +1097,14 @@ function SponsorEditor({
{/* Logo:点击即上传 */}
<label className="j13-ads-logo shrink-0 cursor-pointer" title="点击上传/更换 Logo">
{item.logo_url ? (
// 120×56 Logo 位:本站图走 160/256 变体
// eslint-disable-next-line @next/next/no-img-element
<img src={item.logo_url} alt="" className="h-full w-full rounded-lg object-contain" />
<img
src={isOwnImage(item.logo_url) ? variantURL(item.logo_url, pickVariantWidth(120)) : item.logo_url}
srcSet={isOwnImage(item.logo_url) ? densitySrcSet(item.logo_url, 120) : undefined}
alt=""
className="h-full w-full rounded-lg object-contain"
/>
) : (
<span className="flex flex-col items-center gap-1 text-[11px]" style={{ color: "var(--ink-3)" }}>
<Upload size={15} />

View File

@@ -12,7 +12,7 @@ import {
type SponsorsConfig,
} from "@/lib/api";
import { getMeCached } from "@/lib/serverData";
import { isAdminOrAbove } from "@/lib/roles";
import { hasPerm, PERMS } from "@/lib/roles";
import Forbidden from "../Forbidden";
export const metadata: Metadata = {
@@ -22,7 +22,7 @@ export const metadata: Metadata = {
export default async function AdminAdsPage() {
const cookie = authCookieHeader(await cookies());
const me = await getMeCached(cookie || undefined);
if (!me.user || !isAdminOrAbove(me.user.role)) {
if (!me.user || !hasPerm(me.user, PERMS.ADS)) {
return <Forbidden text="推广与赞助管理仅管理员及以上角色可操作。" />;
}