From 654257a2d2de2ae8c62b6453928a3672d1dade57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E5=AE=87=E5=AE=81?= Date: Wed, 28 Oct 2026 17:07:32 +0700 Subject: [PATCH] =?UTF-8?q?=E5=B9=BF=E5=91=8A=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../short-drama/[sort]/[[...page]]/page.tsx | 10 +- src/app/video/[sort]/[[...page]]/page.tsx | 6 +- src/components/ad/ad-dialog.tsx | 141 +++++++++++++++--- src/components/address/address-dialog.tsx | 6 +- src/components/age-gate/age-gate-dialog.tsx | 6 +- src/components/auth/login-dialog.tsx | 6 +- src/components/common/ad-icon-marquee.tsx | 27 +++- src/components/common/ad-icon-row.tsx | 18 --- src/components/common/ads/ad-banners.tsx | 6 +- src/components/common/ads/ad-icons.tsx | 70 --------- src/components/common/ads/ad-squares.tsx | 67 +++++++++ src/components/common/ads/index.ts | 4 +- src/components/common/ads/pick.ts | 24 ++- src/components/common/ads/slots.tsx | 26 +--- src/components/common/friend-links-slot.tsx | 1 - src/components/common/friend-links.tsx | 75 ---------- src/components/common/h5-ad-grid-slot.tsx | 6 - src/components/common/h5-ad-grid.tsx | 16 -- src/components/common/h5-banner-ads-slot.tsx | 6 - src/components/common/h5-banner-ads.tsx | 16 -- src/components/download/download-dialog.tsx | 6 +- .../drama-detail/drama-detail-page.tsx | 10 +- src/components/drama-play/drama-play-page.tsx | 17 ++- src/components/drama-play/h5-play.tsx | 10 ++ src/components/home/drama-card.tsx | 14 -- src/components/home/home-ad-row.tsx | 14 -- src/components/home/home-page-h5.tsx | 8 +- src/components/home/home-page.tsx | 6 +- src/components/layout/mobile-menu.tsx | 6 +- src/components/more/more-page.tsx | 7 - src/components/ranking/data.ts | 14 +- src/components/ranking/ranking-page.tsx | 7 - src/components/search/search-page.tsx | 9 -- src/components/short-drama/data.ts | 83 +++-------- .../short-drama/short-drama-card.tsx | 23 +-- .../short-drama/short-drama-page.tsx | 7 - src/components/tag/tag-page.tsx | 9 -- src/components/topic/topic-page.tsx | 7 - src/components/video-play/h5-video-play.tsx | 9 +- src/components/video-play/video-play-page.tsx | 16 +- src/components/video/data.ts | 40 +---- src/components/video/video-card.tsx | 23 +-- src/components/video/video-page.tsx | 7 - src/lib/api/types.ts | 26 ++-- src/lib/browser/scroll-lock.ts | 24 +++ 45 files changed, 363 insertions(+), 576 deletions(-) delete mode 100644 src/components/common/ad-icon-row.tsx delete mode 100644 src/components/common/ads/ad-icons.tsx create mode 100644 src/components/common/ads/ad-squares.tsx delete mode 100644 src/components/common/friend-links-slot.tsx delete mode 100644 src/components/common/friend-links.tsx delete mode 100644 src/components/common/h5-ad-grid-slot.tsx delete mode 100644 src/components/common/h5-ad-grid.tsx delete mode 100644 src/components/common/h5-banner-ads-slot.tsx delete mode 100644 src/components/common/h5-banner-ads.tsx delete mode 100644 src/components/home/home-ad-row.tsx create mode 100644 src/lib/browser/scroll-lock.ts diff --git a/src/app/short-drama/[sort]/[[...page]]/page.tsx b/src/app/short-drama/[sort]/[[...page]]/page.tsx index ab4973f..39ea739 100644 --- a/src/app/short-drama/[sort]/[[...page]]/page.tsx +++ b/src/app/short-drama/[sort]/[[...page]]/page.tsx @@ -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 }) { } + adIcons={} promo={} - adGrid={} + adGrid={} bannerAds={} /> @@ -428,8 +428,10 @@ async function ShortDramaRouteContent({ params }: { params: Params }) { episode={play.episode} src={play.src} related={play.related} - adIcons={} promo={} + topAds={} + h5Ads={} + h5Banners={} /> ); diff --git a/src/app/video/[sort]/[[...page]]/page.tsx b/src/app/video/[sort]/[[...page]]/page.tsx index 12fc372..962c606 100644 --- a/src/app/video/[sort]/[[...page]]/page.tsx +++ b/src/app/video/[sort]/[[...page]]/page.tsx @@ -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={} promo={} - adGrid={} + topAds={} + h5Ads={} bannerAds={} /> diff --git a/src/components/ad/ad-dialog.tsx b/src/components/ad/ad-dialog.tsx index d2976b8..9d6f5fb 100644 --- a/src/components/ad/ad-dialog.tsx +++ b/src/components/ad/ad-dialog.tsx @@ -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 ? : null; + } const picture = ( ); @@ -251,3 +306,53 @@ export function AdDialogHost({ ); } + +/** 九宫格弹窗:4 列应用图标宫格 + 底部关闭按钮;点任一应用或空白处即关闭 */ +function NineGridDialog({ ads, onClose }: { ads: Array; onClose: () => void }) { + return ( +
event.target === event.currentTarget && onClose()} + > +
event.target === event.currentTarget && onClose()} + > + + +
+
+ ); +} diff --git a/src/components/address/address-dialog.tsx b/src/components/address/address-dialog.tsx index 9351928..91a5787 100644 --- a/src/components/address/address-dialog.tsx +++ b/src/components/address/address-dialog.tsx @@ -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]); diff --git a/src/components/age-gate/age-gate-dialog.tsx b/src/components/age-gate/age-gate-dialog.tsx index f7f60d9..f280436 100644 --- a/src/components/age-gate/age-gate-dialog.tsx +++ b/src/components/age-gate/age-gate-dialog.tsx @@ -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]); diff --git a/src/components/auth/login-dialog.tsx b/src/components/auth/login-dialog.tsx index 5f68462..eb1cd93 100644 --- a/src/components/auth/login-dialog.tsx +++ b/src/components/auth/login-dialog.tsx @@ -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]); diff --git a/src/components/common/ad-icon-marquee.tsx b/src/components/common/ad-icon-marquee.tsx index f3a50a5..af95046 100644 --- a/src/components/common/ad-icon-marquee.tsx +++ b/src/components/common/ad-icon-marquee.tsx @@ -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(null); const segmentRef = useRef(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 (
{children}
-
- {children} - -
+ {overflowing ? ( +
+ {children} + +
+ ) : null}
); diff --git a/src/components/common/ad-icon-row.tsx b/src/components/common/ad-icon-row.tsx deleted file mode 100644 index 6ebd036..0000000 --- a/src/components/common/ad-icon-row.tsx +++ /dev/null @@ -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 ; -} diff --git a/src/components/common/ads/ad-banners.tsx b/src/components/common/ads/ad-banners.tsx index e9debcd..c1dbeed 100644 --- a/src/components/common/ads/ad-banners.tsx +++ b/src/components/common/ads/ad-banners.tsx @@ -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" > - {apiList.map((ad, index) => { - const href = adHref(ad.link, isH5 ? "#h5-download" : "#download"); - const external = Boolean(ad.link?.trim()); - return ( - - - {showLabels ? {ad.title || "广告"} : null} - - ); - })} - - ); -} diff --git a/src/components/common/ads/ad-squares.tsx b/src/components/common/ads/ad-squares.tsx new file mode 100644 index 0000000..d2622a7 --- /dev/null +++ b/src/components/common/ads/ad-squares.tsx @@ -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 ( +
+

