广告修改

This commit is contained in:
谢宇宁
2026-10-28 17:07:32 +07:00
parent d5d28c92ce
commit 654257a2d2
45 changed files with 363 additions and 576 deletions
+19 -8
View File
@@ -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>
);
-18
View File
@@ -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} />;
}
+3 -3
View File
@@ -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}
-70
View File
@@ -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>
);
}
+67
View File
@@ -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>
);
}
+2 -2
View File
@@ -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";
+9 -15
View File
@@ -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;
+7 -19
View File
@@ -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";
-75
View File
@@ -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} />;
}
-16
View File
@@ -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} />;
}
-16
View File
@@ -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} />;
}