@@ -0,0 +1,396 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user