福利应用删除
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user