福利应用删除

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
-3
View File
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.60471 11.5419C7.60471 13.3238 6.1602 14.7683 4.37829 14.7683C2.59638 14.7683 1.15186 13.3238 1.15186 11.5419C1.15186 9.75997 2.59638 8.31546 4.37829 8.31546H6.52923H7.60471V9.39094V11.5419ZM8.32171 11.5419C8.32171 13.3238 9.76622 14.7683 11.5481 14.7683C13.33 14.7683 14.7746 13.3238 14.7746 11.5419C14.7746 9.75997 13.33 8.31546 11.5481 8.31546H9.39719H8.32171V9.39094V11.5419ZM7.60471 4.37205C7.60471 2.59012 6.1602 1.14563 4.37829 1.14563C2.59638 1.14563 1.15186 2.59012 1.15186 4.37205C1.15186 6.15398 2.59638 7.59847 4.37829 7.59847H6.52923H7.60471V6.52299V4.37205ZM8.32171 4.37205C8.32171 2.59012 9.76622 1.14563 11.5481 1.14563C13.33 1.14563 14.7746 2.59014 14.7746 4.37205C14.7746 6.15396 13.33 7.59847 11.5481 7.59847H9.39719H8.32171V6.52299V4.37205Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 891 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="11" height="9" viewBox="0 0 11 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.19629 9L0.000136209 -2.51245e-08L10.3924 8.834e-07L5.19629 9Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 191 B

-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;