福利应用删除

This commit is contained in:
谢宇宁
2026-10-26 20:31:41 +07:00
parent 6c66b792aa
commit d2c8c63a36
13 changed files with 27 additions and 442 deletions
-58
View File
@@ -1,58 +0,0 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { PageLoading } from "@/components/common/page-loading";
import { JsonLd } from "@/components/seo/json-ld";
import { WelfarePage } from "@/components/welfare/welfare-page";
import { buildWelfareCategories, type WelfareCategory } from "@/components/welfare/data";
import { getAppListCached } from "@/lib/api/applist";
import { buildWebPageJSONLD, getHomeBaseSEOCached, resolvePageMetadata } from "@/lib/seo";
export async function generateMetadata(): Promise<Metadata> {
return resolvePageMetadata({
seoInfo: {
title: "福利应用",
description: "黄果黑料福利应用与精选推荐。",
keyworld: ["黄果黑料", "福利应用"],
},
canonical: "/welfare",
});
}
async function WelfareContent() {
let categories: WelfareCategory[] = [];
try {
const data = await getAppListCached(0);
categories = buildWelfareCategories(data);
} catch (error) {
console.error("[welfare]", error);
}
const siteSeo = await getHomeBaseSEOCached();
return (
<>
<JsonLd
data={buildWebPageJSONLD({
path: "/welfare",
title: "福利应用",
description: "黄果黑料福利应用与精选推荐。",
type: "CollectionPage",
breadcrumbs: [
["首页", "/"],
["福利应用", "/welfare"],
],
siteName: siteSeo.siteName,
})}
/>
<WelfarePage categories={categories} />
</>
);
}
export default function Page() {
return (
<Suspense fallback={<PageLoading />}>
<WelfareContent />
</Suspense>
);
}
+6 -1
View File
@@ -1,5 +1,7 @@
import type { AppTranslationInfo, TranslationInfo } from "@/lib/api/types";
import { isRemovedNavItem } from "./nav-href";
/** 接口失败时兜底:PC「关于平台」(与 home/base → bottonTranslations 对齐) */
export const BOTTOM_NAV_FALLBACK_NAMES = ["用户条款", "隐私政策", "联系我们", "常见问题"] as const;
@@ -19,5 +21,8 @@ export function footerNavItems(bottonTranslations?: AppTranslationInfo | null):
const source = bottomCats.length ? bottomCats : categories;
return source
.flatMap((category) => category.translationList ?? [])
.filter((item): item is TranslationInfo => Boolean(item.name?.trim()) && !isCustomerServiceName(item.name));
.filter(
(item): item is TranslationInfo =>
Boolean(item.name?.trim()) && !isCustomerServiceName(item.name) && !isRemovedNavItem(item),
);
}
+14 -1
View File
@@ -57,7 +57,6 @@ export function resolveNavHref(urlValue?: string | null): string {
const path = raw.startsWith("/") ? raw : `/${raw}`;
if (path === "/" || path === "/#top") return "/";
if (path === "/application" || path.startsWith("/application/")) return "/welfare";
if (path === "/drama-rank") return "/ranking/drama";
if (path === "/video-rank") return "/ranking/video";
if (path.startsWith("/drama/")) return `/short-drama${path.slice("/drama".length)}`;
@@ -66,6 +65,20 @@ export function resolveNavHref(urlValue?: string | null): string {
return path;
}
/** 已下线的福利应用入口(接口仍可能下发 /application 或「福利应用」):导航中不展示 */
export function isRemovedNavItem(item: { name?: string | null; urlValue?: string | null }) {
const name = item.name?.trim() || "";
const url = (item.urlValue || "").trim().replace(/^https?:\/\/[^/]+/i, "");
const path = url.startsWith("/") ? url : `/${url}`;
return (
name.includes("福利") ||
path === "/application" ||
path.startsWith("/application/") ||
path === "/welfare" ||
path.startsWith("/welfare/")
);
}
/** 底部导航 / 页脚:优先接口 urlValue,无配置时再按 name 兜底 */
export function resolveAboutPlatformHref(item: { name?: string; urlValue?: string | null }): string {
const name = item.name?.trim();
+7 -1
View File
@@ -5,6 +5,7 @@ import { getNavigationHeaderCached } from "@/lib/api/navigation";
import type { AppTranslationInfo, ConfigInfo, TranslationCategory } from "@/lib/api/types";
import { DesktopNav } from "./desktop-nav";
import { isRemovedNavItem } from "./nav-href";
import { HideOnChromeless, HideOnMobileChromeless } from "./route-visibility";
import { SiteFooter } from "./site-footer";
import { SiteHeader } from "./site-header";
@@ -13,7 +14,12 @@ import { SiteHeader } from "./site-header";
async function loadNavigationCategories(): Promise<TranslationCategory[]> {
try {
const data = await getNavigationHeaderCached();
return data.categoryList?.filter((item) => item.name?.trim()) ?? [];
return (data.categoryList ?? [])
.filter((item) => item.name?.trim() && !isRemovedNavItem(item))
.map((item) => ({
...item,
translationList: item.translationList?.filter((child) => !isRemovedNavItem(child)) ?? item.translationList,
}));
} catch {
return [];
}
-1
View File
@@ -57,7 +57,6 @@ function buildShortcuts(config?: ConfigInfo | null): Shortcut[] {
config?.appAddr?.trim()
? { label: "APP", icon: "/logo2.png", href: hrefOf(config.appAddr), external: true }
: { label: "APP", icon: "/logo2.png" },
{ label: "福利应用", icon: "/images/nav_flyy.svg", href: "/welfare" },
];
}
-64
View File
@@ -1,64 +0,0 @@
import type { AppCategoryInfo, AppListResp } from "@/lib/api/types";
export type WelfareApp = {
id: string;
title: string;
subtitle: string;
icon: string;
href: string;
};
export type WelfareCategory = {
id: string;
title: string;
description: string;
apps: WelfareApp[];
};
/** PC 默认展示 12,移动端 8 */
export const WELFARE_DESKTOP_COLLAPSE_LIMIT = 12;
export const WELFARE_MOBILE_COLLAPSE_LIMIT = 8;
/**
* 按接口返回的 categoryList / appList 顺序组装,不做二次排序。
* 规则对齐 jha-1043 applistser.BuildPageVM。
*/
export function buildWelfareCategories(data: AppListResp | null | undefined): WelfareCategory[] {
if (!data) return [];
let list = data.categoryList ?? [];
if (!list.length && data.category?.id) {
list = [data.category];
}
if (!list.length) return [];
return list.map((cat) => mapCategory(cat));
}
function mapCategory(cat: AppCategoryInfo): WelfareCategory {
const apps: WelfareApp[] = [];
for (const app of cat.appList ?? []) {
const name = (app.name || "").trim();
if (!name) continue;
const href = (app.url || "").trim() || "#";
const intro = (app.intro || "").trim() || (app.description || "").trim();
apps.push({
id: String(app.id),
title: name,
subtitle: intro,
icon: (app.logo || "").trim(),
href,
});
}
const title = (cat.title || "").trim() || "应用分类";
return {
id: `app-category-${cat.id}`,
title,
description: (cat.intro || "").trim(),
apps,
};
}
@@ -1,132 +0,0 @@
"use client";
import { useState } from "react";
import { DecryptedImage } from "@/components/common/decrypted-image";
import { IMG_PLACEHOLDER } from "@/lib/image/decrypt";
import {
WELFARE_DESKTOP_COLLAPSE_LIMIT,
WELFARE_MOBILE_COLLAPSE_LIMIT,
type WelfareCategory,
} from "./data";
type Variant = "pc" | "h5";
const variants = {
pc: {
rowSize: WELFARE_DESKTOP_COLLAPSE_LIMIT,
section: "scroll-mt-24 gap-6",
heading: "gap-3",
title: "text-4xl leading-[50px] font-normal text-white/90",
description: "text-lg leading-6 text-white/55",
descriptionFirst: "font-semibold text-white/70",
grid: "grid-cols-6 gap-x-4 gap-y-6 xl:grid-cols-12",
icon: "aspect-square w-full max-w-[120px] rounded-[18px]",
appTitle: "text-base leading-[22px] font-medium text-white/90",
appSubtitle: "text-xs leading-[18px] text-white/55",
tile: "gap-2",
button: "h-[50px] rounded-full px-6 text-lg leading-6",
showExpandIcon: true,
},
h5: {
rowSize: WELFARE_MOBILE_COLLAPSE_LIMIT,
section: "scroll-mt-16 gap-3",
heading: "w-full gap-3",
title: "text-lg leading-6 font-semibold text-white/90",
description: "text-sm leading-5 font-normal text-white/55",
descriptionFirst: "",
grid: "grid-cols-[repeat(4,77px)] justify-between gap-y-3",
icon: "size-[77px] overflow-hidden rounded-[18px]",
appTitle: "text-sm leading-5 font-medium text-white",
appSubtitle: "text-xs leading-[18px] font-normal text-white",
tile: "w-[77px] gap-1 rounded-xl",
button: "rounded-[25px] px-[18px] py-2.5 text-xs leading-[18px]",
showExpandIcon: false,
},
} as const;
export function WelfareCategorySection({
category,
variant,
isFirst,
}: {
category: WelfareCategory;
variant: Variant;
isFirst: boolean;
}) {
const styles = variants[variant];
const [expanded, setExpanded] = useState(false);
const limit = styles.rowSize;
const total = category.apps.length;
const expandable = total > limit;
const apps = expandable && !expanded ? category.apps.slice(0, limit) : category.apps;
return (
<section
id={variant === "h5" ? `h5-${category.id}` : category.id}
className={`flex w-full flex-col items-center ${styles.section}`}
>
<div className={`flex flex-col items-center text-center ${styles.heading}`}>
<h2 className={styles.title}>{category.title}</h2>
{category.description ? (
<p
className={`w-full whitespace-pre-line ${styles.description} ${
isFirst ? styles.descriptionFirst : ""
}`}
>
{category.description}
</p>
) : null}
</div>
{apps.length ? (
<div className={`grid w-full ${styles.grid}`}>
{apps.map((app) => (
<a
key={app.id}
href={app.href}
target="_blank"
rel="sponsored nofollow noopener noreferrer"
aria-label={app.title}
className={`flex min-w-0 flex-col items-center text-center ${styles.tile}`}
>
<DecryptedImage
src={app.icon || null}
fallback={IMG_PLACEHOLDER}
alt={app.title}
title={app.title}
className={`object-cover ${styles.icon}`}
/>
<span className={`w-full truncate ${styles.appTitle}`}>{app.title}</span>
{app.subtitle ? (
<span className={`w-full truncate ${styles.appSubtitle}`}>{app.subtitle}</span>
) : null}
</a>
))}
</div>
) : (
<p className="text-sm leading-5 text-white/45">该分类暂无应用</p>
)}
{expandable ? (
<button
type="button"
aria-expanded={expanded}
onClick={() => setExpanded((value) => !value)}
className={`cursor-pointer inline-flex items-center justify-center gap-2.5 bg-[#F12B9E] text-white ${styles.button}`}
>
<span>{expanded ? "收起" : `显示全部${total}个站点`}</span>
{styles.showExpandIcon ? (
<img
src="/images/trangle_down.svg"
alt=""
aria-hidden="true"
className={`size-3 shrink-0 transition-transform ${expanded ? "rotate-180" : ""}`}
/>
) : null}
</button>
) : null}
</section>
);
}
-56
View File
@@ -1,56 +0,0 @@
import { AdBannersSlot, AdIconsSlot, FriendLinksSlot, H5FriendLinksSlot } from "@/components/common/ads/slots";
import { FloatingActions } from "@/components/home/floating-actions";
import { DownloadBanner } from "@/components/home/page-sections";
import { SeoBreadcrumb } from "@/components/seo/seo-breadcrumb";
import type { WelfareCategory } from "./data";
import { WelfareCategorySection } from "./welfare-category-section";
import { WelfareCategoryTabs } from "./welfare-tabs";
export function WelfarePage({ categories }: { categories: WelfareCategory[] }) {
if (!categories.length) {
return (
<div data-component="welfare-page" className="flex min-h-[200px] flex-col items-center justify-center gap-2 text-sm leading-5 text-white/45">
<SeoBreadcrumb items={[{ name: "首页", href: "/" }, { name: "福利应用" }]} />
<h1 className="text-base font-semibold text-white/70">福利应用</h1>
<p>暂无福利应用</p>
</div>
);
}
return (
<>
<SeoBreadcrumb items={[{ name: "首页", href: "/" }, { name: "福利应用" }]} />
<h1 className="sr-only">福利应用</h1>
<main
data-component="welfare-page"
className="flex w-full min-w-0 flex-col items-center gap-[18px] overflow-x-clip bg-[#07080B] px-4 pt-3 pb-[18px] lg:hidden"
>
<WelfareCategoryTabs variant="h5" categories={categories} />
{categories.map((category, index) => (
<WelfareCategorySection key={category.id} category={category} variant="h5" isFirst={index === 0} />
))}
<AdIconsSlot variant="h5" />
<AdBannersSlot variant="h5" />
<H5FriendLinksSlot />
</main>
<main className="hidden lg:block">
<div className="mx-auto flex w-[calc(100%-64px)] max-w-[1700px] flex-col gap-5 pt-10 pb-9">
<div className="flex flex-col items-center gap-9">
<WelfareCategoryTabs variant="pc" categories={categories} />
{categories.map((category, index) => (
<WelfareCategorySection key={category.id} category={category} variant="pc" isFirst={index === 0} />
))}
</div>
<AdIconsSlot variant="pc" />
<AdBannersSlot variant="pc" />
<DownloadBanner />
<FriendLinksSlot />
</div>
</main>
<FloatingActions />
</>
);
}
-66
View File
@@ -1,66 +0,0 @@
"use client";
import { useState } from "react";
import type { WelfareCategory } from "./data";
type Variant = "pc" | "h5";
const tabStyles = {
pc: {
tabs: "flex w-full flex-wrap items-center justify-center gap-[18px]",
tab: "min-h-[50px] max-w-full px-5 py-2 text-lg leading-6 whitespace-normal",
tabActive: "bg-[#F12B9E] text-white",
tabIdle: "bg-black/5 text-[#999999] outline outline-black/10 -outline-offset-1 hover:text-white",
},
h5: {
tabs: "flex w-full gap-1.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
tab: "shrink-0 rounded-[25px] px-3 py-1.5 text-xs leading-[18px]",
tabActive: "bg-[#F12B9E] text-white",
tabIdle:
"bg-white/5 text-white/55 outline outline-1 -outline-offset-1 outline-white/10",
},
} as const;
export function WelfareCategoryTabs({
variant,
categories,
}: {
variant: Variant;
categories: WelfareCategory[];
}) {
const [activeId, setActiveId] = useState(categories[0]?.id ?? "");
const styles = tabStyles[variant];
const validActive =
categories.some((item) => item.id === activeId) ? activeId : (categories[0]?.id ?? "");
if (validActive !== activeId) {
setActiveId(validActive);
}
const jumpTo = (id: string) => {
setActiveId(id);
const prefix = variant === "h5" ? "h5-" : "";
document.getElementById(`${prefix}${id}`)?.scrollIntoView({ behavior: "smooth" });
};
return (
<div className={styles.tabs}>
{categories.map((category) => {
const active = category.id === validActive;
return (
<button
key={category.id}
type="button"
aria-pressed={active}
onClick={() => jumpTo(category.id)}
className={`cursor-pointer text-center break-words transition-colors ${variant === "pc" ? "rounded-full shrink-0" : ""} ${styles.tab} ${
active ? styles.tabActive : styles.tabIdle
}`}
>
{category.title}
</button>
);
})}
</div>
);
}
-22
View File
@@ -1,22 +0,0 @@
import { cacheLife, cacheTag } from "next/cache";
import { apiRequest } from "./client";
import { serverFetchJson } from "./server-fetch";
import type { AppListResp } from "./types";
/** categoryId=0 表示默认(返回全部分类列表);浏览器端可用 */
export function fetchAppList(categoryId = 0, token?: string | null) {
return apiRequest<AppListResp>(`/api/app/applist/list?categoryId=${categoryId}`, {
method: "GET",
token,
});
}
/** 服务端渲染拉取应用列表(带缓存) */
export async function getAppListCached(categoryId = 0): Promise<AppListResp> {
"use cache";
cacheLife("hours");
cacheTag("applist");
return serverFetchJson<AppListResp>(`/api/app/applist/list?categoryId=${categoryId}`);
}
-34
View File
@@ -48,40 +48,6 @@ export type UpdateProfileReq = {
newPassword?: string;
};
export type ImageUploadData = {
id?: string;
sourceNameFs?: string;
url?: string;
};
/** GET /api/app/applist/list */
export type AppInfoData = {
id: number;
categoryId?: number;
name: string;
intro?: string;
description?: string;
logo?: string;
url?: string;
sort?: number;
advitiseId?: string;
location?: number;
};
export type AppCategoryInfo = {
id: number;
code?: number;
title: string;
intro?: string;
sort?: number;
appList?: AppInfoData[];
};
export type AppListResp = {
category?: AppCategoryInfo;
categoryList?: AppCategoryInfo[];
};
/** GET /api/app/playlet/content/list */
export type PlayletData = {
id: number;