@@ -0,0 +1,123 @@
|
||||
import { mapContentListItem } from "@/components/ranking/data";
|
||||
import type { ShortDramaItem } from "@/components/short-drama/data";
|
||||
import { getPlayletHomeConfigListCached } from "@/lib/api/playlet";
|
||||
import type {
|
||||
HomeModuleData,
|
||||
HomeTopicData,
|
||||
PlayletContentListItem,
|
||||
PlayletData,
|
||||
PlayletHomeConfigListResp,
|
||||
VideoInfo,
|
||||
} from "@/lib/api/types";
|
||||
|
||||
export const MORE_PAGE_SIZE = 24;
|
||||
|
||||
export type MoreTab = "短剧" | "视频";
|
||||
|
||||
export type MorePageData = {
|
||||
configId: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
dataType: 1 | 2;
|
||||
tab: MoreTab;
|
||||
items: ShortDramaItem[];
|
||||
topics: HomeTopicData[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalCount: number;
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
export function parseMoreTab(value: string | null | undefined): MoreTab {
|
||||
return value === "视频" ? "视频" : "短剧";
|
||||
}
|
||||
|
||||
function resolveDataType(config?: HomeModuleData | null, fallback: 1 | 2 = 1): 1 | 2 {
|
||||
return config?.dataType === 2 ? 2 : fallback;
|
||||
}
|
||||
|
||||
function asContentItem(
|
||||
item: PlayletContentListItem | PlayletData | VideoInfo,
|
||||
dataType: 1 | 2,
|
||||
): PlayletContentListItem {
|
||||
if (item && typeof item === "object" && ("playlet" in item || "video" in item || "advertise" in item || "type" in item)) {
|
||||
return item as PlayletContentListItem;
|
||||
}
|
||||
if (dataType === 2) return { type: 0, video: item as VideoInfo };
|
||||
return { type: 0, playlet: item as PlayletData };
|
||||
}
|
||||
|
||||
export function buildMoreList(
|
||||
data: PlayletHomeConfigListResp | null | undefined,
|
||||
configId: string,
|
||||
fallbackPage = 1,
|
||||
fallbackPageSize = MORE_PAGE_SIZE,
|
||||
): MorePageData {
|
||||
const config = data?.config;
|
||||
const dataType = resolveDataType(config);
|
||||
const tab: MoreTab = dataType === 2 ? "视频" : "短剧";
|
||||
const page = data?.pageNum || fallbackPage;
|
||||
const pageSize = data?.pageSize || fallbackPageSize;
|
||||
const totalCount = data?.totalCount || 0;
|
||||
const items = (data?.list ?? [])
|
||||
.map((item) => mapContentListItem(asContentItem(item, dataType), dataType))
|
||||
.filter((item): item is ShortDramaItem => Boolean(item));
|
||||
const topics = data?.topicList ?? config?.topicList ?? [];
|
||||
|
||||
return {
|
||||
configId,
|
||||
title: (config?.title || config?.label || "").trim() || "更多内容",
|
||||
subtitle: (config?.desc || "").trim() || "按频道浏览更多内容,快速发现下一部想看的作品",
|
||||
dataType,
|
||||
tab,
|
||||
items,
|
||||
topics,
|
||||
page,
|
||||
pageSize,
|
||||
totalCount,
|
||||
totalPages: Math.max(1, Math.ceil(totalCount / pageSize) || 1),
|
||||
};
|
||||
}
|
||||
|
||||
function homeConfigLookup(idOrSlug: string): { id?: string; slug?: string } {
|
||||
if (/^\d+$/.test(idOrSlug)) return { id: idOrSlug };
|
||||
return { slug: idOrSlug };
|
||||
}
|
||||
|
||||
/** 规范路径段:优先 config.slug,回退 id */
|
||||
export function moreConfigPathKey(config?: { id?: number | string | null; slug?: string | null } | null, fallback = "") {
|
||||
return (config?.slug?.trim() || String(config?.id ?? "").trim() || fallback).trim();
|
||||
}
|
||||
|
||||
export async function loadMorePage(configId: string, page = 1): Promise<MorePageData> {
|
||||
const safePage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
|
||||
try {
|
||||
const data = await getPlayletHomeConfigListCached({
|
||||
...homeConfigLookup(configId),
|
||||
pageNum: safePage,
|
||||
pageSize: MORE_PAGE_SIZE,
|
||||
});
|
||||
const pathKey = moreConfigPathKey(data.config, configId);
|
||||
return buildMoreList(data, pathKey || configId, safePage, MORE_PAGE_SIZE);
|
||||
} catch (error) {
|
||||
console.error("[more/home-config]", error);
|
||||
return buildMoreList(null, configId, safePage, MORE_PAGE_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated 无 configId 时的空态 */
|
||||
export function emptyMorePage(tab: MoreTab = "短剧"): MorePageData {
|
||||
return {
|
||||
configId: "",
|
||||
title: "更多内容",
|
||||
subtitle: "按频道浏览更多短剧与视频,快速发现下一部想看的作品",
|
||||
dataType: tab === "视频" ? 2 : 1,
|
||||
tab,
|
||||
items: [],
|
||||
topics: [],
|
||||
page: 1,
|
||||
pageSize: MORE_PAGE_SIZE,
|
||||
totalCount: 0,
|
||||
totalPages: 1,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { moreListHref } from "./route";
|
||||
|
||||
type MoreConfigKey = {
|
||||
id?: number | string | null;
|
||||
slug?: string | null;
|
||||
};
|
||||
|
||||
/** 首页模块「查看更多」:优先 slug,无 slug 回退 id → /more/{slug|id} */
|
||||
export function morePageHref(config: MoreConfigKey | number | string, page = 1) {
|
||||
if (typeof config === "number" || typeof config === "string") {
|
||||
return moreListHref(config, page);
|
||||
}
|
||||
const key = (config.slug?.trim() || String(config.id ?? "").trim()).trim();
|
||||
if (!key) return "/more";
|
||||
return moreListHref(key, page);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { AdBannersSlot, AdIconsSlot, FriendLinksSlot, H5FriendLinksSlot } from "@/components/common/ads/slots";
|
||||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { ChannelHeader, H5ChannelHeader } from "@/components/common/channel-header";
|
||||
import { Pagination } from "@/components/common/pagination";
|
||||
import { buildPageHrefMap } from "@/components/common/pagination-map";
|
||||
import { DecryptedImage } from "@/components/common/decrypted-image";
|
||||
import { FloatingActions } from "@/components/home/floating-actions";
|
||||
import { topicCountLabel, topicCoverSrc } from "@/components/home/home-module-style";
|
||||
import { DownloadBanner } from "@/components/home/page-sections";
|
||||
import { SeoBreadcrumb } from "@/components/seo/seo-breadcrumb";
|
||||
import type { ShortDramaItem } from "@/components/short-drama/data";
|
||||
import { H5ShortDramaCard, ShortDramaCard } from "@/components/short-drama/short-drama-card";
|
||||
import { topicDetailHref } from "@/components/topic/href";
|
||||
import { H5VideoCard, VideoCard } from "@/components/video/video-card";
|
||||
import { IMG_PLACEHOLDER } from "@/lib/image/decrypt";
|
||||
|
||||
import type { MorePageData } from "./data";
|
||||
import { moreListHref } from "./route";
|
||||
|
||||
type CardRenderer = (item: ShortDramaItem) => ReactNode;
|
||||
|
||||
const layouts = {
|
||||
短剧: {
|
||||
gridClassName: "grid-cols-[repeat(auto-fill,266px)] gap-5",
|
||||
h5GridClassName: "grid-cols-2 gap-x-[7px] gap-y-3",
|
||||
renderCard: (item: ShortDramaItem) => <ShortDramaCard key={item.id} item={item} />,
|
||||
renderH5Card: (item: ShortDramaItem) => <H5ShortDramaCard key={item.id} item={item} />,
|
||||
},
|
||||
视频: {
|
||||
gridClassName: "grid-cols-[repeat(auto-fill,324px)] gap-5",
|
||||
h5GridClassName: "grid-cols-2 gap-x-[7px] gap-y-3",
|
||||
renderCard: (item: ShortDramaItem) => <VideoCard key={item.id} item={item} />,
|
||||
renderH5Card: (item: ShortDramaItem) => <H5VideoCard key={item.id} item={item} />,
|
||||
},
|
||||
} satisfies Record<MorePageData["tab"], { gridClassName: string; h5GridClassName: string; renderCard: CardRenderer; renderH5Card: CardRenderer }>;
|
||||
|
||||
function TopicGrid({
|
||||
topics,
|
||||
dataType,
|
||||
compact,
|
||||
}: {
|
||||
topics: MorePageData["topics"];
|
||||
dataType: MorePageData["dataType"];
|
||||
compact?: boolean;
|
||||
}) {
|
||||
if (!topics.length) return null;
|
||||
return (
|
||||
<div className={compact ? "grid grid-cols-2 gap-[7px]" : "grid grid-cols-2 gap-5 lg:grid-cols-4"}>
|
||||
{topics.map((topic) => {
|
||||
const cover = topicCoverSrc(topic);
|
||||
const countText = topicCountLabel(topic, dataType, "部");
|
||||
return (
|
||||
<Link
|
||||
key={topic.id}
|
||||
href={topicDetailHref(topic, dataType)}
|
||||
className={`relative overflow-hidden rounded-lg bg-[#110F1F] ${compact ? "aspect-[16/10]" : "h-[231px]"}`}
|
||||
>
|
||||
<DecryptedImage
|
||||
src={cover}
|
||||
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]"
|
||||
: "truncate text-2xl leading-8 font-medium text-[#F2F2FA]"
|
||||
}
|
||||
>
|
||||
{topic.name}
|
||||
</h3>
|
||||
{countText ? (
|
||||
<span
|
||||
className={
|
||||
compact
|
||||
? "mt-1 inline-flex rounded bg-black/50 px-1.5 py-0.5 text-[10px] text-[#F68804]"
|
||||
: "mt-3 inline-flex rounded bg-black/50 px-2.5 py-1 text-xs text-[#F68804]"
|
||||
}
|
||||
>
|
||||
{countText}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MorePage({ data }: { data: MorePageData }) {
|
||||
const layout = layouts[data.tab];
|
||||
const header = {
|
||||
eyebrow: "MORE",
|
||||
title: data.title,
|
||||
subtitle: data.subtitle,
|
||||
};
|
||||
const hrefMap =
|
||||
data.configId && data.totalPages > 1
|
||||
? buildPageHrefMap(data.totalPages, (page) => moreListHref(data.configId, page))
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SeoBreadcrumb items={[{ name: "首页", href: "/" }, { name: data.title || "更多内容" }]} />
|
||||
<main data-component="more-page" className="flex w-full min-w-0 flex-col gap-[18px] overflow-x-clip px-4 py-3 lg:hidden">
|
||||
<H5ChannelHeader {...header} />
|
||||
<AdIconsSlot variant="h5" />
|
||||
|
||||
<TopicGrid topics={data.topics} dataType={data.dataType} compact />
|
||||
|
||||
{data.items.length ? (
|
||||
<div className={`grid ${layout.h5GridClassName}`}>{data.items.map((item) => layout.renderH5Card(item))}</div>
|
||||
) : !data.topics.length ? (
|
||||
<p className="py-16 text-center text-sm leading-5 text-white/45">暂无更多内容</p>
|
||||
) : null}
|
||||
|
||||
{hrefMap ? <Pagination page={data.page} total={data.totalPages} hrefMap={hrefMap} className="[&_form]:hidden" /> : null}
|
||||
|
||||
<AdBannersSlot variant="h5" />
|
||||
<H5FriendLinksSlot />
|
||||
</main>
|
||||
|
||||
<main className="hidden lg:block">
|
||||
<div className="design-frame flex flex-col gap-6 py-[43px]">
|
||||
<ChannelHeader {...header} />
|
||||
<AdIconsSlot variant="pc" />
|
||||
|
||||
<TopicGrid topics={data.topics} dataType={data.dataType} />
|
||||
|
||||
{data.items.length ? (
|
||||
<div className={`grid scroll-mt-24 justify-between ${layout.gridClassName}`}>
|
||||
{data.items.map((item) => layout.renderCard(item))}
|
||||
</div>
|
||||
) : !data.topics.length ? (
|
||||
<p className="py-20 text-center text-sm leading-5 text-white/45">暂无更多内容</p>
|
||||
) : null}
|
||||
</div>
|
||||
{/* 分页放在 design-frame 外,避免 Chrome CSS zoom 热区错位导致点击无响应 */}
|
||||
{hrefMap ? (
|
||||
<div className="flex justify-center px-6 py-6">
|
||||
<Pagination page={data.page} total={data.totalPages} hrefMap={hrefMap} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="design-frame flex flex-col gap-6 pb-[43px]">
|
||||
<AdBannersSlot variant="pc" />
|
||||
<DownloadBanner />
|
||||
<FriendLinksSlot />
|
||||
</div>
|
||||
</main>
|
||||
<FloatingActions />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/** 首页模块更多页:/more/{id} 、/more/{id}/{page} */
|
||||
export function moreListHref(configId: number | string, page = 1) {
|
||||
const id = String(configId).trim();
|
||||
if (!id) return "/more";
|
||||
const base = `/more/${encodeURIComponent(id)}`;
|
||||
return page > 1 ? `${base}/${page}` : base;
|
||||
}
|
||||
|
||||
export function parseMorePageSegment(segments: string[] | undefined): number | null {
|
||||
if (!segments?.length) return 1;
|
||||
if (segments.length !== 1) return null;
|
||||
const num = Number(segments[0]);
|
||||
if (!Number.isInteger(num) || num < 1) return null;
|
||||
return num;
|
||||
}
|
||||
Reference in New Issue
Block a user