推广

+ + {list.map((ad, index) => { + const href = adHref(ad.link, isH5 ? "#h5-download" : "#download"); + const external = Boolean(ad.link?.trim()); + return ( + + + + {ad.title || "推广"} + + + ); + })} + +
+ ); +} diff --git a/src/components/common/ads/index.ts b/src/components/common/ads/index.ts index 401ff55..36727eb 100644 --- a/src/components/common/ads/index.ts +++ b/src/components/common/ads/index.ts @@ -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"; diff --git a/src/components/common/ads/pick.ts b/src/components/common/ads/pick.ts index 0aab501..05150d6 100644 --- a/src/components/common/ads/pick.ts +++ b/src/components/common/ads/pick.ts @@ -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; diff --git a/src/components/common/ads/slots.tsx b/src/components/common/ads/slots.tsx index 798d161..f63922c 100644 --- a/src/components/common/ads/slots.tsx +++ b/src/components/common/ads/slots.tsx @@ -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 ; + return ; } /** 服务端:Banner 广告 */ @@ -29,15 +29,3 @@ export async function AdBannersSlot({ const advitise = await getAdvitiseCached(); return ; } - -/** 服务端:PC 友情链接 */ -export async function FriendLinksSlot() { - const advitise = await getAdvitiseCached(); - return ; -} - -/** 服务端:H5 友情链接 */ -export async function H5FriendLinksSlot() { - const advitise = await getAdvitiseCached(); - return ; -} diff --git a/src/components/common/friend-links-slot.tsx b/src/components/common/friend-links-slot.tsx deleted file mode 100644 index cda692f..0000000 --- a/src/components/common/friend-links-slot.tsx +++ /dev/null @@ -1 +0,0 @@ -export { FriendLinksSlot, H5FriendLinksSlot } from "./ads/slots"; diff --git a/src/components/common/friend-links.tsx b/src/components/common/friend-links.tsx deleted file mode 100644 index 50ce2ff..0000000 --- a/src/components/common/friend-links.tsx +++ /dev/null @@ -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 ( - - ); -} - -/** 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 ( - - ); -} diff --git a/src/components/common/h5-ad-grid-slot.tsx b/src/components/common/h5-ad-grid-slot.tsx deleted file mode 100644 index b709408..0000000 --- a/src/components/common/h5-ad-grid-slot.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { AdIconsSlot } from "./ads/slots"; - -/** @deprecated 使用 AdIconsSlot */ -export async function H5AdGridSlot({ className = "" }: { className?: string }) { - return ; -} diff --git a/src/components/common/h5-ad-grid.tsx b/src/components/common/h5-ad-grid.tsx deleted file mode 100644 index 7d5968f..0000000 --- a/src/components/common/h5-ad-grid.tsx +++ /dev/null @@ -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 ; -} diff --git a/src/components/common/h5-banner-ads-slot.tsx b/src/components/common/h5-banner-ads-slot.tsx deleted file mode 100644 index 9137fbe..0000000 --- a/src/components/common/h5-banner-ads-slot.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { AdBannersSlot } from "./ads/slots"; - -/** @deprecated 使用 AdBannersSlot */ -export async function H5BannerAdsSlot({ className = "" }: { className?: string }) { - return ; -} diff --git a/src/components/common/h5-banner-ads.tsx b/src/components/common/h5-banner-ads.tsx deleted file mode 100644 index 9df61a6..0000000 --- a/src/components/common/h5-banner-ads.tsx +++ /dev/null @@ -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 ; -} diff --git a/src/components/download/download-dialog.tsx b/src/components/download/download-dialog.tsx index 6b2afa1..3c1537a 100644 --- a/src/components/download/download-dialog.tsx +++ b/src/components/download/download-dialog.tsx @@ -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]); diff --git a/src/components/drama-detail/drama-detail-page.tsx b/src/components/drama-detail/drama-detail-page.tsx index 66712ff..847e3e8 100644 --- a/src/components/drama-detail/drama-detail-page.tsx +++ b/src/components/drama-detail/drama-detail-page.tsx @@ -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({ - {adIcons ?? } + {adIcons} @@ -298,7 +298,7 @@ export function DramaDetailPage({ ) : null} - {adGrid ?? } + {adGrid} diff --git a/src/components/drama-play/drama-play-page.tsx b/src/components/drama-play/drama-play-page.tsx index 953ef6b..a269e11 100644 --- a/src/components/drama-play/drama-play-page.tsx +++ b/src/components/drama-play/drama-play-page.tsx @@ -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 ? : null} + {!expanded ? topAds : null}
@@ -294,7 +302,6 @@ export function DramaPlayPage({ {expanded ? : null} - {adIcons ?? } @@ -317,6 +324,8 @@ export function DramaPlayPage({ onLike={toggleLike} onCollect={toggleCollect} onShare={share} + ads={h5Ads} + bannerAds={h5Banners} />
diff --git a/src/components/drama-play/h5-play.tsx b/src/components/drama-play/h5-play.tsx index fd5a058..3a4de68 100644 --- a/src/components/drama-play/h5-play.tsx +++ b/src/components/drama-play/h5-play.tsx @@ -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(null); @@ -316,9 +322,13 @@ export function H5Play({ + {ads} + + + {bannerAds}
diff --git a/src/components/home/drama-card.tsx b/src/components/home/drama-card.tsx index 3a86a57..aa1aa6a 100644 --- a/src/components/home/drama-card.tsx +++ b/src/components/home/drama-card.tsx @@ -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 ); } - -/** @deprecated 使用 AdBanners;covers 保留兼容,有接口数据时优先接口 */ -export function PromoRow({ - items, - advitise, -}: { - covers?: readonly string[]; - items?: AdvertiseInfo[] | null; - advitise?: AdvitiseData | null; -}) { - return ; -} diff --git a/src/components/home/home-ad-row.tsx b/src/components/home/home-ad-row.tsx deleted file mode 100644 index 546baa5..0000000 --- a/src/components/home/home-ad-row.tsx +++ /dev/null @@ -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 ; -} diff --git a/src/components/home/home-page-h5.tsx b/src/components/home/home-page-h5.tsx index a26f7b1..33f5103 100644 --- a/src/components/home/home-page-h5.tsx +++ b/src/components/home/home-page-h5.tsx @@ -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 }) { - - + {hasModules ? : null} - - ); diff --git a/src/components/home/home-page.tsx b/src/components/home/home-page.tsx index 697624c..1c3cb57 100644 --- a/src/components/home/home-page.tsx +++ b/src/components/home/home-page.tsx @@ -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({
- + {hasModules ? : null} -
diff --git a/src/components/layout/mobile-menu.tsx b/src/components/layout/mobile-menu.tsx index dd97ff5..e0d0d4a 100644 --- a/src/components/layout/mobile-menu.tsx +++ b/src/components/layout/mobile-menu.tsx @@ -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]); diff --git a/src/components/more/more-page.tsx b/src/components/more/more-page.tsx index 196f3d6..8a0b22c 100644 --- a/src/components/more/more-page.tsx +++ b/src/components/more/more-page.tsx @@ -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 }) {
- @@ -80,14 +78,11 @@ export function MorePage({ data }: { data: MorePageData }) { {hrefMap ? : null} - -
- @@ -106,9 +101,7 @@ export function MorePage({ data }: { data: MorePageData }) {
) : null}
- -
diff --git a/src/components/ranking/data.ts b/src/components/ranking/data.ts index 17e4c18..2bfd8ef 100644 --- a/src/components/ranking/data.ts +++ b/src/components/ranking/data.ts @@ -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; diff --git a/src/components/ranking/ranking-page.tsx b/src/components/ranking/ranking-page.tsx index 9ac088d..c43baf5 100644 --- a/src/components/ranking/ranking-page.tsx +++ b/src/components/ranking/ranking-page.tsx @@ -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 />
-
@@ -94,14 +92,11 @@ export function RankingPage({ tab, list }: { tab: RankingTab; list: RankingListR )}
- -
-
@@ -126,9 +121,7 @@ export function RankingPage({ tab, list }: { tab: RankingTab; list: RankingListR
) : null}
- -
diff --git a/src/components/search/search-page.tsx b/src/components/search/search-page.tsx index 67ebb83..8764a7e 100644 --- a/src/components/search/search-page.tsx +++ b/src/components/search/search-page.tsx @@ -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 }) { /> )} -
- - -
-
- - -
diff --git a/src/components/short-drama/data.ts b/src/components/short-drama/data.ts index cfa89b6..0ca7f0b 100644 --- a/src/components/short-drama/data.ts +++ b/src/components/short-drama/data.ts @@ -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) { diff --git a/src/components/short-drama/short-drama-card.tsx b/src/components/short-drama/short-drama-card.tsx index ae1f5c1..e5498f0 100644 --- a/src/components/short-drama/short-drama-card.tsx +++ b/src/components/short-drama/short-drama-card.tsx @@ -16,7 +16,7 @@ function RankLabel({ rank, className = "" }: { rank?: number; className?: string } /** 短剧卡元信息:集数 + 热度(文字行,不叠在封面上) */ -function DramaMeta({ item, className }: { item: Extract; className: string }) { +function DramaMeta({ item, className }: { item: ShortDramaItem; className: string }) { return (

{item.episodes ? 共{item.episodes} : null} @@ -31,27 +31,7 @@ function DramaMeta({ item, className }: { item: Extract - - - - 推广 - - - {item.title} - - ); -} - export function ShortDramaCard({ item, rank }: { item: ShortDramaItem; rank?: number }) { - if (item.kind === "ad") return ; 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 ; const href = item.href || dramaHref(item.id, item.slug); return ( diff --git a/src/components/short-drama/short-drama-page.tsx b/src/components/short-drama/short-drama-page.tsx index 54f3f9f..455a09e 100644 --- a/src/components/short-drama/short-drama-page.tsx +++ b/src/components/short-drama/short-drama-page.tsx @@ -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 }) {

-
- -
-
) : null}
- -
diff --git a/src/components/tag/tag-page.tsx b/src/components/tag/tag-page.tsx index 7d43d6d..249d1b3 100644 --- a/src/components/tag/tag-page.tsx +++ b/src/components/tag/tag-page.tsx @@ -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 }) { ) : null} -
- - -
-
- - -
diff --git a/src/components/topic/topic-page.tsx b/src/components/topic/topic-page.tsx index b4a2f24..3d81ed3 100644 --- a/src/components/topic/topic-page.tsx +++ b/src/components/topic/topic-page.tsx @@ -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" > -
@@ -181,14 +179,11 @@ export function TopicPage({ data }: { data: TopicPageData }) { gridClassName={layout.morePcClassName} /> - -
-
@@ -204,9 +199,7 @@ export function TopicPage({ data }: { data: TopicPageData }) {
- -
diff --git a/src/components/video-play/h5-video-play.tsx b/src/components/video-play/h5-video-play.tsx index 9757a8d..206a24d 100644 --- a/src/components/video-play/h5-video-play.tsx +++ b/src/components/video-play/h5-video-play.tsx @@ -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} - {adGrid ?? } + {squareAds} diff --git a/src/components/video-play/video-play-page.tsx b/src/components/video-play/video-play-page.tsx index 1c40c05..1957579 100644 --- a/src/components/video-play/video-play-page.tsx +++ b/src/components/video-play/video-play-page.tsx @@ -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" > + {topAds} - {adIcons ?? } @@ -283,8 +285,8 @@ export function VideoPlayPage({ onLike={liking ? () => undefined : toggleLike} onCollect={collecting ? () => undefined : toggleCollect} onShare={share} - adGrid={adGrid} bannerAds={bannerAds} + squareAds={h5Ads} />
diff --git a/src/components/video/data.ts b/src/components/video/data.ts index 373f36d..05c4fa4 100644 --- a/src/components/video/data.ts +++ b/src/components/video/data.ts @@ -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, - }; - }); -} diff --git a/src/components/video/video-card.tsx b/src/components/video/video-card.tsx index bc7d9a4..3cfbd1b 100644 --- a/src/components/video/video-card.tsx +++ b/src/components/video/video-card.tsx @@ -26,7 +26,7 @@ function RankLabel({ rank, className = "" }: { rank?: number; className?: string } /** 视频卡元信息:观看数 + 发布日期 */ -function VideoMeta({ item, className }: { item: Extract; className: string }) { +function VideoMeta({ item, className }: { item: ShortDramaItem; className: string }) { const day = formatDay(item.date); return (

@@ -42,27 +42,7 @@ function VideoMeta({ item, className }: { item: Extract - - - - 推广 - - - {item.title} - - ); -} - export function VideoCard({ item, rank }: { item: ShortDramaItem; rank?: number }) { - if (item.kind === "ad") return ; 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 ; const href = item.href || videoHref(item.id, item.slug); return ( diff --git a/src/components/video/video-page.tsx b/src/components/video/video-page.tsx index 3c3902c..3d17b40 100644 --- a/src/components/video/video-page.tsx +++ b/src/components/video/video-page.tsx @@ -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 }) {

-
- -
-
) : null}
- -
diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts index c3abeb5..ce6c707 100644 --- a/src/lib/api/types.ts +++ b/src/lib/api/types.ts @@ -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(回家的路 / 公告地址) */ diff --git a/src/lib/browser/scroll-lock.ts b/src/lib/browser/scroll-lock.ts new file mode 100644 index 0000000..23f4ac5 --- /dev/null +++ b/src/lib/browser/scroll-lock.ts @@ -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; + }; +}