Adapters (pure functions + tests): - detail-description: strip HTML/BBCode residue, drop site template blocks and ad/signature lines (configurable rules), parse PT info blocks (◎ / 【】 / 『』 / 「键:」) into infoBlock and body paragraphs - mediainfo: MediaInfo and BDInfo (quick summary + full report) into a summary of container/duration/bitrate/size, video (HDR/DV), audio and subtitle tracks - file-tree: file list into a flattened directory tree with sizes and main/sample/subtitle/extra roles - formatDetailPage combines them; the server computes it when reading the cached detail page, so existing caches benefit without a refetch Contracts: SiteReleasePage gains infoBlock, paragraphs, media, fileTree. Web: drawer now reads top-down: meta row (promo/H&R, size, peers, upload time, IMDb/TMDB/豆瓣 tags) → 资料 → 简介 (collapsible) → 技术规格 (raw folded) → 截图 grid with keyboard lightbox → 文件 tree; inventory overlay moved into the fixed footer next to 下载. Fixtures: 6 desensitized samples from the local cache (site.detail and RSS descriptions) plus synthetic multi-track MediaInfo, BDInfo, bracket blocks and directory file lists. Co-Authored-By: Claude Opus 5.5 <[email protected]>
72 lines
2.1 KiB
TypeScript
72 lines
2.1 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/components/ui/sheet.tsx";
|
|
import { cn } from "~/lib/utils.ts";
|
|
|
|
/**
|
|
* 右侧详情抽屉(手机全宽)。内容区自带纵向滚动;footer 固定在底部放操作按钮。
|
|
* <DetailSheet open={!!id} onOpenChange={(o) => !o && setId(null)} title="详情" description="…">
|
|
* <KvList items={…} />
|
|
* </DetailSheet>
|
|
*/
|
|
export function DetailSheet({
|
|
open,
|
|
onOpenChange,
|
|
title,
|
|
description,
|
|
children,
|
|
footer,
|
|
width = "md",
|
|
className,
|
|
}: {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
title: ReactNode;
|
|
description?: ReactNode;
|
|
children: ReactNode;
|
|
footer?: ReactNode;
|
|
/** 桌面宽度:md ≈ 32rem,lg ≈ 44rem */
|
|
width?: "md" | "lg";
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
<SheetContent
|
|
side="right"
|
|
className={cn("w-full gap-0 p-0", width === "lg" ? "sm:max-w-2xl" : "sm:max-w-lg", className)}
|
|
>
|
|
<SheetHeader className="border-b border-[var(--shell-border)] px-5 py-4 pr-14">
|
|
<SheetTitle className="line-clamp-2 text-lead break-words">{title}</SheetTitle>
|
|
{description ? <SheetDescription className="break-words">{description}</SheetDescription> : null}
|
|
</SheetHeader>
|
|
<div className="min-h-0 flex-1 space-y-5 overflow-y-auto px-5 py-4">{children}</div>
|
|
{footer ? (
|
|
<div className="flex flex-wrap items-center justify-end gap-2 border-t border-[var(--shell-border)] px-5 py-3">
|
|
{footer}
|
|
</div>
|
|
) : null}
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
}
|
|
|
|
/** 抽屉/卡片内的小节:大写小标签 + 内容 */
|
|
export function DetailSection({
|
|
title,
|
|
children,
|
|
actions,
|
|
}: {
|
|
title: string;
|
|
children: ReactNode;
|
|
actions?: ReactNode;
|
|
}) {
|
|
return (
|
|
<section>
|
|
<div className="mb-2 flex items-center justify-between gap-2">
|
|
<h3 className="text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase">{title}</h3>
|
|
{actions}
|
|
</div>
|
|
{children}
|
|
</section>
|
|
);
|
|
}
|