Files
web_hghl_h5/src/components/home/home-module-style-e.tsx
T
谢宇宁andClaude Opus 5.5 61d0a11dfa Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 17:51:31 +07:00

397 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}`}>&nbsp;</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]">&nbsp;</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]">&nbsp;</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]">&nbsp;</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>
);
}