@@ -0,0 +1,245 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { AdBannersSlot, AdIconsSlot, FriendLinksSlot, H5FriendLinksSlot } from "@/components/common/ads/slots";
|
||||
import { ChannelHeader, H5ChannelHeader } from "@/components/common/channel-header";
|
||||
import { DecryptedImage } from "@/components/common/decrypted-image";
|
||||
import { FilterChipLink, FilterOptionLink } from "@/components/common/filter-link";
|
||||
import { MEDIA_CARD_TITLE_PC } from "@/components/common/media-card-hover";
|
||||
import { Pagination } from "@/components/common/pagination";
|
||||
import { buildPageHrefMap } from "@/components/common/pagination-map";
|
||||
import { FloatingActions } from "@/components/home/floating-actions";
|
||||
import { topicCoverSrc } from "@/components/home/home-module-style";
|
||||
import { DownloadBanner } from "@/components/home/page-sections";
|
||||
import { SeoBreadcrumb } from "@/components/seo/seo-breadcrumb";
|
||||
import type { HomeTopicData, PlayletData, VideoInfo } from "@/lib/api/types";
|
||||
import { IMG_PLACEHOLDER } from "@/lib/image/decrypt";
|
||||
|
||||
import {
|
||||
isTopicVideo,
|
||||
topicTotalPages,
|
||||
unwrapTopicListItem,
|
||||
type TopicPageData,
|
||||
} from "./data";
|
||||
import { topicDetailHref } from "./href";
|
||||
import { topicListHref, topicSortOptions } from "./route";
|
||||
import { TopicPlayletCard, TopicVideoCard } from "./topic-cards";
|
||||
|
||||
const layouts = {
|
||||
playlet: {
|
||||
eyebrow: "短剧专题",
|
||||
gridClassName: "grid grid-cols-[repeat(6,266px)] justify-between gap-y-6",
|
||||
h5GridClassName: "grid grid-cols-2 gap-x-[7px] gap-y-3",
|
||||
morePcClassName: "grid grid-cols-2 gap-5 lg:grid-cols-4",
|
||||
},
|
||||
video: {
|
||||
eyebrow: "视频专题",
|
||||
gridClassName: "grid grid-cols-[repeat(4,400px)] justify-between gap-y-6",
|
||||
h5GridClassName: "grid grid-cols-2 gap-x-[7px] gap-y-3",
|
||||
morePcClassName: "grid grid-cols-2 gap-5 lg:grid-cols-3",
|
||||
},
|
||||
} as const;
|
||||
|
||||
function MoreTopicGrid({
|
||||
topics,
|
||||
dataType,
|
||||
compact,
|
||||
gridClassName,
|
||||
}: {
|
||||
topics: HomeTopicData[];
|
||||
dataType: 1 | 2;
|
||||
compact?: boolean;
|
||||
gridClassName: string;
|
||||
}) {
|
||||
if (!topics.length) return null;
|
||||
return (
|
||||
<section className="flex flex-col gap-3 lg:gap-5">
|
||||
<h2 className={compact ? "text-base font-medium text-white" : "text-xl font-medium text-white"}>更多专题</h2>
|
||||
<div className={compact ? "grid grid-cols-2 gap-[7px]" : gridClassName}>
|
||||
{topics.map((topic) => (
|
||||
<Link
|
||||
key={topic.id}
|
||||
href={topicDetailHref(topic, dataType)}
|
||||
className={`relative overflow-hidden rounded-lg bg-[#110F1F] ${
|
||||
compact ? (dataType === 2 ? "aspect-[7/4]" : "aspect-[16/10]") : "h-[231px]"
|
||||
}`}
|
||||
>
|
||||
<DecryptedImage
|
||||
src={topicCoverSrc(topic)}
|
||||
fallback={IMG_PLACEHOLDER}
|
||||
alt={topic.name || ""}
|
||||
className="absolute inset-0 h-full w-full object-cover object-center"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_38.85%_58.45%_at_66.18%_60.1%,rgba(255,255,255,0)_0%,rgba(0,0,0,0.55)_100%)]" />
|
||||
<div className={compact ? "absolute bottom-3 left-3 right-3" : "absolute bottom-6 left-8 right-8"}>
|
||||
<h3
|
||||
className={
|
||||
compact
|
||||
? "truncate text-base font-medium text-[#F2F2FA]"
|
||||
: `text-[#F2F2FA] ${MEDIA_CARD_TITLE_PC}`
|
||||
}
|
||||
>
|
||||
{topic.name}
|
||||
</h3>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function TopicList({
|
||||
data,
|
||||
compact,
|
||||
}: {
|
||||
data: TopicPageData;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const dataType: 1 | 2 = isTopicVideo(data.dataType) ? 2 : 1;
|
||||
const items = (data.list ?? [])
|
||||
.map((item) => unwrapTopicListItem(item, dataType))
|
||||
.filter((item): item is PlayletData | VideoInfo => Boolean(item?.id));
|
||||
|
||||
if (!items.length) {
|
||||
return <p className={compact ? "py-16 text-center text-sm leading-5 text-white/45" : "py-20 text-center text-sm leading-5 text-white/45"}>暂无专题内容</p>;
|
||||
}
|
||||
|
||||
const layout = dataType === 2 ? layouts.video : layouts.playlet;
|
||||
|
||||
return (
|
||||
<div className={compact ? layout.h5GridClassName : layout.gridClassName}>
|
||||
{items.map((item) =>
|
||||
dataType === 2 ? (
|
||||
<div key={item.id} className="min-w-0 h-full overflow-hidden">
|
||||
<TopicVideoCard item={item as VideoInfo} compact={compact} />
|
||||
</div>
|
||||
) : (
|
||||
<div key={item.id} className="min-w-0 h-full overflow-hidden">
|
||||
<TopicPlayletCard item={item as PlayletData} compact={compact} />
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TopicSortBar({
|
||||
pathKey,
|
||||
dataType,
|
||||
sortType,
|
||||
compact,
|
||||
}: {
|
||||
pathKey: string;
|
||||
dataType: 1 | 2;
|
||||
sortType: TopicPageData["sortType"];
|
||||
compact?: boolean;
|
||||
}) {
|
||||
if (compact) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 overflow-x-auto">
|
||||
<span className="shrink-0 whitespace-nowrap text-xs leading-[18px] text-[#948F9E]">排序:</span>
|
||||
<div className="flex shrink-0 items-center gap-x-3.5">
|
||||
{topicSortOptions.map((item) => (
|
||||
<FilterOptionLink
|
||||
key={item.sortType}
|
||||
label={item.label}
|
||||
href={topicListHref(pathKey, { dataType, sortType: item.sortType, page: 1 })}
|
||||
active={sortType === item.sortType}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-nowrap items-center gap-3">
|
||||
<span className="shrink-0 whitespace-nowrap text-xs leading-[18px] text-white">排序:</span>
|
||||
{topicSortOptions.map((item) => (
|
||||
<FilterChipLink
|
||||
key={item.sortType}
|
||||
label={item.label}
|
||||
href={topicListHref(pathKey, { dataType, sortType: item.sortType, page: 1 })}
|
||||
active={sortType === item.sortType}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TopicPage({ data }: { data: TopicPageData }) {
|
||||
const isVideo = isTopicVideo(data.dataType);
|
||||
const dataType: 1 | 2 = isVideo ? 2 : 1;
|
||||
const layout = isVideo ? layouts.video : layouts.playlet;
|
||||
const name = (data.name || "").trim() || "专题";
|
||||
const desc = (data.desc || "").trim();
|
||||
// 与标题相同或为空时不展示副标题,避免「科幻 / 科幻」重复
|
||||
const subtitle = desc && desc !== name ? desc : "";
|
||||
const moreTopics = data.moreTopics ?? [];
|
||||
const pageNum = data.pageNum || 1;
|
||||
const totalPages = topicTotalPages(data);
|
||||
const pathKey = (data.slug || "").trim() || (data.id != null ? String(data.id) : data.routeKey);
|
||||
const sortType = data.sortType === 2 || data.sortType === 3 ? data.sortType : 1;
|
||||
|
||||
const hrefMap =
|
||||
totalPages > 1
|
||||
? buildPageHrefMap(totalPages, (page) => topicListHref(pathKey, { dataType, page, sortType }))
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SeoBreadcrumb items={[{ name: "首页", href: "/" }, { name: "专题", href: "/topic" }, { name }]} />
|
||||
<main
|
||||
data-component="topic-page"
|
||||
data-datatype={dataType}
|
||||
className="flex w-full min-w-0 flex-col gap-[18px] overflow-x-clip px-4 py-3 lg:hidden"
|
||||
>
|
||||
<H5ChannelHeader eyebrow={layout.eyebrow} title={name} subtitle={subtitle} />
|
||||
<AdIconsSlot variant="h5" />
|
||||
|
||||
<div className="flex scroll-mt-16 flex-col gap-[9px] rounded-2xl border border-[#292138] bg-[#110D19] px-[19px] py-4">
|
||||
<TopicSortBar pathKey={pathKey} dataType={dataType} sortType={sortType} compact />
|
||||
</div>
|
||||
<TopicList data={data} compact />
|
||||
|
||||
{hrefMap ? <Pagination page={pageNum} total={totalPages} hrefMap={hrefMap} className="[&_form]:hidden" /> : null}
|
||||
|
||||
<MoreTopicGrid
|
||||
topics={moreTopics}
|
||||
dataType={dataType}
|
||||
compact
|
||||
gridClassName={layout.morePcClassName}
|
||||
/>
|
||||
|
||||
<AdBannersSlot variant="h5" />
|
||||
<H5FriendLinksSlot />
|
||||
</main>
|
||||
|
||||
<main data-component="topic-page" data-datatype={dataType} className="hidden lg:block">
|
||||
<div className="design-frame flex flex-col gap-6 py-[43px]">
|
||||
<ChannelHeader eyebrow={layout.eyebrow} title={name} subtitle={subtitle} />
|
||||
<AdIconsSlot variant="pc" />
|
||||
|
||||
<div className="flex scroll-mt-24 flex-col gap-4 rounded-[17px] border border-[#34304B] bg-[#151326] px-5 py-4">
|
||||
<TopicSortBar pathKey={pathKey} dataType={dataType} sortType={sortType} />
|
||||
</div>
|
||||
<TopicList data={data} />
|
||||
</div>
|
||||
{/* 分页放在 design-frame 外,避免 Chrome CSS zoom 热区错位导致点击无响应 */}
|
||||
{hrefMap ? (
|
||||
<div className="flex justify-center px-6 py-6">
|
||||
<Pagination page={pageNum} total={totalPages} hrefMap={hrefMap} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="design-frame flex flex-col gap-6 pb-[43px]">
|
||||
<MoreTopicGrid topics={moreTopics} dataType={dataType} gridClassName={layout.morePcClassName} />
|
||||
|
||||
<AdBannersSlot variant="pc" />
|
||||
<DownloadBanner />
|
||||
<FriendLinksSlot />
|
||||
</div>
|
||||
</main>
|
||||
<FloatingActions />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user