397 lines
16 KiB
TypeScript
397 lines
16 KiB
TypeScript
import Link from "next/link";
|
||
|
||
import {
|
||
CardTags,
|
||
cardTagCanLink,
|
||
pickPlayletCardTags,
|
||
pickVideoCardTags,
|
||
} from "@/components/common/card-tags";
|
||
import { DecryptedImage } from "@/components/common/decrypted-image";
|
||
import {
|
||
MEDIA_CARD_DESC_MD,
|
||
MEDIA_CARD_EPISODES,
|
||
MEDIA_CARD_META_MD,
|
||
MEDIA_CARD_TITLE,
|
||
MEDIA_CARD_TITLE_PC,
|
||
} from "@/components/common/media-card-hover";
|
||
import { formatHeat } from "@/components/ranking/data";
|
||
import { morePageHref } from "@/components/more/href";
|
||
import { dramaHref } from "@/components/short-drama/data";
|
||
import { tagPageHref } from "@/components/tag/href";
|
||
import { videoHref } from "@/components/video/video-card";
|
||
import type { HomeModuleData, PlayletData, VideoInfo } from "@/lib/api/types";
|
||
import { IMG_PLACEHOLDER } from "@/lib/image/decrypt";
|
||
|
||
import { H5SectionHeading } from "./h5-cards";
|
||
import { isVideoDataType, unwrapHomeModuleList, videoCoverSrc } from "./home-module-style";
|
||
|
||
function heatLabel(count?: number | null) {
|
||
// formatHeat → "95.6万 热度";样式 E 文案为「热度 95.6万」
|
||
return formatHeat(count).replace(/^(.+)\s热度$/, "热度 $1");
|
||
}
|
||
|
||
/** H5 样式 E:热度只显示数值(如 95.6万) */
|
||
function heatValueLabel(count?: number | null) {
|
||
return formatHeat(count).replace(/\s*热度$/, "");
|
||
}
|
||
|
||
const H5_E_RANK_BADGES = [
|
||
"/images/rank_num1.png",
|
||
"/images/rank_num2.png",
|
||
"/images/rank_num3.png",
|
||
] as const;
|
||
const H5_E_RANK_BADGE_FALLBACK = "/images/rank_num4.png";
|
||
|
||
function H5StyleERankBadge({
|
||
rank,
|
||
className = "absolute top-0 left-0 z-[1] h-5 w-[22px]",
|
||
}: {
|
||
rank: number;
|
||
className?: string;
|
||
}) {
|
||
const bg = H5_E_RANK_BADGES[rank - 1] ?? H5_E_RANK_BADGE_FALLBACK;
|
||
return (
|
||
<span
|
||
className={`flex items-center justify-center bg-left-top bg-no-repeat text-sm leading-none font-semibold text-white tabular-nums ${className}`}
|
||
style={{ backgroundImage: `url(${bg})`, backgroundSize: "100% 100%" }}
|
||
>
|
||
{rank}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function WatchButton({ href }: { href: string }) {
|
||
return (
|
||
<Link
|
||
href={href}
|
||
className="inline-flex h-10 w-[120px] items-center justify-center gap-1 rounded-lg border border-[rgba(241,43,158,0.1)] bg-[rgba(241,43,158,0.1)] text-xs leading-[18px] text-white transition-opacity hover:opacity-90"
|
||
>
|
||
立即观看
|
||
<svg viewBox="0 0 16 16" className="size-4" aria-hidden="true">
|
||
<circle cx="8" cy="8" r="6.15" fill="none" stroke="currentColor" strokeWidth="1" />
|
||
<path d="M7 5.5L10.5 8 7 10.5V5.5Z" fill="currentColor" />
|
||
</svg>
|
||
</Link>
|
||
);
|
||
}
|
||
|
||
/** 样式 E · 视频:竖排大卡(400 × 499,封面横版在上) */
|
||
function StyleEVideoCard({ item }: { item: VideoInfo }) {
|
||
const href = videoHref(String(item.id), item.slug);
|
||
const tags = pickVideoCardTags(item);
|
||
const heat = heatLabel(item.watchNum);
|
||
|
||
return (
|
||
<article className="group relative h-[499px] w-[400px] shrink-0 rounded-2xl bg-[#121729] transition-shadow duration-300 hover:shadow-[0_0_20px_#FF76C6]">
|
||
<Link href={href} className="absolute top-[19px] left-[18px] block h-[230px] w-[360px] overflow-hidden rounded-t-2xl">
|
||
<DecryptedImage
|
||
src={videoCoverSrc(item)}
|
||
fallback={IMG_PLACEHOLDER}
|
||
alt={item.title || ""}
|
||
className="absolute inset-0 h-full w-full object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-105"
|
||
/>
|
||
</Link>
|
||
<div className="absolute top-[267px] left-5 flex w-[358px] flex-col gap-3">
|
||
<Link href={href} className={`${MEDIA_CARD_TITLE_PC} font-semibold`}>
|
||
{item.title || "未命名"}
|
||
</Link>
|
||
<p className={`line-clamp-3 h-[53px] text-[#A3A3B8] ${MEDIA_CARD_DESC_MD}`}> </p>
|
||
<div className="flex flex-col gap-6">
|
||
<div className="flex items-center justify-between gap-4">
|
||
<CardTags tags={tags} kind="video" className={`min-w-0 text-[#F68804] ${MEDIA_CARD_META_MD}`} />
|
||
<span className={`shrink-0 text-[#EF2385] ${MEDIA_CARD_META_MD}`}>{heat}</span>
|
||
</div>
|
||
<WatchButton href={href} />
|
||
</div>
|
||
</div>
|
||
</article>
|
||
);
|
||
}
|
||
|
||
/** 样式 E · 短剧:横排大卡(553 × 401,竖封面在左) */
|
||
function StyleEPlayletCard({ item }: { item: PlayletData }) {
|
||
const href = dramaHref(item.id, item.slug);
|
||
const tags = pickPlayletCardTags(item);
|
||
const heat = heatLabel(item.heatValue ?? item.countBrowse);
|
||
const episodes = item.totalEpisode ? `${item.totalEpisode}集` : "";
|
||
|
||
return (
|
||
<article className="group relative h-[401px] w-[553px] shrink-0 rounded-2xl bg-[#121729] transition-shadow duration-300 hover:shadow-[0_0_20px_#FF76C6]">
|
||
<Link href={href} className="absolute top-6 left-6 block h-[353px] w-[247px] overflow-hidden rounded-2xl">
|
||
<DecryptedImage
|
||
src={item.cover || null}
|
||
fallback={IMG_PLACEHOLDER}
|
||
alt={item.title || ""}
|
||
className="absolute inset-0 h-full w-full object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-105"
|
||
/>
|
||
{episodes ? (
|
||
<span className={`absolute right-2 bottom-2 rounded-lg bg-black/60 px-2 py-[5px] font-medium text-white ${MEDIA_CARD_EPISODES}`}>
|
||
{episodes}
|
||
</span>
|
||
) : null}
|
||
</Link>
|
||
<div className="absolute top-[46px] left-[294px] flex h-[320px] w-[228px] flex-col gap-4 overflow-hidden">
|
||
<Link
|
||
href={href}
|
||
className={`media-card-title shrink-0 line-clamp-2 font-semibold text-[#F2F2FA] ${MEDIA_CARD_TITLE}`}
|
||
>
|
||
{item.title || "未命名"}
|
||
</Link>
|
||
<p className={`line-clamp-3 text-[#A3A3B8] ${MEDIA_CARD_DESC_MD}`}>{item.desc || "\u00A0"}</p>
|
||
<div className="mt-auto flex flex-col gap-5">
|
||
<div className="flex min-w-0 items-center gap-x-4">
|
||
<CardTags tags={tags} kind="playlet" className={`min-w-0 text-[#F68804] ${MEDIA_CARD_META_MD}`} />
|
||
<span className={`shrink-0 text-[#EF2385] ${MEDIA_CARD_META_MD}`}>{heat}</span>
|
||
</div>
|
||
<WatchButton href={href} />
|
||
</div>
|
||
</div>
|
||
</article>
|
||
);
|
||
}
|
||
|
||
function chunkRows<T>(items: T[], size: number): T[][] {
|
||
const rows: T[][] = [];
|
||
for (let i = 0; i < items.length; i += size) {
|
||
rows.push(items.slice(i, i + size));
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
/** 样式 E:视频竖排 4×2 / 短剧横排 3×2 */
|
||
export function HomeModuleStyleE({ module }: { module: HomeModuleData }) {
|
||
const { videos, playlets } = unwrapHomeModuleList(module.list, module.dataType);
|
||
const isVideo = isVideoDataType(module.dataType) || (videos.length > 0 && playlets.length === 0);
|
||
|
||
if (isVideo) {
|
||
const rows = chunkRows(videos.slice(0, 8), 4);
|
||
return (
|
||
<div data-component="home-module-style-e-video" className="flex flex-col gap-8">
|
||
{rows.map((row, rowIndex) => (
|
||
<div key={rowIndex} className="flex items-start gap-8">
|
||
{row.map((item, i) => (
|
||
<StyleEVideoCard key={item.id} item={item} />
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const rows = chunkRows(playlets.slice(0, 6), 3);
|
||
return (
|
||
<div data-component="home-module-style-e-playlet" className="flex flex-col gap-5">
|
||
{rows.map((row, rowIndex) => (
|
||
<div key={rowIndex} className="flex items-start gap-5">
|
||
{row.map((item) => (
|
||
<StyleEPlayletCard key={item.id} item={item} />
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** H5 · 短剧样式 E 横卡(封面左 + 信息右,带排行) */
|
||
function H5StyleEPlayletCard({ item, rank }: { item: PlayletData; rank: number }) {
|
||
const href = dramaHref(item.id, item.slug);
|
||
// 封面旁极窄,只展示 1 个标签,避免「激…|测试…」
|
||
const tags = pickPlayletCardTags(item).slice(0, 1);
|
||
const themeLabel = (item.themeNames?.[0] || item.tagNames?.[0] || "").trim();
|
||
const heat = heatValueLabel(item.heatValue ?? item.countBrowse);
|
||
const episodes = item.totalEpisode ? `${item.totalEpisode}集` : "";
|
||
const showCoins = Number(item.payType) !== 0;
|
||
|
||
return (
|
||
<article
|
||
data-component="h5-home-module-style-e-playlet-card"
|
||
className="group flex h-[180px] min-w-0 flex-1 gap-2 rounded-lg bg-[#121729] px-2 py-4 transition-shadow duration-300 hover:shadow-[0_0_20px_#FF76C6]"
|
||
>
|
||
<div className="flex w-[54px] shrink-0 flex-col gap-[18px]">
|
||
<Link href={href} className="relative block h-[82px] w-[54px] overflow-hidden rounded">
|
||
<DecryptedImage
|
||
src={item.cover || null}
|
||
fallback={IMG_PLACEHOLDER}
|
||
alt={item.title || ""}
|
||
className="absolute inset-0 h-full w-full object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-105"
|
||
/>
|
||
<H5StyleERankBadge rank={rank} />
|
||
{showCoins ? (
|
||
<span className="absolute top-0.5 right-0 z-[1] flex h-3.5 w-4 items-center justify-center rounded bg-[linear-gradient(139deg,#FFC41C_0%,#FF7226_100%)]">
|
||
<span className="text-[8px] font-bold leading-none text-white">¥</span>
|
||
</span>
|
||
) : null}
|
||
{episodes ? (
|
||
<span className="absolute right-0 bottom-0 rounded-tl bg-black/60 px-1.5 py-0.5 text-[10px] leading-4 text-white">
|
||
{episodes}
|
||
</span>
|
||
) : null}
|
||
</Link>
|
||
<div className="flex w-full flex-col gap-0.5 overflow-hidden">
|
||
{tags.length ? (
|
||
<p className="truncate text-[10px] leading-4 text-[#F68804]">
|
||
{tags.map((tag, i) => (
|
||
<span key={tag.slug ?? tag.id ?? tag.name}>
|
||
{i > 0 ? "|" : null}
|
||
{cardTagCanLink(tag) ? (
|
||
<Link href={tagPageHref(tag, "playlet")} className="hover:opacity-80">
|
||
{tag.name}
|
||
</Link>
|
||
) : (
|
||
tag.name
|
||
)}
|
||
</span>
|
||
))}
|
||
</p>
|
||
) : (
|
||
<p className="text-[10px] leading-4 text-[#F68804]"> </p>
|
||
)}
|
||
<p className="flex items-center gap-0.5 text-xs leading-[18px] text-[#EF2385]">
|
||
<img src="/images/hot.svg" alt="" className="h-3 w-[9px] shrink-0" />
|
||
<span className="truncate">{heat}</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex min-w-0 flex-1 flex-col justify-center gap-2 overflow-hidden">
|
||
{themeLabel ? (
|
||
<span className="inline-flex w-fit max-w-full truncate rounded bg-black/40 px-1 text-[10px] leading-[18px] text-[#F68804]">
|
||
{themeLabel}
|
||
</span>
|
||
) : null}
|
||
<Link href={href} className="line-clamp-2 text-sm leading-[22px] font-semibold text-[#F2F2FA]">
|
||
{item.title || "未命名"}
|
||
</Link>
|
||
<p className="line-clamp-2 text-[10px] leading-[18px] text-[#A3A3B8]">{item.desc || "\u00A0"}</p>
|
||
<Link
|
||
href={href}
|
||
className="inline-flex h-[30px] w-[65px] items-center justify-center rounded-lg border border-[rgba(241,43,158,0.1)] bg-[rgba(241,43,158,0.1)] text-xs leading-[18px] text-white"
|
||
>
|
||
立即观看
|
||
</Link>
|
||
</div>
|
||
</article>
|
||
);
|
||
}
|
||
|
||
/** H5 · 短剧样式 E:标题 + 三行两卡热榜 */
|
||
export function HomeModuleStyleEH5Playlet({ module }: { module: HomeModuleData }) {
|
||
const moreHref = morePageHref(module);
|
||
const title = (module.title || module.label || "推荐").trim();
|
||
const { playlets } = unwrapHomeModuleList(module.list, module.dataType);
|
||
const rows = chunkRows(playlets.slice(0, 6), 2);
|
||
|
||
if (!rows.length) return null;
|
||
|
||
return (
|
||
<div data-component="home-module-style-e-h5-playlet" className="flex w-full flex-col items-start gap-2 overflow-hidden">
|
||
<H5SectionHeading title={title} moreHref={moreHref} />
|
||
{rows.map((row, rowIndex) => (
|
||
<div
|
||
key={rowIndex}
|
||
className="flex w-full items-stretch gap-1 overflow-hidden rounded-2xl bg-[#10101A]"
|
||
>
|
||
{row.map((item, i) => (
|
||
<H5StyleEPlayletCard key={item.id} item={item} rank={rowIndex * 2 + i + 1} />
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** H5 · 视频样式 E 竖卡(封面在上,信息在下,带排行) */
|
||
function H5StyleEVideoCard({ item, rank }: { item: VideoInfo; rank: number }) {
|
||
const href = videoHref(String(item.id), item.slug);
|
||
const tags = pickVideoCardTags(item).slice(0, 1);
|
||
const themeLabel = (item.tagsName?.[0] || "").trim();
|
||
const heat = heatValueLabel(item.watchNum);
|
||
|
||
return (
|
||
<article
|
||
data-component="h5-home-module-style-e-video-card"
|
||
className="group flex h-[264px] min-w-0 flex-1 flex-col gap-2 rounded-lg bg-[#121729] px-2 py-4 transition-shadow duration-300 hover:shadow-[0_0_20px_#FF76C6]"
|
||
>
|
||
<Link href={href} className="relative block aspect-[152/83] w-full overflow-hidden rounded-lg">
|
||
<DecryptedImage
|
||
src={videoCoverSrc(item)}
|
||
fallback={IMG_PLACEHOLDER}
|
||
alt={item.title || ""}
|
||
className="absolute inset-0 h-full w-full object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-105"
|
||
/>
|
||
<H5StyleERankBadge rank={rank} className="absolute top-0 left-0 z-[1] h-[26px] w-7" />
|
||
</Link>
|
||
|
||
<div className="flex min-h-0 flex-1 flex-col justify-center gap-2 overflow-hidden">
|
||
{themeLabel ? (
|
||
<span className="inline-flex w-fit max-w-full truncate rounded bg-black/40 px-1 text-[10px] leading-[18px] text-[#F68804]">
|
||
{themeLabel}
|
||
</span>
|
||
) : null}
|
||
<Link href={href} className="truncate text-base leading-[22px] font-semibold text-[#F2F2FA]">
|
||
{item.title || "未命名"}
|
||
</Link>
|
||
<p className="line-clamp-2 text-xs leading-[18px] text-[#A3A3B8]"> </p>
|
||
<div className="flex items-center gap-2">
|
||
<div className="flex min-w-0 flex-1 flex-col gap-0.5 overflow-hidden">
|
||
{tags.length ? (
|
||
<p className="truncate text-xs leading-[18px] text-[#F68804]">
|
||
{tags.map((tag, i) => (
|
||
<span key={tag.slug ?? tag.id ?? tag.name}>
|
||
{i > 0 ? "|" : null}
|
||
{cardTagCanLink(tag) ? (
|
||
<Link href={tagPageHref(tag, "video")} className="hover:opacity-80">
|
||
{tag.name}
|
||
</Link>
|
||
) : (
|
||
tag.name
|
||
)}
|
||
</span>
|
||
))}
|
||
</p>
|
||
) : (
|
||
<p className="text-xs leading-[18px] text-[#F68804]"> </p>
|
||
)}
|
||
<p className="flex items-center gap-0.5 text-xs leading-[18px] text-[#EF2385]">
|
||
<img src="/images/hot.svg" alt="" className="h-3 w-[9px] shrink-0" />
|
||
<span className="truncate">{heat}</span>
|
||
</p>
|
||
</div>
|
||
<Link
|
||
href={href}
|
||
className="inline-flex h-8 w-[65px] shrink-0 items-center justify-center rounded-lg border border-[rgba(241,43,158,0.1)] bg-[rgba(241,43,158,0.1)] text-xs leading-[18px] text-white"
|
||
>
|
||
立即观看
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
);
|
||
}
|
||
|
||
/** H5 · 视频样式 E:标题 + 三行两卡热榜竖版 */
|
||
export function HomeModuleStyleEH5Video({ module }: { module: HomeModuleData }) {
|
||
const moreHref = morePageHref(module);
|
||
const title = (module.title || module.label || "推荐").trim();
|
||
const { videos } = unwrapHomeModuleList(module.list, module.dataType);
|
||
const rows = chunkRows(videos.slice(0, 6), 2);
|
||
|
||
if (!rows.length) return null;
|
||
|
||
return (
|
||
<div data-component="home-module-style-e-h5-video" className="flex w-full flex-col items-start gap-2 overflow-hidden">
|
||
<H5SectionHeading title={title} moreHref={moreHref} />
|
||
{rows.map((row, rowIndex) => (
|
||
<div
|
||
key={rowIndex}
|
||
className="flex w-full items-stretch gap-1 overflow-hidden rounded-2xl bg-[#10101A]"
|
||
>
|
||
{row.map((item, i) => (
|
||
<H5StyleEVideoCard key={item.id} item={item} rank={rowIndex * 2 + i + 1} />
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|