广告修改
This commit is contained in:
@@ -48,7 +48,7 @@ import {
|
||||
import type { PlayletThemeItem } from "@/lib/api/types";
|
||||
import { buildServerForwardHeaders } from "@/lib/api/client-headers";
|
||||
import { primeRequestForwardHeaders } from "@/lib/api/request-context";
|
||||
import { AdBannersSlot, AdIconsSlot } from "@/components/common/ads/slots";
|
||||
import { AdBannersSlot, AdSquaresSlot } from "@/components/common/ads/slots";
|
||||
import {
|
||||
buildPlayletDetailJSONLD,
|
||||
buildPlayletPlayJSONLD,
|
||||
@@ -387,9 +387,9 @@ async function ShortDramaRouteContent({ params }: { params: Params }) {
|
||||
<DramaDetailPage
|
||||
detail={mapped.detail}
|
||||
related={mapped.related}
|
||||
adIcons={<AdIconsSlot variant="pc" />}
|
||||
adIcons={<AdSquaresSlot slot="AList" variant="pc" />}
|
||||
promo={<AdBannersSlot variant="pc" />}
|
||||
adGrid={<AdIconsSlot variant="h5" />}
|
||||
adGrid={<AdSquaresSlot slot="AList" variant="h5" />}
|
||||
bannerAds={<AdBannersSlot variant="h5" />}
|
||||
/>
|
||||
</>
|
||||
@@ -428,8 +428,10 @@ async function ShortDramaRouteContent({ params }: { params: Params }) {
|
||||
episode={play.episode}
|
||||
src={play.src}
|
||||
related={play.related}
|
||||
adIcons={<AdIconsSlot variant="pc" />}
|
||||
promo={<AdBannersSlot variant="pc" />}
|
||||
topAds={<AdSquaresSlot slot="AList" variant="pc" />}
|
||||
h5Ads={<AdSquaresSlot slot="AList" variant="h5" />}
|
||||
h5Banners={<AdBannersSlot variant="h5" />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
resolvePageMetadata,
|
||||
} from "@/lib/seo";
|
||||
import { videoHref } from "@/components/video/video-card";
|
||||
import { AdBannersSlot, AdIconsSlot } from "@/components/common/ads/slots";
|
||||
import { AdBannersSlot, AdSquaresSlot } from "@/components/common/ads/slots";
|
||||
|
||||
type Params = Promise<{ sort: string; page?: string[] }>;
|
||||
|
||||
@@ -307,9 +307,9 @@ async function VideoRouteContent({ params }: { params: Params }) {
|
||||
detail={mapped.detail}
|
||||
src={mapped.src}
|
||||
related={mapped.related}
|
||||
adIcons={<AdIconsSlot variant="pc" labels={false} />}
|
||||
promo={<AdBannersSlot variant="pc" />}
|
||||
adGrid={<AdIconsSlot variant="h5" />}
|
||||
topAds={<AdSquaresSlot slot="AList" variant="pc" />}
|
||||
h5Ads={<AdSquaresSlot slot="AList" variant="h5" />}
|
||||
bannerAds={<AdBannersSlot variant="h5" />}
|
||||
/>
|
||||
</>
|
||||
|
||||
+123
-18
@@ -5,14 +5,21 @@ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore
|
||||
import { getAgeGateOpen, subscribeAgeGate } from "@/components/age-gate/age-gate-dialog";
|
||||
import { DecryptedImage } from "@/components/common/decrypted-image";
|
||||
import { useChromeless } from "@/components/layout/route-visibility";
|
||||
import { lockBodyScroll } from "@/lib/browser/scroll-lock";
|
||||
import type { AdvertiseInfo, AdvitiseData } from "@/lib/api/types";
|
||||
|
||||
/** 对齐 jha-1043 dialogAd.js:签名队列 + session 进度 / 关闭态 */
|
||||
const QUEUE_VERSION = "2";
|
||||
const DISMISSED_KEY = "dialogAdDismissed";
|
||||
const PROGRESS_KEY = "dialogAdProgress";
|
||||
/** 九宫格弹窗关闭态(按广告内容签名,内容变化后重新弹出) */
|
||||
const GRID_DISMISSED_KEY = "gridAdDismissed";
|
||||
|
||||
let queueIdle = true;
|
||||
/**
|
||||
* 启动弹窗链是否已结束(弹窗广告 + 九宫格都处理完)。
|
||||
* 初始为未结束:等弹窗组件判断完再放行,避免最新地址弹窗抢先和九宫格同时弹出。
|
||||
*/
|
||||
let queueIdle = false;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function emit() {
|
||||
@@ -101,12 +108,27 @@ function rememberProgress(signature: string, index: number) {
|
||||
writeSession(PROGRESS_KEY, JSON.stringify({ signature, index }));
|
||||
}
|
||||
|
||||
function pickGridAds(advitise?: AdvitiseData | null) {
|
||||
return (advitise?.nineGridList ?? []).filter((item): item is AdvertiseInfo & { images: string } =>
|
||||
Boolean(item.images?.trim()),
|
||||
);
|
||||
}
|
||||
|
||||
function buildGridSignature(ads: AdvertiseInfo[]) {
|
||||
return ads.length ? JSON.stringify(ads.map((item) => [item.id, item.images, item.link, item.title])) : "";
|
||||
}
|
||||
|
||||
function pickDialogAds(items?: AdvertiseInfo[] | null, advitise?: AdvitiseData | null) {
|
||||
const source = items ?? advitise?.dialogList ?? [];
|
||||
return source.filter((item): item is AdvertiseInfo & { images: string } => Boolean(item.images?.trim()));
|
||||
}
|
||||
|
||||
/** 弹窗广告:使用 advitise.dialogList,全站队列展示(对齐 jha-1043) */
|
||||
/**
|
||||
* 启动弹窗链(对齐 huangguocg.com):年龄确认 → 弹窗广告 → 九宫格弹窗 → (首页)最新地址。
|
||||
* 弹窗广告:advitise.dialogList(后台位置 1「弹窗」,素材 600×800),依次展示;
|
||||
* 九宫格:advitise.nineGridList(后台位置 2「9宫格」,小方块),弹窗广告结束后弹出一次,
|
||||
* 点任一应用或关闭即不再弹(同一会话、内容不变时)。
|
||||
*/
|
||||
export function AdDialogHost({
|
||||
items,
|
||||
advitise,
|
||||
@@ -119,22 +141,41 @@ export function AdDialogHost({
|
||||
const ads = useMemo(() => pickDialogAds(items, advitise), [items, advitise]);
|
||||
const signature = useMemo(() => buildSignature(ads), [ads]);
|
||||
|
||||
const gridAds = useMemo(() => pickGridAds(advitise), [advitise]);
|
||||
const gridSignature = useMemo(() => buildGridSignature(gridAds), [gridAds]);
|
||||
|
||||
const [index, setIndex] = useState(-1);
|
||||
const [gridOpen, setGridOpen] = useState(false);
|
||||
const hasDisplayedRef = useRef(false);
|
||||
const enabled = !chromeless && !ageGateOpen && ads.length > 0;
|
||||
const ready = !chromeless && !ageGateOpen;
|
||||
const enabled = ready && ads.length > 0;
|
||||
|
||||
/** 弹窗广告结束(或没有)后:未关闭过九宫格则弹出,否则整条链结束 */
|
||||
const startGrid = useCallback(() => {
|
||||
if (gridAds.length && readSession(GRID_DISMISSED_KEY) !== gridSignature) {
|
||||
setGridOpen(true);
|
||||
setQueueIdle(false);
|
||||
return;
|
||||
}
|
||||
setGridOpen(false);
|
||||
setQueueIdle(true);
|
||||
}, [gridAds.length, gridSignature]);
|
||||
|
||||
const dismissGrid = useCallback(() => {
|
||||
writeSession(GRID_DISMISSED_KEY, gridSignature);
|
||||
setGridOpen(false);
|
||||
setQueueIdle(true);
|
||||
}, [gridSignature]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
await Promise.resolve();
|
||||
if (cancelled) return;
|
||||
if (!enabled) {
|
||||
if (!ads.length) setQueueIdle(true);
|
||||
return;
|
||||
}
|
||||
if (isAdQueueDone(signature)) {
|
||||
if (!ready) return;
|
||||
if (!enabled || isAdQueueDone(signature)) {
|
||||
setIndex(-1);
|
||||
setQueueIdle(true);
|
||||
startGrid();
|
||||
return;
|
||||
}
|
||||
const start = readProgress(signature);
|
||||
@@ -142,7 +183,7 @@ export function AdDialogHost({
|
||||
if (hasDisplayedRef.current) writeSession(DISMISSED_KEY, signature);
|
||||
removeSession(PROGRESS_KEY);
|
||||
setIndex(-1);
|
||||
setQueueIdle(true);
|
||||
startGrid();
|
||||
return;
|
||||
}
|
||||
setIndex(start);
|
||||
@@ -151,15 +192,15 @@ export function AdDialogHost({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [enabled, signature, ads.length]);
|
||||
}, [ready, enabled, signature, ads.length, startGrid]);
|
||||
|
||||
const finishQueue = useCallback(() => {
|
||||
if (hasDisplayedRef.current) writeSession(DISMISSED_KEY, signature);
|
||||
else removeSession(DISMISSED_KEY);
|
||||
removeSession(PROGRESS_KEY);
|
||||
setIndex(-1);
|
||||
setQueueIdle(true);
|
||||
}, [signature]);
|
||||
startGrid();
|
||||
}, [signature, startGrid]);
|
||||
|
||||
const showNext = useCallback(() => {
|
||||
setIndex((current) => {
|
||||
@@ -184,29 +225,43 @@ export function AdDialogHost({
|
||||
setQueueIdle(false);
|
||||
}, [index, ads.length, signature]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!gridOpen || index >= 0) return;
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") dismissGrid();
|
||||
};
|
||||
const unlockScroll = lockBodyScroll();
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
unlockScroll();
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [gridOpen, index, dismissGrid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (index < 0) return;
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") showNext();
|
||||
};
|
||||
const { overflow } = document.body.style;
|
||||
document.body.style.overflow = "hidden";
|
||||
const unlockScroll = lockBodyScroll();
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
document.body.style.overflow = overflow;
|
||||
unlockScroll();
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [index, showNext]);
|
||||
|
||||
const ad = index >= 0 && index < ads.length ? ads[index] : null;
|
||||
if (!ad?.images) return null;
|
||||
if (!ad?.images) {
|
||||
return gridOpen ? <NineGridDialog ads={gridAds} onClose={dismissGrid} /> : null;
|
||||
}
|
||||
|
||||
const picture = (
|
||||
<DecryptedImage
|
||||
src={ad.images}
|
||||
alt={ad.title || "广告"}
|
||||
priority
|
||||
className="h-[468px] w-[300px] rounded-3xl object-cover lg:h-[602px] lg:w-[400px] lg:rounded-[40px]"
|
||||
className="h-[400px] w-[300px] rounded-3xl object-cover lg:h-[560px] lg:w-[420px] lg:rounded-[32px]"
|
||||
onError={skipBroken}
|
||||
/>
|
||||
);
|
||||
@@ -251,3 +306,53 @@ export function AdDialogHost({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 九宫格弹窗:4 列应用图标宫格 + 底部关闭按钮;点任一应用或空白处即关闭 */
|
||||
function NineGridDialog({ ads, onClose }: { ads: Array<AdvertiseInfo & { images: string }>; onClose: () => void }) {
|
||||
return (
|
||||
<div
|
||||
data-component="nine-grid-dialog"
|
||||
className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-black/85 px-6 py-10 backdrop-blur-md"
|
||||
onClick={(event) => event.target === event.currentTarget && onClose()}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="推荐应用"
|
||||
className="flex w-full max-w-[360px] flex-col items-center gap-6 lg:max-w-[520px]"
|
||||
onClick={(event) => event.target === event.currentTarget && onClose()}
|
||||
>
|
||||
<ul className="grid max-h-[70vh] w-full grid-cols-4 gap-x-3 gap-y-5 overflow-y-auto overscroll-contain lg:gap-x-6 lg:gap-y-7">
|
||||
{ads.map((item, i) => (
|
||||
<li key={item.id || `${item.images}-${i}`} className="min-w-0">
|
||||
<a
|
||||
href={item.link || undefined}
|
||||
target="_blank"
|
||||
rel="sponsored nofollow noopener noreferrer"
|
||||
title={item.title || undefined}
|
||||
onClick={onClose}
|
||||
className="flex flex-col items-center gap-2"
|
||||
>
|
||||
<DecryptedImage
|
||||
src={item.images}
|
||||
alt={item.title || "推荐应用"}
|
||||
priority
|
||||
className="aspect-square w-full max-w-[70px] rounded-2xl object-cover lg:max-w-20"
|
||||
/>
|
||||
<span className="max-w-full truncate text-[13px] leading-[18px] text-white lg:text-[15px] lg:leading-[22px]">
|
||||
{item.title || "推荐应用"}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button type="button" aria-label="关闭" onClick={onClose} className="size-9 cursor-pointer">
|
||||
<svg viewBox="0 0 36 36" className="size-full" aria-hidden="true">
|
||||
<circle cx="18" cy="18" r="16" fill="none" stroke="white" strokeWidth="2" />
|
||||
<path d="M12.5 12.5l11 11M23.5 12.5l-11 11" stroke="white" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from
|
||||
import { getAdOpen, hasAdShown, subscribeAd } from "@/components/ad/ad-dialog";
|
||||
import { getAgeGateOpen, subscribeAgeGate } from "@/components/age-gate/age-gate-dialog";
|
||||
import { useChromeless } from "@/components/layout/route-visibility";
|
||||
import { lockBodyScroll } from "@/lib/browser/scroll-lock";
|
||||
import type { AnnouncementInfo, ConfigInfo } from "@/lib/api/types";
|
||||
import { OFFICIAL_TG_URL, SITE_DOMAIN } from "@/lib/seo/metadata";
|
||||
|
||||
@@ -95,10 +96,9 @@ export function AddressDialogHost({
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const { overflow } = document.body.style;
|
||||
document.body.style.overflow = "hidden";
|
||||
const unlockScroll = lockBodyScroll();
|
||||
return () => {
|
||||
document.body.style.overflow = overflow;
|
||||
unlockScroll();
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useSyncExternalStore } from "react";
|
||||
import { lockBodyScroll } from "@/lib/browser/scroll-lock";
|
||||
import { useChromeless } from "@/components/layout/route-visibility";
|
||||
|
||||
/** 对齐 jha-1043 eightPop.js:sessionStorage + 关闭/退出均离开站点 */
|
||||
@@ -65,10 +66,9 @@ export function AgeGateDialogHost() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const { overflow } = document.body.style;
|
||||
document.body.style.overflow = "hidden";
|
||||
const unlockScroll = lockBodyScroll();
|
||||
return () => {
|
||||
document.body.style.overflow = overflow;
|
||||
unlockScroll();
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useId, useState, useSyncExternalStore, type FormEvent, type ReactNode } from "react";
|
||||
|
||||
import { lockBodyScroll } from "@/lib/browser/scroll-lock";
|
||||
import { useMockUser } from "@/components/auth/use-mock-user";
|
||||
import {
|
||||
ACCOUNT_RULE_HINT,
|
||||
@@ -96,11 +97,10 @@ export function LoginDialogHost() {
|
||||
useEffect(() => {
|
||||
if (!mode) return;
|
||||
const onKeyDown = (event: KeyboardEvent) => event.key === "Escape" && close();
|
||||
const { overflow } = document.body.style;
|
||||
document.body.style.overflow = "hidden";
|
||||
const unlockScroll = lockBodyScroll();
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
document.body.style.overflow = overflow;
|
||||
unlockScroll();
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [mode, close]);
|
||||
|
||||
@@ -16,7 +16,10 @@ type AdIconMarqueeProps = {
|
||||
contentKey?: string;
|
||||
};
|
||||
|
||||
/** Icon 广告从右向左无缝自动滚动(内容复制两段,translateX -50%) */
|
||||
/**
|
||||
* Icon 广告从右向左无缝自动滚动(内容复制两段,translateX -50%)。
|
||||
* 一行放得下时不复制、不滚动,避免广告少时出现重复图标。
|
||||
*/
|
||||
export function AdIconMarquee({
|
||||
children,
|
||||
className = "",
|
||||
@@ -24,42 +27,50 @@ export function AdIconMarquee({
|
||||
spacerClassName = "w-3",
|
||||
contentKey = "",
|
||||
}: AdIconMarqueeProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const segmentRef = useRef<HTMLDivElement>(null);
|
||||
const [durationSec, setDurationSec] = useState(20);
|
||||
const [overflowing, setOverflowing] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const el = segmentRef.current;
|
||||
if (!el) return;
|
||||
if (!container || !el) return;
|
||||
|
||||
const update = () => {
|
||||
const width = el.getBoundingClientRect().width;
|
||||
if (width <= 0) return;
|
||||
setOverflowing(width > container.clientWidth + 1);
|
||||
setDurationSec(Math.max(width / SCROLL_PX_PER_SEC, 8));
|
||||
};
|
||||
|
||||
update();
|
||||
const observer = new ResizeObserver(update);
|
||||
observer.observe(container);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [contentKey]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
data-component="ad-icon-marquee"
|
||||
className={`overflow-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden ${className}`}
|
||||
>
|
||||
<div
|
||||
className="ad-icon-marquee-track flex w-max items-start"
|
||||
style={{ animationDuration: `${durationSec}s` }}
|
||||
className={`flex w-max items-start ${overflowing ? "ad-icon-marquee-track" : ""}`}
|
||||
style={overflowing ? { animationDuration: `${durationSec}s` } : undefined}
|
||||
>
|
||||
<div ref={segmentRef} className={`flex shrink-0 items-start ${gapClassName}`}>
|
||||
{children}
|
||||
<span className={`${spacerClassName} shrink-0`} aria-hidden />
|
||||
</div>
|
||||
<div className={`flex shrink-0 items-start ${gapClassName}`} aria-hidden>
|
||||
{children}
|
||||
<span className={`${spacerClassName} shrink-0`} aria-hidden />
|
||||
</div>
|
||||
{overflowing ? (
|
||||
<div className={`flex shrink-0 items-start ${gapClassName}`} aria-hidden>
|
||||
{children}
|
||||
<span className={`${spacerClassName} shrink-0`} aria-hidden />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { AdvertiseInfo, AdvitiseData } from "@/lib/api/types";
|
||||
|
||||
import { AdIcons } from "./ads/ad-icons";
|
||||
|
||||
/** @deprecated 使用 AdIcons;保留别名兼容旧引用 */
|
||||
export function AdIconRow({
|
||||
labels = true,
|
||||
items,
|
||||
advitise,
|
||||
className = "",
|
||||
}: {
|
||||
labels?: boolean;
|
||||
items?: AdvertiseInfo[] | null;
|
||||
advitise?: AdvitiseData | null;
|
||||
className?: string;
|
||||
}) {
|
||||
return <AdIcons variant="pc" labels={labels} items={items} advitise={advitise} className={className} />;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ type AdBannersProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/** 公共 Banner 广告:对接 advitise.detailbannerList;无数据不渲染 */
|
||||
/** 公共 Banner 广告:对接 advitise.detailbannerList(后台位置 3「详情横幅」,素材 720×150);无数据不渲染 */
|
||||
export function AdBanners({
|
||||
variant = "pc",
|
||||
items,
|
||||
@@ -38,7 +38,7 @@ export function AdBanners({
|
||||
href={href}
|
||||
target={external ? "_blank" : undefined}
|
||||
rel={external ? "sponsored nofollow noopener noreferrer" : undefined}
|
||||
className="block h-10 overflow-hidden rounded-xl"
|
||||
className="block aspect-[720/150] overflow-hidden rounded-xl"
|
||||
>
|
||||
<DecryptedImage
|
||||
src={ad.images || null}
|
||||
@@ -68,7 +68,7 @@ export function AdBanners({
|
||||
href={href}
|
||||
target={external ? "_blank" : undefined}
|
||||
rel={external ? "sponsored nofollow noopener noreferrer" : undefined}
|
||||
className="block h-[97px] overflow-hidden rounded-xl"
|
||||
className="block aspect-[720/150] overflow-hidden rounded-xl"
|
||||
>
|
||||
<DecryptedImage
|
||||
src={ad.images || null}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { DecryptedImage } from "@/components/common/decrypted-image";
|
||||
import { AdIconMarquee } from "@/components/common/ad-icon-marquee";
|
||||
import type { AdvertiseInfo, AdvitiseData } from "@/lib/api/types";
|
||||
import { IMG_PLACEHOLDER } from "@/lib/image/decrypt";
|
||||
|
||||
import { adContentKey, adHref, pickIconAds } from "./pick";
|
||||
|
||||
type AdIconsProps = {
|
||||
variant?: "pc" | "h5";
|
||||
items?: AdvertiseInfo[] | null;
|
||||
advitise?: AdvitiseData | null;
|
||||
/** PC 是否显示标题;H5 始终显示 */
|
||||
labels?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/** 公共 Icon 广告:对接 advitise.nineGridList,无数据不渲染;从右向左无缝滚动 */
|
||||
export function AdIcons({
|
||||
variant = "pc",
|
||||
items,
|
||||
advitise,
|
||||
labels = true,
|
||||
className = "",
|
||||
}: AdIconsProps) {
|
||||
const isH5 = variant === "h5";
|
||||
const apiList = pickIconAds(items, advitise, variant);
|
||||
if (!apiList.length) return null;
|
||||
|
||||
const showLabels = isH5 || labels;
|
||||
const iconClass = isH5
|
||||
? "flex w-12 shrink-0 flex-col items-center gap-1"
|
||||
: "flex w-20 shrink-0 flex-col items-center gap-1";
|
||||
const imgClass = isH5
|
||||
? "aspect-square size-12 rounded-xl border border-white/30 object-cover"
|
||||
: "size-20 rounded-xl border border-white/30 object-cover";
|
||||
const titleClass = isH5
|
||||
? "max-w-full truncate text-xs leading-[18px] text-white/90"
|
||||
: "text-center text-sm leading-5 text-white/90";
|
||||
|
||||
return (
|
||||
<AdIconMarquee
|
||||
className={className}
|
||||
gapClassName={isH5 ? "gap-2" : "gap-3"}
|
||||
spacerClassName={isH5 ? "w-2" : "w-3"}
|
||||
contentKey={adContentKey(apiList)}
|
||||
>
|
||||
{apiList.map((ad, index) => {
|
||||
const href = adHref(ad.link, isH5 ? "#h5-download" : "#download");
|
||||
const external = Boolean(ad.link?.trim());
|
||||
return (
|
||||
<a
|
||||
key={ad.id || `ad-${index}`}
|
||||
href={href}
|
||||
target={external ? "_blank" : undefined}
|
||||
rel={external ? "sponsored nofollow noopener noreferrer" : undefined}
|
||||
className={iconClass}
|
||||
>
|
||||
<DecryptedImage
|
||||
src={ad.images || null}
|
||||
fallback={IMG_PLACEHOLDER}
|
||||
alt={ad.title || ""}
|
||||
className={imgClass}
|
||||
/>
|
||||
{showLabels ? <span className={titleClass}>{ad.title || "广告"}</span> : null}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</AdIconMarquee>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { AdIconMarquee } from "@/components/common/ad-icon-marquee";
|
||||
import { DecryptedImage } from "@/components/common/decrypted-image";
|
||||
import type { AdvertiseInfo, AdvitiseData } from "@/lib/api/types";
|
||||
import { IMG_PLACEHOLDER } from "@/lib/image/decrypt";
|
||||
|
||||
import { adContentKey, adHref, pickSquareAds, type SquareAdSlot } from "./pick";
|
||||
|
||||
type AdSquaresProps = {
|
||||
/** BList:首页小图标;AList:播放页小图标 */
|
||||
slot: SquareAdSlot;
|
||||
variant?: "pc" | "h5";
|
||||
items?: AdvertiseInfo[] | null;
|
||||
advitise?: AdvitiseData | null;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 小方块广告(方形素材),无数据不渲染;一行放不下时自动滚动。
|
||||
* 首页用 BList(位置 13「首页小图标」),播放页用 AList(位置 12「播放页小图标」)。
|
||||
*/
|
||||
export function AdSquares({ slot, variant = "pc", items, advitise, className = "" }: AdSquaresProps) {
|
||||
const isH5 = variant === "h5";
|
||||
const list = pickSquareAds(slot, items, advitise);
|
||||
if (!list.length) return null;
|
||||
|
||||
return (
|
||||
<section
|
||||
data-component="ad-squares"
|
||||
aria-label="推广"
|
||||
className={`flex flex-col ${isH5 ? "gap-1.5" : "gap-2.5"} ${className}`}
|
||||
>
|
||||
<p className={`text-[#6F6C7B] ${isH5 ? "text-[11px] leading-4" : "text-xs leading-[18px]"}`}>推广</p>
|
||||
<AdIconMarquee
|
||||
gapClassName={isH5 ? "gap-2.5" : "gap-4"}
|
||||
spacerClassName={isH5 ? "w-2.5" : "w-4"}
|
||||
contentKey={adContentKey(list)}
|
||||
>
|
||||
{list.map((ad, index) => {
|
||||
const href = adHref(ad.link, isH5 ? "#h5-download" : "#download");
|
||||
const external = Boolean(ad.link?.trim());
|
||||
return (
|
||||
<a
|
||||
key={ad.id || `square-${index}`}
|
||||
href={href}
|
||||
target={external ? "_blank" : undefined}
|
||||
rel={external ? "sponsored nofollow noopener noreferrer" : undefined}
|
||||
title={ad.title || undefined}
|
||||
className={`group flex shrink-0 flex-col items-center gap-1.5 ${isH5 ? "w-[72px]" : "w-[120px]"}`}
|
||||
>
|
||||
<DecryptedImage
|
||||
src={ad.images || null}
|
||||
fallback={IMG_PLACEHOLDER}
|
||||
alt={ad.title || "推广"}
|
||||
className={`aspect-square w-full rounded-lg object-cover outline outline-1 -outline-offset-1 outline-white/10 transition-[outline-color] group-hover:outline-[#FFB020]/80 ${
|
||||
isH5 ? "" : "rounded-xl"
|
||||
}`}
|
||||
/>
|
||||
<span className={`max-w-full truncate text-[#D8D6DF] ${isH5 ? "text-[11px] leading-4" : "text-sm leading-5"}`}>
|
||||
{ad.title || "推广"}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</AdIconMarquee>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/** 仅导出可进 Client Component 的广告 UI;Slot 请从 `@/components/common/ads/slots` 引入 */
|
||||
export { AdIcons } from "./ad-icons";
|
||||
export { AdBanners } from "./ad-banners";
|
||||
export { pickIconAds, pickBannerAds, pickFriendAds } from "./pick";
|
||||
export { AdSquares } from "./ad-squares";
|
||||
export { pickBannerAds, pickSquareAds } from "./pick";
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { AdvertiseInfo, AdvitiseData } from "@/lib/api/types";
|
||||
|
||||
export function hasAdVisual(item: AdvertiseInfo) {
|
||||
return Boolean(item.images?.trim() || item.title?.trim());
|
||||
}
|
||||
/**
|
||||
* 小方块广告(后台「站点」广告位):
|
||||
* BList = 位置 13「首页小图标」,AList = 位置 12「播放页小图标」
|
||||
*/
|
||||
export type SquareAdSlot = "BList" | "AList";
|
||||
|
||||
/** Icon:统一对接 advitise.nineGridList(可显式传 items 覆盖) */
|
||||
export function pickIconAds(
|
||||
export function pickSquareAds(
|
||||
slot: SquareAdSlot,
|
||||
items?: AdvertiseInfo[] | null,
|
||||
advitise?: AdvitiseData | null,
|
||||
_variant: "pc" | "h5" = "pc",
|
||||
) {
|
||||
void _variant;
|
||||
const source = items ?? advitise?.nineGridList ?? [];
|
||||
return source.filter(hasAdVisual);
|
||||
const source = items ?? advitise?.[slot] ?? [];
|
||||
return source.filter((item) => Boolean(item.images?.trim()));
|
||||
}
|
||||
|
||||
/** Banner:detailbannerList(需有图) */
|
||||
@@ -21,12 +21,6 @@ export function pickBannerAds(items?: AdvertiseInfo[] | null, advitise?: Advitis
|
||||
return source.filter((item) => Boolean(item.images?.trim()));
|
||||
}
|
||||
|
||||
/** 友情链接:friendList(需有标题) */
|
||||
export function pickFriendAds(items?: AdvertiseInfo[] | null, advitise?: AdvitiseData | null) {
|
||||
const source = items ?? advitise?.friendList ?? [];
|
||||
return source.filter((item): item is AdvertiseInfo & { title: string } => Boolean(item.title?.trim()));
|
||||
}
|
||||
|
||||
export function adHref(link?: string, fallback = "#download") {
|
||||
const value = link?.trim();
|
||||
if (!value) return fallback;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { getAdvitiseCached } from "@/lib/api/home";
|
||||
|
||||
import { AdBanners } from "./ad-banners";
|
||||
import { AdIcons } from "./ad-icons";
|
||||
import { FriendLinks, H5FriendLinks } from "../friend-links";
|
||||
import { AdSquares } from "./ad-squares";
|
||||
import type { SquareAdSlot } from "./pick";
|
||||
|
||||
/** 服务端:Icon 广告(home/base → advitise) */
|
||||
export async function AdIconsSlot({
|
||||
/** 服务端:小方块广告(BList 首页小图标 / AList 播放页小图标) */
|
||||
export async function AdSquaresSlot({
|
||||
slot,
|
||||
variant = "pc",
|
||||
labels = true,
|
||||
className = "",
|
||||
}: {
|
||||
slot: SquareAdSlot;
|
||||
variant?: "pc" | "h5";
|
||||
labels?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const advitise = await getAdvitiseCached();
|
||||
return <AdIcons variant={variant} advitise={advitise} labels={labels} className={className} />;
|
||||
return <AdSquares slot={slot} variant={variant} advitise={advitise} className={className} />;
|
||||
}
|
||||
|
||||
/** 服务端:Banner 广告 */
|
||||
@@ -29,15 +29,3 @@ export async function AdBannersSlot({
|
||||
const advitise = await getAdvitiseCached();
|
||||
return <AdBanners variant={variant} advitise={advitise} className={className} />;
|
||||
}
|
||||
|
||||
/** 服务端:PC 友情链接 */
|
||||
export async function FriendLinksSlot() {
|
||||
const advitise = await getAdvitiseCached();
|
||||
return <FriendLinks advitise={advitise} />;
|
||||
}
|
||||
|
||||
/** 服务端:H5 友情链接 */
|
||||
export async function H5FriendLinksSlot() {
|
||||
const advitise = await getAdvitiseCached();
|
||||
return <H5FriendLinks advitise={advitise} />;
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { FriendLinksSlot, H5FriendLinksSlot } from "./ads/slots";
|
||||
@@ -1,75 +0,0 @@
|
||||
import { H5SectionHeading } from "@/components/home/h5-cards";
|
||||
import type { AdvertiseInfo, AdvitiseData } from "@/lib/api/types";
|
||||
|
||||
import { adHref, pickFriendAds } from "./ads/pick";
|
||||
|
||||
/** PC 友情链接:对接 advitise.friendList,无数据不渲染 */
|
||||
export function FriendLinks({
|
||||
items,
|
||||
advitise,
|
||||
}: {
|
||||
items?: AdvertiseInfo[] | null;
|
||||
advitise?: AdvitiseData | null;
|
||||
}) {
|
||||
const apiList = pickFriendAds(items, advitise);
|
||||
if (!apiList.length) return null;
|
||||
|
||||
// 不用 design-unscaled:嵌套 zoom 会在 Chrome 把热区错位叠到上方分页上
|
||||
return (
|
||||
<section data-component="friend-links" id="links" className="border-t border-white/10 pt-1">
|
||||
<h2 className="text-2xl leading-8 font-semibold text-white/90">友情链接</h2>
|
||||
<div className="mt-[18px] flex flex-wrap gap-3">
|
||||
{apiList.map((item, index) => {
|
||||
const href = adHref(item.link, "#links");
|
||||
const external = Boolean(item.link?.trim());
|
||||
return (
|
||||
<a
|
||||
key={item.id || `${item.title}-${index}`}
|
||||
href={href}
|
||||
target={external ? "_blank" : undefined}
|
||||
rel={external ? "sponsored nofollow noopener noreferrer" : undefined}
|
||||
className="inline-flex min-w-[146px] items-center justify-center rounded-md bg-white/5 px-2 py-[9px] text-base leading-[22px] text-white/55"
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/** H5 友情链接:对接 advitise.friendList,无数据不渲染 */
|
||||
export function H5FriendLinks({
|
||||
items,
|
||||
advitise,
|
||||
}: {
|
||||
items?: AdvertiseInfo[] | null;
|
||||
advitise?: AdvitiseData | null;
|
||||
}) {
|
||||
const apiList = pickFriendAds(items, advitise);
|
||||
if (!apiList.length) return null;
|
||||
|
||||
return (
|
||||
<section id="h5-links" className="flex flex-col gap-3">
|
||||
<H5SectionHeading title="友情链接" more={false} />
|
||||
<div className="flex flex-wrap gap-[18px]">
|
||||
{apiList.map((item, index) => {
|
||||
const href = adHref(item.link, "#h5-links");
|
||||
const external = Boolean(item.link?.trim());
|
||||
return (
|
||||
<a
|
||||
key={item.id || `${item.title}-${index}`}
|
||||
href={href}
|
||||
target={external ? "_blank" : undefined}
|
||||
rel={external ? "sponsored nofollow noopener noreferrer" : undefined}
|
||||
className="rounded-md border border-white/5 bg-white/10 px-3 py-[9px] text-xs leading-[18px] text-white/90"
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { AdIconsSlot } from "./ads/slots";
|
||||
|
||||
/** @deprecated 使用 AdIconsSlot */
|
||||
export async function H5AdGridSlot({ className = "" }: { className?: string }) {
|
||||
return <AdIconsSlot variant="h5" className={className} />;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { AdvertiseInfo, AdvitiseData } from "@/lib/api/types";
|
||||
|
||||
import { AdIcons } from "./ads/ad-icons";
|
||||
|
||||
/** @deprecated 使用 AdIcons variant="h5";保留别名兼容旧引用 */
|
||||
export function H5AdGrid({
|
||||
items,
|
||||
advitise,
|
||||
className = "",
|
||||
}: {
|
||||
items?: AdvertiseInfo[] | null;
|
||||
advitise?: AdvitiseData | null;
|
||||
className?: string;
|
||||
}) {
|
||||
return <AdIcons variant="h5" items={items} advitise={advitise} className={className} />;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { AdBannersSlot } from "./ads/slots";
|
||||
|
||||
/** @deprecated 使用 AdBannersSlot */
|
||||
export async function H5BannerAdsSlot({ className = "" }: { className?: string }) {
|
||||
return <AdBannersSlot variant="h5" className={className} />;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { AdvertiseInfo, AdvitiseData } from "@/lib/api/types";
|
||||
|
||||
import { AdBanners } from "./ads/ad-banners";
|
||||
|
||||
/** @deprecated 使用 AdBanners variant="h5";保留别名兼容旧引用 */
|
||||
export function H5BannerAds({
|
||||
items,
|
||||
advitise,
|
||||
className = "",
|
||||
}: {
|
||||
items?: AdvertiseInfo[] | null;
|
||||
advitise?: AdvitiseData | null;
|
||||
className?: string;
|
||||
}) {
|
||||
return <AdBanners variant="h5" items={items} advitise={advitise} className={className} />;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { lockBodyScroll } from "@/lib/browser/scroll-lock";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { useCallback, useEffect, useRef, useState, useSyncExternalStore, type MouseEvent } from "react";
|
||||
|
||||
@@ -132,11 +133,10 @@ export function DownloadDialogHost({ appAddr }: { appAddr?: string | null }) {
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKeyDown = (event: KeyboardEvent) => event.key === "Escape" && close();
|
||||
const { overflow } = document.body.style;
|
||||
document.body.style.overflow = "hidden";
|
||||
const unlockScroll = lockBodyScroll();
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
document.body.style.overflow = overflow;
|
||||
unlockScroll();
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [open, close]);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { toast } from "@heroui/react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
|
||||
import { openLoginDialog } from "@/components/auth/login-dialog";
|
||||
import { AdBanners, AdIcons } from "@/components/common/ads";
|
||||
import { AdBanners } from "@/components/common/ads";
|
||||
import { BackButton } from "@/components/common/back-button";
|
||||
import { pickPlayletCardTags } from "@/components/common/card-tags";
|
||||
import { DecryptedImage } from "@/components/common/decrypted-image";
|
||||
@@ -70,9 +70,9 @@ export function DramaDetailPage({
|
||||
related?: RelatedDrama[];
|
||||
/** 服务端渲染的 PC banner */
|
||||
promo?: ReactNode;
|
||||
/** 服务端渲染的 PC icon */
|
||||
/** 服务端渲染的播放页小图标(AList):PC */
|
||||
adIcons?: ReactNode;
|
||||
/** 服务端渲染的 H5 icon */
|
||||
/** 服务端渲染的播放页小图标(AList):H5 */
|
||||
adGrid?: ReactNode;
|
||||
/** 服务端渲染的 H5 banner */
|
||||
bannerAds?: ReactNode;
|
||||
@@ -254,7 +254,7 @@ export function DramaDetailPage({
|
||||
</figure>
|
||||
</article>
|
||||
|
||||
{adIcons ?? <AdIcons variant="pc" />}
|
||||
{adIcons}
|
||||
|
||||
<EpisodeDirectory detail={detail} variant="panel" />
|
||||
|
||||
@@ -298,7 +298,7 @@ export function DramaDetailPage({
|
||||
) : null}
|
||||
</article>
|
||||
|
||||
{adGrid ?? <AdIcons variant="h5" />}
|
||||
{adGrid}
|
||||
|
||||
<EpisodeDirectory detail={detail} variant="h5" />
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import Link from "next/link";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
|
||||
import { openLoginDialog } from "@/components/auth/login-dialog";
|
||||
import { AdBanners, AdIcons } from "@/components/common/ads";
|
||||
import { AdBanners } from "@/components/common/ads";
|
||||
import { pickPlayletCardTags } from "@/components/common/card-tags";
|
||||
import {
|
||||
dramaDetailHref,
|
||||
@@ -67,15 +67,22 @@ export function DramaPlayPage({
|
||||
episode,
|
||||
src,
|
||||
related = [],
|
||||
adIcons,
|
||||
promo,
|
||||
topAds,
|
||||
h5Ads,
|
||||
h5Banners,
|
||||
}: {
|
||||
detail: DramaDetail;
|
||||
episode: number;
|
||||
src: string;
|
||||
related?: RelatedDrama[];
|
||||
adIcons?: ReactNode;
|
||||
promo?: ReactNode;
|
||||
/** 服务端渲染的播放页小图标(AList):PC 顶部 */
|
||||
topAds?: ReactNode;
|
||||
/** 服务端渲染的播放页小图标(AList):H5 正文下方 */
|
||||
h5Ads?: ReactNode;
|
||||
/** 服务端渲染的详情横幅(detailbannerList):H5 */
|
||||
h5Banners?: ReactNode;
|
||||
}) {
|
||||
const [liked, setLiked] = useState(Boolean(detail.isLike));
|
||||
const [displayCountLike, setDisplayCountLike] = useState(() => initialDisplayCountLike(detail));
|
||||
@@ -229,6 +236,7 @@ export function DramaPlayPage({
|
||||
className={`relative hidden flex-col gap-8 pb-14 lg:flex ${expanded ? "" : `pt-6 ${pcFrame}`}`}
|
||||
>
|
||||
{!expanded ? <PageBreadcrumb items={crumbs} /> : null}
|
||||
{!expanded ? topAds : null}
|
||||
|
||||
<div className={expanded ? "contents" : "flex flex-col gap-8"}>
|
||||
<div className={expanded ? "contents" : "flex h-[648px] gap-5"}>
|
||||
@@ -294,7 +302,6 @@ export function DramaPlayPage({
|
||||
|
||||
{expanded ? <EpisodeDirectory detail={detail} current={episode} variant="panel" /> : null}
|
||||
|
||||
{adIcons ?? <AdIcons variant="pc" />}
|
||||
|
||||
<DramaRecommend items={related} />
|
||||
|
||||
@@ -317,6 +324,8 @@ export function DramaPlayPage({
|
||||
onLike={toggleLike}
|
||||
onCollect={toggleCollect}
|
||||
onShare={share}
|
||||
ads={h5Ads}
|
||||
bannerAds={h5Banners}
|
||||
/>
|
||||
|
||||
<div className="max-lg:hidden">
|
||||
|
||||
@@ -64,6 +64,10 @@ type H5PlayProps = {
|
||||
onCollect: () => void;
|
||||
onShare: () => void;
|
||||
onPlaySuccess?: () => void;
|
||||
/** 播放页小图标广告(AList) */
|
||||
ads?: ReactNode;
|
||||
/** 详情横幅广告(detailbannerList) */
|
||||
bannerAds?: ReactNode;
|
||||
};
|
||||
|
||||
function LockedNotice() {
|
||||
@@ -152,6 +156,8 @@ export function H5Play({
|
||||
onCollect,
|
||||
onShare,
|
||||
onPlaySuccess,
|
||||
ads,
|
||||
bannerAds,
|
||||
}: H5PlayProps) {
|
||||
const router = useRouter();
|
||||
const playerRef = useRef<XgPlayerInstance | null>(null);
|
||||
@@ -316,9 +322,13 @@ export function H5Play({
|
||||
</nav>
|
||||
</article>
|
||||
|
||||
{ads}
|
||||
|
||||
<EpisodeDirectory detail={detail} current={episode} variant="h5" />
|
||||
|
||||
<DramaRecommend items={related} compact />
|
||||
|
||||
{bannerAds}
|
||||
</div>
|
||||
|
||||
<BottomSheet open={sheet === "speed"} onClose={closeSheet} label="倍速" className="gap-3 px-4 py-[18px]">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { AdBanners } from "@/components/common/ads/ad-banners";
|
||||
import { CardTags } from "@/components/common/card-tags";
|
||||
import {
|
||||
MEDIA_CARD_COVER,
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
MEDIA_CARD_TITLE_PC_MD,
|
||||
} from "@/components/common/media-card-hover";
|
||||
import { dramaHref } from "@/components/short-drama/data";
|
||||
import type { AdvertiseInfo, AdvitiseData } from "@/lib/api/types";
|
||||
|
||||
import type { Drama } from "./data";
|
||||
import { Badge } from "./section-heading";
|
||||
@@ -80,15 +78,3 @@ export function ThemeBlock({ title, subtitle, cover }: { title: string; subtitle
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated 使用 AdBanners;covers 保留兼容,有接口数据时优先接口 */
|
||||
export function PromoRow({
|
||||
items,
|
||||
advitise,
|
||||
}: {
|
||||
covers?: readonly string[];
|
||||
items?: AdvertiseInfo[] | null;
|
||||
advitise?: AdvitiseData | null;
|
||||
}) {
|
||||
return <AdBanners variant="pc" items={items} advitise={advitise} />;
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { AdvertiseInfo, AdvitiseData } from "@/lib/api/types";
|
||||
|
||||
import { AdIcons } from "@/components/common/ads/ad-icons";
|
||||
|
||||
/** @deprecated 使用 AdIcons;保留别名兼容首页旧引用 */
|
||||
export function HomeAdRow({
|
||||
items,
|
||||
advitise,
|
||||
}: {
|
||||
items?: AdvertiseInfo[] | null;
|
||||
advitise?: AdvitiseData | null;
|
||||
}) {
|
||||
return <AdIcons variant="pc" items={items} advitise={advitise} />;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AdBanners, AdIcons } from "@/components/common/ads";
|
||||
import { H5FriendLinks } from "@/components/common/friend-links";
|
||||
import { AdSquares } from "@/components/common/ads";
|
||||
import type { HomePlayletResponse } from "@/lib/api/types";
|
||||
|
||||
import { DownloadBannerH5 } from "./page-sections";
|
||||
@@ -16,13 +15,10 @@ export function HomePageH5({ home }: { home?: HomePlayletResponse | null }) {
|
||||
|
||||
<NoticeMarquee items={home?.announcements} variant="h5" />
|
||||
|
||||
<AdIcons variant="h5" advitise={home?.advitise} />
|
||||
<AdBanners variant="h5" advitise={home?.advitise} />
|
||||
<AdSquares slot="BList" variant="h5" advitise={home?.advitise} />
|
||||
|
||||
{hasModules ? <HomeModules modules={home?.modules} variant="h5" /> : null}
|
||||
|
||||
<H5FriendLinks advitise={home?.advitise} />
|
||||
|
||||
<DownloadBannerH5 />
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AdIcons } from "@/components/common/ads";
|
||||
import { FriendLinks } from "@/components/common/friend-links";
|
||||
import { AdSquares } from "@/components/common/ads";
|
||||
import { SeoBreadcrumb } from "@/components/seo/seo-breadcrumb";
|
||||
import type { HomePlayletResponse } from "@/lib/api/types";
|
||||
|
||||
@@ -31,12 +30,11 @@ export function HomePage({
|
||||
<main data-component="home-page" className="design-frame hidden flex-col gap-8 pt-8 pb-10 lg:flex">
|
||||
<HeroBanner banners={home?.homeBannerData} />
|
||||
<NoticeMarquee items={home?.announcements} />
|
||||
<AdIcons variant="pc" advitise={home?.advitise} />
|
||||
<AdSquares slot="BList" variant="pc" advitise={home?.advitise} />
|
||||
|
||||
{hasModules ? <HomeModules modules={home?.modules} /> : null}
|
||||
|
||||
<DownloadBanner />
|
||||
<FriendLinks advitise={home?.advitise} />
|
||||
</main>
|
||||
<FloatingActions />
|
||||
</>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useEffect, useState, type ReactNode } from "react";
|
||||
|
||||
import { openAppDownload, useAppDownloadHref } from "@/components/download/download-dialog";
|
||||
import { footerNavItems } from "@/components/layout/footer-nav-items";
|
||||
import { lockBodyScroll } from "@/lib/browser/scroll-lock";
|
||||
import type { AppTranslationInfo, ConfigInfo, TranslationCategory, TranslationInfo } from "@/lib/api/types";
|
||||
import { OFFICIAL_TG_HANDLE, OFFICIAL_TG_URL } from "@/lib/seo/metadata";
|
||||
|
||||
@@ -77,11 +78,10 @@ export function MobileMenu({
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") onClose();
|
||||
};
|
||||
const { overflow } = document.body.style;
|
||||
document.body.style.overflow = "hidden";
|
||||
const unlockScroll = lockBodyScroll();
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
document.body.style.overflow = overflow;
|
||||
unlockScroll();
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [open, onClose]);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AdBannersSlot, AdIconsSlot, FriendLinksSlot, H5FriendLinksSlot } from "@/components/common/ads/slots";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { ChannelHeader, H5ChannelHeader } from "@/components/common/channel-header";
|
||||
@@ -68,7 +67,6 @@ export function MorePage({ data }: { data: MorePageData }) {
|
||||
<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 />
|
||||
|
||||
@@ -80,14 +78,11 @@ export function MorePage({ data }: { data: MorePageData }) {
|
||||
|
||||
{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} />
|
||||
|
||||
@@ -106,9 +101,7 @@ export function MorePage({ data }: { data: MorePageData }) {
|
||||
</div>
|
||||
) : null}
|
||||
<div className="design-frame flex flex-col gap-6 pb-[43px]">
|
||||
<AdBannersSlot variant="pc" />
|
||||
<DownloadBanner />
|
||||
<FriendLinksSlot />
|
||||
</div>
|
||||
</main>
|
||||
<FloatingActions />
|
||||
|
||||
@@ -40,18 +40,8 @@ export function formatHeat(count?: number | null) {
|
||||
}
|
||||
|
||||
export function mapContentListItem(item: PlayletContentListItem, dataType: 1 | 2): ShortDramaItem | null {
|
||||
if (item.type === 2) {
|
||||
const ad = item.advertise;
|
||||
if (!ad) return null;
|
||||
return {
|
||||
kind: "ad",
|
||||
id: `ad-${ad.id || ad.link || "unknown"}`,
|
||||
title: (ad.title || "").trim() || "广告",
|
||||
subtitle: "",
|
||||
cover: (ad.images || "").trim(),
|
||||
href: (ad.link || "").trim() || undefined,
|
||||
};
|
||||
}
|
||||
// 信息流广告(type=2)不展示:网页只保留后台 5 个站点广告位
|
||||
if (item.type === 2) return null;
|
||||
|
||||
if (dataType === 1) {
|
||||
const playlet = item.playlet;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AdBannersSlot, AdIconsSlot, FriendLinksSlot, H5FriendLinksSlot } from "@/components/common/ads/slots";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { ChannelHeader, H5ChannelHeader } from "@/components/common/channel-header";
|
||||
@@ -66,7 +65,6 @@ export function RankingPage({ tab, list }: { tab: RankingTab; list: RankingListR
|
||||
/>
|
||||
<main data-component="ranking-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" />
|
||||
|
||||
<section id="h5-list" className="flex scroll-mt-16 flex-col gap-[18px]">
|
||||
<RankingTabs tabs={rankingTabs} tab={tab} />
|
||||
@@ -94,14 +92,11 @@ export function RankingPage({ tab, list }: { tab: RankingTab; list: RankingListR
|
||||
)}
|
||||
</section>
|
||||
|
||||
<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" />
|
||||
|
||||
<div id="pc-list" className="scroll-mt-24 flex flex-col gap-6">
|
||||
<RankingTabs tabs={rankingTabs} tab={tab} />
|
||||
@@ -126,9 +121,7 @@ export function RankingPage({ tab, list }: { tab: RankingTab; list: RankingListR
|
||||
</div>
|
||||
) : null}
|
||||
<div className="design-frame flex flex-col gap-6 pb-[43px]">
|
||||
<AdBannersSlot variant="pc" />
|
||||
<DownloadBanner />
|
||||
<FriendLinksSlot />
|
||||
</div>
|
||||
</main>
|
||||
<FloatingActions />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Form from "next/form";
|
||||
|
||||
import { AdBannersSlot, AdIconsSlot } from "@/components/common/ads/slots";
|
||||
import { EmptyState } from "@/components/common/empty-state";
|
||||
import { TabLink } from "@/components/common/filter-link";
|
||||
import { Pagination } from "@/components/common/pagination";
|
||||
@@ -138,14 +137,6 @@ export function SearchPage({ list }: { list: SearchListResult }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="hidden lg:block">
|
||||
<AdIconsSlot variant="pc" />
|
||||
<AdBannersSlot variant="pc" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-[18px] lg:hidden">
|
||||
<AdIconsSlot variant="h5" />
|
||||
<AdBannersSlot variant="h5" />
|
||||
</div>
|
||||
</div>
|
||||
<FloatingActions />
|
||||
</main>
|
||||
|
||||
@@ -24,32 +24,23 @@ export type Genre = (typeof genres)[number];
|
||||
export type Sort = (typeof sorts)[number];
|
||||
export type Region = (typeof regions)[number];
|
||||
|
||||
export type ShortDramaItem =
|
||||
| {
|
||||
kind: "drama";
|
||||
id: string;
|
||||
slug?: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
/** 左下角标签,最多 3 个(优先带 id 以便跳转) */
|
||||
tags: CardTagItem[];
|
||||
heat: string;
|
||||
episodes: string;
|
||||
badge?: BadgeTone;
|
||||
cover: string;
|
||||
/** 发布时间(接口原值,视频卡展示日期用) */
|
||||
date?: string;
|
||||
/** 覆盖默认详情/播放链接(如历史继续看到某集) */
|
||||
href?: string;
|
||||
}
|
||||
| {
|
||||
kind: "ad";
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
cover: string;
|
||||
href?: string;
|
||||
};
|
||||
export type ShortDramaItem = {
|
||||
kind: "drama";
|
||||
id: string;
|
||||
slug?: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
/** 左下角标签,最多 3 个(优先带 id 以便跳转) */
|
||||
tags: CardTagItem[];
|
||||
heat: string;
|
||||
episodes: string;
|
||||
badge?: BadgeTone;
|
||||
cover: string;
|
||||
/** 发布时间(接口原值,视频卡展示日期用) */
|
||||
date?: string;
|
||||
/** 覆盖默认详情/播放链接(如历史继续看到某集) */
|
||||
href?: string;
|
||||
};
|
||||
|
||||
export const PAGE_SIZE = 24;
|
||||
export const TOTAL_PAGES = 11;
|
||||
@@ -59,16 +50,8 @@ export function shortDramaSortType(sort: Sort): 1 | 2 {
|
||||
}
|
||||
|
||||
export function mapPlayletListItem(item: PlayletListItem): ShortDramaItem | null {
|
||||
if (item.type === 2) {
|
||||
return {
|
||||
kind: "ad",
|
||||
id: `ad-${item.advertiseId || item.id || item.link || "unknown"}`,
|
||||
title: (item.title || "").trim() || "广告",
|
||||
subtitle: "",
|
||||
cover: (item.cover || "").trim(),
|
||||
href: (item.link || "").trim() || undefined,
|
||||
};
|
||||
}
|
||||
// 信息流广告(type=2)不展示:网页只保留后台 5 个站点广告位
|
||||
if (item.type === 2) return null;
|
||||
|
||||
if (!item.id) return null;
|
||||
const tags = pickPlayletCardTags(item);
|
||||
@@ -124,34 +107,6 @@ export function buildShortDramaList(
|
||||
};
|
||||
}
|
||||
|
||||
/** @deprecated 本地 mock,供「更多」等未接接口页面临时使用 */
|
||||
const adSlots = new Set([4, 9, 14, 18, 23]);
|
||||
const freshSlots = new Set([3, 11, 21]);
|
||||
|
||||
export function getShortDramas(genre: Genre, sort: Sort, page: number, region: Region = "全部"): ShortDramaItem[] {
|
||||
const seed = `${genres.indexOf(genre)}-${sorts.indexOf(sort)}-${regions.indexOf(region)}-${page}`;
|
||||
|
||||
return Array.from({ length: PAGE_SIZE }, (_, index): ShortDramaItem => {
|
||||
const id = `short-${seed}-${index}`;
|
||||
const cover = "";
|
||||
|
||||
if (adSlots.has(index)) {
|
||||
return { kind: "ad", id, title: "广告文案", subtitle: "我是副标题", cover };
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "drama",
|
||||
id,
|
||||
title: "有恩必报",
|
||||
subtitle: "权谋交锋爱意藏在局中",
|
||||
tags: [{ name: genre === "全部" ? "标签" : genre }],
|
||||
heat: "25.8万 热度",
|
||||
episodes: "30集",
|
||||
badge: freshSlots.has(index) ? "fresh" : "original",
|
||||
cover,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 有 slug 时路径用 slug,否则回退 id */
|
||||
export function dramaPathKey(id: string | number, slug?: string | null) {
|
||||
|
||||
@@ -16,7 +16,7 @@ function RankLabel({ rank, className = "" }: { rank?: number; className?: string
|
||||
}
|
||||
|
||||
/** 短剧卡元信息:集数 + 热度(文字行,不叠在封面上) */
|
||||
function DramaMeta({ item, className }: { item: Extract<ShortDramaItem, { kind: "drama" }>; className: string }) {
|
||||
function DramaMeta({ item, className }: { item: ShortDramaItem; className: string }) {
|
||||
return (
|
||||
<p className={`flex min-w-0 items-center gap-2 text-[#8E8C99] ${className}`}>
|
||||
{item.episodes ? <span className="shrink-0 text-[#D9C59A]">共{item.episodes}</span> : null}
|
||||
@@ -31,27 +31,7 @@ function DramaMeta({ item, className }: { item: Extract<ShortDramaItem, { kind:
|
||||
);
|
||||
}
|
||||
|
||||
function AdDramaCard({ item, compact }: { item: ShortDramaItem; compact?: boolean }) {
|
||||
return (
|
||||
<a
|
||||
href={item.href || (compact ? "#h5-download" : "#download")}
|
||||
target={item.href ? "_blank" : undefined}
|
||||
rel={item.href ? "sponsored nofollow noopener noreferrer" : undefined}
|
||||
className={`group flex flex-col ${compact ? "gap-1.5" : "w-[266px] gap-2.5"}`}
|
||||
>
|
||||
<span className={`${HL_COVER} aspect-[2/3]`}>
|
||||
<DecryptedImage src={item.cover} fallback={IMG_PLACEHOLDER} alt={item.title} className={HL_COVER_IMG} />
|
||||
<span className="absolute top-0 left-0 rounded-br-md bg-white/85 px-1.5 py-0.5 text-[10px] leading-[14px] text-black">
|
||||
推广
|
||||
</span>
|
||||
</span>
|
||||
<span className={`truncate text-white ${compact ? "text-sm leading-5" : "media-card-title-md"}`}>{item.title}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function ShortDramaCard({ item, rank }: { item: ShortDramaItem; rank?: number }) {
|
||||
if (item.kind === "ad") return <AdDramaCard item={item} />;
|
||||
const href = item.href || dramaHref(item.id, item.slug);
|
||||
|
||||
return (
|
||||
@@ -84,7 +64,6 @@ export function H5ShortDramaCard({
|
||||
/** @deprecated 新卡片不在封面显示集数,保留参数兼容调用方 */
|
||||
showEpisodes?: boolean;
|
||||
}) {
|
||||
if (item.kind === "ad") return <AdDramaCard item={item} compact />;
|
||||
const href = item.href || dramaHref(item.id, item.slug);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AdBannersSlot, AdIconsSlot, FriendLinksSlot, H5FriendLinksSlot } from "@/components/common/ads/slots";
|
||||
import { ChannelHeader, H5ChannelHeader } from "@/components/common/channel-header";
|
||||
import { EmptyState } from "@/components/common/empty-state";
|
||||
import { FilterChipLink, FilterOptionLink } from "@/components/common/filter-link";
|
||||
@@ -25,7 +24,6 @@ export function ShortDramaPage({ list }: { list: ShortDramaListResult }) {
|
||||
<SeoBreadcrumb items={[{ name: "首页", href: "/" }, { name: "短剧" }]} />
|
||||
<main data-component="short-drama-page" className="flex w-full min-w-0 flex-col gap-[18px] overflow-x-clip px-4 py-3 lg:hidden">
|
||||
<H5ChannelHeader title="短剧" />
|
||||
<AdIconsSlot variant="h5" />
|
||||
|
||||
<div
|
||||
id="h5-list"
|
||||
@@ -89,14 +87,11 @@ export function ShortDramaPage({ list }: { list: ShortDramaListResult }) {
|
||||
)}
|
||||
</section>
|
||||
|
||||
<AdBannersSlot variant="h5" />
|
||||
<H5FriendLinksSlot />
|
||||
</main>
|
||||
|
||||
<main className="hidden lg:block">
|
||||
<div className="design-frame flex flex-col gap-6 py-[43px]">
|
||||
<ChannelHeader title="短剧" />
|
||||
<AdIconsSlot variant="pc" />
|
||||
|
||||
<div
|
||||
id="pc-list"
|
||||
@@ -155,9 +150,7 @@ export function ShortDramaPage({ list }: { list: ShortDramaListResult }) {
|
||||
</div>
|
||||
) : null}
|
||||
<div className="design-frame flex flex-col gap-6 pb-[43px]">
|
||||
<AdBannersSlot variant="pc" />
|
||||
<DownloadBanner />
|
||||
<FriendLinksSlot />
|
||||
</div>
|
||||
</main>
|
||||
<FloatingActions />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { AdBannersSlot, AdIconsSlot } from "@/components/common/ads/slots";
|
||||
import { EmptyState } from "@/components/common/empty-state";
|
||||
import { Pagination } from "@/components/common/pagination";
|
||||
import { buildPageHrefMap } from "@/components/common/pagination-map";
|
||||
@@ -101,14 +100,6 @@ export function TagPage({ data }: { data: TagPageData }) {
|
||||
<Pagination page={page} total={totalPages} hrefMap={hrefMap} className="max-lg:[&_form]:hidden" />
|
||||
) : null}
|
||||
|
||||
<div className="hidden space-y-5 lg:block">
|
||||
<AdIconsSlot variant="pc" />
|
||||
<AdBannersSlot variant="pc" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-[18px] lg:hidden">
|
||||
<AdIconsSlot variant="h5" />
|
||||
<AdBannersSlot variant="h5" />
|
||||
</div>
|
||||
</div>
|
||||
<FloatingActions />
|
||||
</main>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
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";
|
||||
@@ -165,7 +164,6 @@ export function TopicPage({ data }: { data: TopicPageData }) {
|
||||
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 />
|
||||
@@ -181,14 +179,11 @@ export function TopicPage({ data }: { data: TopicPageData }) {
|
||||
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} />
|
||||
@@ -204,9 +199,7 @@ export function TopicPage({ data }: { data: TopicPageData }) {
|
||||
<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 />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { AdBanners, AdIcons } from "@/components/common/ads";
|
||||
import { AdBanners } from "@/components/common/ads";
|
||||
import { BackButton } from "@/components/common/back-button";
|
||||
import { HlAction, InfoList, SectionTitle, TagLinks, VideoRecommend, type InfoRow, type TagLinkItem } from "@/components/media-page/parts";
|
||||
import { XgPlayer } from "@/components/player/xg-player";
|
||||
@@ -30,8 +30,8 @@ export function H5VideoPlay({
|
||||
infoRows,
|
||||
tags,
|
||||
onPlaySuccess,
|
||||
adGrid,
|
||||
bannerAds,
|
||||
squareAds,
|
||||
liked,
|
||||
likeNum,
|
||||
collected,
|
||||
@@ -46,8 +46,9 @@ export function H5VideoPlay({
|
||||
infoRows: InfoRow[];
|
||||
tags: TagLinkItem[];
|
||||
onPlaySuccess?: () => void;
|
||||
adGrid?: ReactNode;
|
||||
bannerAds?: ReactNode;
|
||||
/** 播放页小图标广告(AList) */
|
||||
squareAds?: ReactNode;
|
||||
}) {
|
||||
const title = detail.title || "视频";
|
||||
const desc = detail.desc?.trim() || "";
|
||||
@@ -91,7 +92,7 @@ export function H5VideoPlay({
|
||||
) : null}
|
||||
</article>
|
||||
|
||||
{adGrid ?? <AdIcons variant="h5" />}
|
||||
{squareAds}
|
||||
|
||||
<VideoRecommend items={related} compact />
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { toast } from "@heroui/react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
|
||||
import { openLoginDialog } from "@/components/auth/login-dialog";
|
||||
import { AdBanners, AdIcons } from "@/components/common/ads";
|
||||
import { AdBanners } from "@/components/common/ads";
|
||||
import { formatDay, formatDuration } from "@/components/common/media-meta";
|
||||
import { PcPlayer } from "@/components/drama-play/pc-player";
|
||||
import { FloatingActions } from "@/components/home/floating-actions";
|
||||
@@ -56,16 +56,18 @@ export function VideoPlayPage({
|
||||
src,
|
||||
related = [],
|
||||
promo,
|
||||
adIcons,
|
||||
adGrid,
|
||||
topAds,
|
||||
h5Ads,
|
||||
bannerAds,
|
||||
}: {
|
||||
detail: VideoDetail;
|
||||
src: string;
|
||||
related?: ShortDramaItem[];
|
||||
promo?: ReactNode;
|
||||
adIcons?: ReactNode;
|
||||
adGrid?: ReactNode;
|
||||
/** 服务端渲染的播放页小图标(AList):PC 顶部 */
|
||||
topAds?: ReactNode;
|
||||
/** 服务端渲染的播放页小图标(AList):H5 正文下方 */
|
||||
h5Ads?: ReactNode;
|
||||
bannerAds?: ReactNode;
|
||||
}) {
|
||||
const [liked, setLiked] = useState(Boolean(detail.isLike));
|
||||
@@ -226,6 +228,7 @@ export function VideoPlayPage({
|
||||
className="relative mx-auto hidden w-[1700px] flex-col gap-8 pt-6 pb-14 [zoom:var(--page-scale,1)] lg:flex"
|
||||
>
|
||||
<PageBreadcrumb items={crumbs} />
|
||||
{topAds}
|
||||
|
||||
<PcPlayer
|
||||
src={src}
|
||||
@@ -263,7 +266,6 @@ export function VideoPlayPage({
|
||||
) : null}
|
||||
</article>
|
||||
|
||||
{adIcons ?? <AdIcons variant="pc" labels={false} />}
|
||||
|
||||
<VideoRecommend items={related} />
|
||||
|
||||
@@ -283,8 +285,8 @@ export function VideoPlayPage({
|
||||
onLike={liking ? () => undefined : toggleLike}
|
||||
onCollect={collecting ? () => undefined : toggleCollect}
|
||||
onShare={share}
|
||||
adGrid={adGrid}
|
||||
bannerAds={bannerAds}
|
||||
squareAds={h5Ads}
|
||||
/>
|
||||
|
||||
<div className="max-lg:hidden">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { pickVideoCardTags } from "@/components/common/card-tags";
|
||||
import { genres, regions, sorts, type Genre, type Region, type ShortDramaItem, type Sort } from "@/components/short-drama/data";
|
||||
import { sorts, type ShortDramaItem, type Sort } from "@/components/short-drama/data";
|
||||
import { formatHeat } from "@/components/ranking/data";
|
||||
import type { VideoCategoryListItem, VideoListItem, VideoListResp } from "@/lib/api/types";
|
||||
|
||||
@@ -13,16 +13,8 @@ export function videoSortType(sort: VideoSort): 1 | 2 {
|
||||
}
|
||||
|
||||
export function mapVideoListItem(item: VideoListItem): ShortDramaItem | null {
|
||||
if (item.type === 2) {
|
||||
return {
|
||||
kind: "ad",
|
||||
id: `ad-${item.advertiseId || item.id || item.link || "unknown"}`,
|
||||
title: (item.title || "").trim() || "广告",
|
||||
subtitle: "",
|
||||
cover: (item.coverImg || "").trim(),
|
||||
href: (item.link || "").trim() || undefined,
|
||||
};
|
||||
}
|
||||
// 信息流广告(type=2)不展示:网页只保留后台 5 个站点广告位
|
||||
if (item.type === 2) return null;
|
||||
|
||||
if (!item.id) return null;
|
||||
const slug = (item.slug || "").trim();
|
||||
@@ -77,29 +69,3 @@ export function buildVideoList(
|
||||
};
|
||||
}
|
||||
|
||||
/** @deprecated 本地 mock,供「更多」等未接接口页面临时使用 */
|
||||
const adSlots = new Set([5, 11, 17, 24, 25]);
|
||||
export function getVideos(genre: Genre, sort: Sort, page: number, region: Region = "全部"): ShortDramaItem[] {
|
||||
const seed = `${genres.indexOf(genre)}-${sorts.indexOf(sort)}-${regions.indexOf(region)}-${page}`;
|
||||
|
||||
return Array.from({ length: VIDEO_PAGE_SIZE }, (_, index): ShortDramaItem => {
|
||||
const id = `video-${seed}-${index}`;
|
||||
const cover = "";
|
||||
|
||||
if (adSlots.has(index)) {
|
||||
return { kind: "ad", id, title: "广告文案", subtitle: "我是副标题", cover };
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "drama",
|
||||
id,
|
||||
title: "有恩必报",
|
||||
subtitle: "权谋交锋爱意藏在局中",
|
||||
tags: [{ name: genre === "全部" ? "标签" : genre }],
|
||||
heat: "25.8万 热度",
|
||||
episodes: "30集",
|
||||
badge: "original",
|
||||
cover,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ function RankLabel({ rank, className = "" }: { rank?: number; className?: string
|
||||
}
|
||||
|
||||
/** 视频卡元信息:观看数 + 发布日期 */
|
||||
function VideoMeta({ item, className }: { item: Extract<ShortDramaItem, { kind: "drama" }>; className: string }) {
|
||||
function VideoMeta({ item, className }: { item: ShortDramaItem; className: string }) {
|
||||
const day = formatDay(item.date);
|
||||
return (
|
||||
<p className={`flex min-w-0 items-center gap-2 text-[#8E8C99] ${className}`}>
|
||||
@@ -42,27 +42,7 @@ function VideoMeta({ item, className }: { item: Extract<ShortDramaItem, { kind:
|
||||
);
|
||||
}
|
||||
|
||||
function AdVideoCard({ item, compact }: { item: ShortDramaItem; compact?: boolean }) {
|
||||
return (
|
||||
<a
|
||||
href={item.href || (compact ? "#h5-download" : "#download")}
|
||||
target={item.href ? "_blank" : undefined}
|
||||
rel={item.href ? "sponsored nofollow noopener noreferrer" : undefined}
|
||||
className={`group flex flex-col ${compact ? "gap-1.5" : "w-[324px] gap-2.5"}`}
|
||||
>
|
||||
<span className={`${HL_COVER} aspect-[16/10]`}>
|
||||
<DecryptedImage src={item.cover} fallback={IMG_PLACEHOLDER} alt={item.title} className={HL_COVER_IMG} />
|
||||
<span className="absolute top-0 left-0 rounded-br-md bg-white/85 px-1.5 py-0.5 text-[10px] leading-[14px] text-black">
|
||||
推广
|
||||
</span>
|
||||
</span>
|
||||
<span className={`line-clamp-2 text-white ${compact ? "text-sm leading-5" : "media-card-title-md"}`}>{item.title}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function VideoCard({ item, rank }: { item: ShortDramaItem; rank?: number }) {
|
||||
if (item.kind === "ad") return <AdVideoCard item={item} />;
|
||||
const href = item.href || videoHref(item.id, item.slug);
|
||||
|
||||
return (
|
||||
@@ -92,7 +72,6 @@ export function H5VideoCard({
|
||||
/** @deprecated 新卡片不在封面显示集数,保留参数兼容调用方 */
|
||||
showEpisodes?: boolean;
|
||||
}) {
|
||||
if (item.kind === "ad") return <AdVideoCard item={item} compact />;
|
||||
const href = item.href || videoHref(item.id, item.slug);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AdBannersSlot, AdIconsSlot, FriendLinksSlot, H5FriendLinksSlot } from "@/components/common/ads/slots";
|
||||
import { ChannelHeader, H5ChannelHeader } from "@/components/common/channel-header";
|
||||
import { EmptyState } from "@/components/common/empty-state";
|
||||
import { FilterChipLink, FilterOptionLink } from "@/components/common/filter-link";
|
||||
@@ -27,7 +26,6 @@ export function VideoPage({ list }: { list: VideoListResult }) {
|
||||
<SeoBreadcrumb items={[{ name: "首页", href: "/" }, { name: "视频" }]} />
|
||||
<main data-component="video-page" className="flex w-full min-w-0 flex-col gap-[18px] overflow-x-clip px-4 py-3 lg:hidden">
|
||||
<H5ChannelHeader title="视频" />
|
||||
<AdIconsSlot variant="h5" />
|
||||
|
||||
<div
|
||||
id="h5-list"
|
||||
@@ -91,14 +89,11 @@ export function VideoPage({ list }: { list: VideoListResult }) {
|
||||
)}
|
||||
</section>
|
||||
|
||||
<AdBannersSlot variant="h5" />
|
||||
<H5FriendLinksSlot />
|
||||
</main>
|
||||
|
||||
<main className="hidden lg:block">
|
||||
<div className="design-frame flex flex-col gap-6 py-[43px]">
|
||||
<ChannelHeader title="视频" />
|
||||
<AdIconsSlot variant="pc" />
|
||||
|
||||
<div
|
||||
id="pc-list"
|
||||
@@ -157,9 +152,7 @@ export function VideoPage({ list }: { list: VideoListResult }) {
|
||||
</div>
|
||||
) : null}
|
||||
<div className="design-frame flex flex-col gap-6 pb-[43px]">
|
||||
<AdBannersSlot variant="pc" />
|
||||
<DownloadBanner />
|
||||
<FriendLinksSlot />
|
||||
</div>
|
||||
</main>
|
||||
<FloatingActions />
|
||||
|
||||
+13
-13
@@ -625,21 +625,21 @@ export type AnnouncementItem = {
|
||||
};
|
||||
|
||||
/** GET /api/app/home/base → AdvitiseData(广告位列表) */
|
||||
/**
|
||||
* home/base → advitise:网页只使用后台「站点」的 5 个广告位
|
||||
* (接口还会返回 App 用的其它广告位,网页不读取)。
|
||||
*/
|
||||
export type AdvitiseData = {
|
||||
homeList?: AdvertiseInfo[];
|
||||
detailTopList?: AdvertiseInfo[];
|
||||
nineGridList?: AdvertiseInfo[];
|
||||
detailbannerList?: AdvertiseInfo[];
|
||||
launchScreenList?: AdvertiseInfo[];
|
||||
floatingList?: AdvertiseInfo[];
|
||||
AList?: AdvertiseInfo[];
|
||||
BList?: AdvertiseInfo[];
|
||||
/** 位置 1「弹窗」,素材 600×800 */
|
||||
dialogList?: AdvertiseInfo[];
|
||||
postList?: AdvertiseInfo[];
|
||||
mustAppList?: AdvertiseInfo[];
|
||||
titleAppList?: AdvertiseInfo[];
|
||||
friendList?: AdvertiseInfo[];
|
||||
videoList?: AdvertiseInfo[];
|
||||
/** 位置 2「9宫格」,小方块 */
|
||||
nineGridList?: AdvertiseInfo[];
|
||||
/** 位置 3「详情横幅」,素材 720×150 */
|
||||
detailbannerList?: AdvertiseInfo[];
|
||||
/** 位置 12「播放页小图标」,小方块 */
|
||||
AList?: AdvertiseInfo[];
|
||||
/** 位置 13「首页小图标」,小方块 */
|
||||
BList?: AdvertiseInfo[];
|
||||
};
|
||||
|
||||
/** GET /api/app/home/base → AnnouncementInfo(回家的路 / 公告地址) */
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 全站弹窗共用的页面滚动锁(引用计数)。
|
||||
* 各弹窗不再各自记录 / 恢复 body.overflow:多个弹窗同时打开、以任意顺序关闭时,
|
||||
* 只有最后一个关闭才恢复滚动,避免「后关的弹窗把 hidden 当成原值恢复」导致全站无法滚动。
|
||||
*/
|
||||
let lockCount = 0;
|
||||
let previousOverflow = "";
|
||||
|
||||
export function lockBodyScroll(): () => void {
|
||||
if (typeof document === "undefined") return () => undefined;
|
||||
if (lockCount === 0) {
|
||||
previousOverflow = document.body.style.overflow === "hidden" ? "" : document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
lockCount += 1;
|
||||
|
||||
let released = false;
|
||||
return () => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
lockCount = Math.max(0, lockCount - 1);
|
||||
if (lockCount === 0) document.body.style.overflow = previousOverflow;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user