广告修改
This commit is contained in:
@@ -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} />;
|
||||
}
|
||||
Reference in New Issue
Block a user