276 lines
12 KiB
TypeScript
276 lines
12 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useMemo, useEffect } from "react";
|
||
import { useTranslation } from "@/i18n/navigation";
|
||
import type { ResourceData } from "@/app/lib/theme-data";
|
||
|
||
const PER_PAGE_MOBILE = 4;
|
||
const PER_PAGE_PC = 8;
|
||
const BREAKPOINT = 768;
|
||
|
||
function useIsPC() {
|
||
const [isPC, setIsPC] = useState(false);
|
||
useEffect(() => {
|
||
const mq = window.matchMedia(`(min-width: ${BREAKPOINT}px)`);
|
||
setIsPC(mq.matches);
|
||
const fn = () => setIsPC(mq.matches);
|
||
mq.addEventListener("change", fn);
|
||
return () => mq.removeEventListener("change", fn);
|
||
}, []);
|
||
return isPC;
|
||
}
|
||
|
||
/** 每页条数:H5=4,PC=8;总数不足则全显 */
|
||
function getPerPage(total: number, isPC: boolean): number {
|
||
const cap = isPC ? PER_PAGE_PC : PER_PAGE_MOBILE;
|
||
return total <= cap ? total : cap;
|
||
}
|
||
|
||
type ResourceNavigationProps = {
|
||
data: ResourceData;
|
||
};
|
||
|
||
export default function ResourceNavigation({ data }: ResourceNavigationProps) {
|
||
const { t } = useTranslation("community");
|
||
const isPC = useIsPC();
|
||
const firstCategoryId = data.pan.categories[0]?.id ?? "";
|
||
const [activePanCategory, setActivePanCategory] = useState(firstCategoryId);
|
||
const [booksPage, setBooksPage] = useState(0);
|
||
const [productsPage, setProductsPage] = useState(0);
|
||
const [toast, setToast] = useState(false);
|
||
|
||
const showUpdating = () => {
|
||
setToast(true);
|
||
setTimeout(() => setToast(false), 2000);
|
||
};
|
||
|
||
const activeCategory = data.pan.categories.find((c) => c.id === activePanCategory);
|
||
|
||
const booksPerPage = getPerPage(data.books.length, isPC);
|
||
const productsPerPage = getPerPage(data.products.length, isPC);
|
||
const booksTotalPages = Math.ceil(data.books.length / booksPerPage) || 1;
|
||
const productsTotalPages = Math.ceil(data.products.length / productsPerPage) || 1;
|
||
|
||
useEffect(() => {
|
||
setBooksPage((p) => Math.max(0, Math.min(p, booksTotalPages - 1)));
|
||
}, [booksTotalPages]);
|
||
useEffect(() => {
|
||
setProductsPage((p) => Math.max(0, Math.min(p, productsTotalPages - 1)));
|
||
}, [productsTotalPages]);
|
||
|
||
const paginatedBooks = useMemo(() => {
|
||
const start = booksPage * booksPerPage;
|
||
return data.books.slice(start, start + booksPerPage);
|
||
}, [data.books, booksPage, booksPerPage]);
|
||
|
||
const paginatedProducts = useMemo(() => {
|
||
const start = productsPage * productsPerPage;
|
||
return data.products.slice(start, start + productsPerPage);
|
||
}, [data.products, productsPage, productsPerPage]);
|
||
|
||
return (
|
||
<div className="mt-12 space-y-16">
|
||
{/* 图书 - 封面+书名,数量少则大封面,多则分页 */}
|
||
<div>
|
||
<h3 className="mb-4 flex items-center gap-2 text-xl font-bold text-slate-900 dark:text-slate-100">
|
||
<span>📚</span>
|
||
{t("resourceSections.books")}
|
||
{data.books.length > 0 && (
|
||
<span className="text-sm font-normal text-slate-500 dark:text-slate-400">
|
||
({t("resourceSections.totalBooks").replace("{count}", String(data.books.length))})
|
||
</span>
|
||
)}
|
||
</h3>
|
||
<div
|
||
className="grid grid-cols-2 gap-3 gap-y-4 sm:gap-4 md:grid-cols-8"
|
||
>
|
||
{paginatedBooks.map((book, i) => (
|
||
<button
|
||
key={`book-${booksPage}-${i}`}
|
||
type="button"
|
||
onClick={showUpdating}
|
||
className="group flex cursor-not-allowed flex-col text-left"
|
||
>
|
||
<div
|
||
className="overflow-hidden rounded-lg border border-slate-200 bg-slate-100 shadow-sm transition-all group-hover:shadow-md dark:border-slate-700 dark:bg-slate-800"
|
||
style={{ aspectRatio: "2/3" }}
|
||
>
|
||
<img
|
||
src={book.cover}
|
||
alt={book.title}
|
||
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||
/>
|
||
</div>
|
||
<p className="mt-2 line-clamp-2 text-center text-xs font-medium text-slate-700 dark:text-slate-300 sm:text-sm">
|
||
{book.title}
|
||
</p>
|
||
</button>
|
||
))}
|
||
</div>
|
||
{booksTotalPages > 1 && (
|
||
<div className="mt-6 flex items-center justify-center gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setBooksPage((p) => Math.max(0, p - 1))}
|
||
disabled={booksPage === 0}
|
||
className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
|
||
aria-label="上一页"
|
||
>
|
||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||
</svg>
|
||
</button>
|
||
<span className="px-4 text-sm text-slate-600 dark:text-slate-400">
|
||
{booksPage + 1} / {booksTotalPages}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => setBooksPage((p) => Math.min(booksTotalPages - 1, p + 1))}
|
||
disabled={booksPage === booksTotalPages - 1}
|
||
className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
|
||
aria-label="下一页"
|
||
>
|
||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 商业数码 - 商品图+跳转电商 */}
|
||
<div>
|
||
<h3 className="mb-4 flex items-center gap-2 text-xl font-bold text-slate-900 dark:text-slate-100">
|
||
<span>🛒</span>
|
||
{t("resourceSections.products")}
|
||
{data.products.length > 0 && (
|
||
<span className="text-sm font-normal text-slate-500 dark:text-slate-400">
|
||
({t("resourceSections.totalProducts").replace("{count}", String(data.products.length))})
|
||
</span>
|
||
)}
|
||
</h3>
|
||
<div
|
||
className="grid grid-cols-2 gap-3 gap-y-4 sm:gap-4 md:grid-cols-8"
|
||
>
|
||
{paginatedProducts.map((product, i) => (
|
||
<button
|
||
key={`product-${productsPage}-${i}`}
|
||
type="button"
|
||
onClick={showUpdating}
|
||
className="group flex cursor-not-allowed flex-col text-left"
|
||
>
|
||
<div
|
||
className="overflow-hidden rounded-xl border border-slate-200 bg-white p-3 shadow-sm transition-all group-hover:shadow-md dark:border-slate-700 dark:bg-slate-900"
|
||
style={{ aspectRatio: "1/1" }}
|
||
>
|
||
<img
|
||
src={product.image}
|
||
alt={product.name}
|
||
className="h-full w-full object-contain transition-transform duration-300 group-hover:scale-105"
|
||
/>
|
||
</div>
|
||
<p className="mt-2 line-clamp-2 text-center text-xs font-medium text-slate-700 dark:text-slate-300 sm:text-sm">
|
||
{product.name}
|
||
</p>
|
||
</button>
|
||
))}
|
||
</div>
|
||
{productsTotalPages > 1 && (
|
||
<div className="mt-6 flex items-center justify-center gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setProductsPage((p) => Math.max(0, p - 1))}
|
||
disabled={productsPage === 0}
|
||
className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
|
||
aria-label="上一页"
|
||
>
|
||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||
</svg>
|
||
</button>
|
||
<span className="px-4 text-sm text-slate-600 dark:text-slate-400">
|
||
{productsPage + 1} / {productsTotalPages}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => setProductsPage((p) => Math.min(productsTotalPages - 1, p + 1))}
|
||
disabled={productsPage === productsTotalPages - 1}
|
||
className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
|
||
aria-label="下一页"
|
||
>
|
||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 网盘 - 分类 Tab */}
|
||
<div>
|
||
<h3 className="mb-4 flex items-center gap-2 text-xl font-bold text-slate-900 dark:text-slate-100">
|
||
<span>☁️</span>
|
||
{t("resourceSections.pan")}
|
||
</h3>
|
||
<div className="rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
|
||
<div className="flex flex-wrap gap-1 border-b border-slate-200 p-2 dark:border-slate-700">
|
||
{data.pan.categories.map((cat) => (
|
||
<button
|
||
key={cat.id}
|
||
type="button"
|
||
onClick={() => setActivePanCategory(cat.id)}
|
||
className={`rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
|
||
activePanCategory === cat.id
|
||
? "bg-sky-100 text-sky-700 dark:bg-sky-900/50 dark:text-sky-300"
|
||
: "text-slate-600 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800"
|
||
}`}
|
||
>
|
||
<span className="mr-1.5">{cat.icon}</span>
|
||
{cat.title}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="p-4">
|
||
{activeCategory && (
|
||
<div
|
||
className="grid gap-3"
|
||
style={{
|
||
gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 260px), 1fr))",
|
||
}}
|
||
>
|
||
{activeCategory.items.map((item, i) => (
|
||
<a
|
||
key={i}
|
||
href={item.href}
|
||
target="_blank"
|
||
rel="noopener noreferrer sponsored"
|
||
className="flex items-center gap-3 rounded-xl border border-slate-100 p-3 transition-colors hover:border-sky-200 hover:bg-sky-50/50 dark:border-slate-700 dark:hover:border-sky-800 dark:hover:bg-sky-900/20"
|
||
>
|
||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-slate-100 text-lg dark:bg-slate-800">
|
||
{activeCategory.icon}
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="font-medium text-slate-900 dark:text-slate-100">{item.name}</div>
|
||
<div className="text-xs text-slate-500 dark:text-slate-400">{item.desc}</div>
|
||
</div>
|
||
<span className="shrink-0 text-sm font-medium text-sky-600">→</span>
|
||
</a>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{toast && (
|
||
<div
|
||
className="fixed bottom-8 left-1/2 z-50 -translate-x-1/2 rounded-lg bg-slate-800 px-6 py-3 text-sm font-medium text-white shadow-lg dark:bg-slate-700"
|
||
role="alert"
|
||
>
|
||
{t("updating")}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|