216 lines
7.6 KiB
TypeScript
216 lines
7.6 KiB
TypeScript
|
|
import { AdBannersSlot, AdIconsSlot, FriendLinksSlot, H5FriendLinksSlot } from "@/components/common/ads/slots";
|
|
import { ChannelHeader, H5ChannelHeader } from "@/components/common/channel-header";
|
|
import { FilterChipLink, FilterOptionLink } from "@/components/common/filter-link";
|
|
import { Pagination } from "@/components/common/pagination";
|
|
import { buildPageHrefMap } from "@/components/common/pagination-map";
|
|
import { FloatingActions } from "@/components/home/floating-actions";
|
|
import { DownloadBanner } from "@/components/home/page-sections";
|
|
import { SeoBreadcrumb } from "@/components/seo/seo-breadcrumb";
|
|
import type { HomeTopicData, PlayletData, VideoInfo } from "@/lib/api/types";
|
|
|
|
import {
|
|
isTopicVideo,
|
|
topicTotalPages,
|
|
unwrapTopicListItem,
|
|
type TopicPageData,
|
|
} from "./data";
|
|
import { topicListHref, topicSortOptions } from "./route";
|
|
import { TopicPlayletCard, TopicVideoCard } from "./topic-cards";
|
|
import { TopicListCard } from "./topic-list-card";
|
|
|
|
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-x-2.5 gap-y-4" : gridClassName}>
|
|
{topics.map((topic) => (
|
|
<TopicListCard key={topic.id} topic={topic} dataType={dataType} compact={compact} />
|
|
))}
|
|
</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 />
|
|
</>
|
|
);
|
|
}
|