241 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState } from "react";
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination } from "swiper/modules";
import Link from "next/link";
import Image from "next/image";
export default function PageItems({ page_slug = "", page_size = 8, columns = 4 }) {
const [posts, setPosts] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [loading, setLoading] = useState(false);
// 静态映射,避免 Tailwind JIT 无法识别动态类名
const colClass = {
1: "lg:grid-cols-1",
2: "lg:grid-cols-2",
3: "lg:grid-cols-3",
4: "lg:grid-cols-4",
5: "lg:grid-cols-5",
6: "lg:grid-cols-6",
}[columns] || "lg:grid-cols-4";
useEffect(() => {
async function fetchData() {
setLoading(true);
try {
const res = await fetch("/api/get-page-data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug_list: [page_slug], page: currentPage, page_size }),
});
const json = await res.json();
if (Array.isArray(json.data)) {
setPosts(json.data);
setTotalPages(Math.ceil((json.total || 0) / page_size));
} else {
setPosts([]);
setTotalPages(1);
}
} catch (e) {
setPosts([]);
setTotalPages(1);
}
setLoading(false);
}
fetchData();
}, [currentPage, page_slug, page_size]);
// 分页控件
function PaginationComp() {
if (totalPages <= 1) return null;
return (
<nav className="flex justify-center mt-8" aria-label="pagination">
<ul className="pagination flex gap-2">
{Array.from({ length: totalPages }).map((_, idx) => (
<li key={idx}>
<button
className={`px-3 py-1 rounded ${currentPage === idx + 1 ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700"}`}
onClick={() => setCurrentPage(idx + 1)}
disabled={currentPage === idx + 1}
>
{idx + 1}
</button>
</li>
))}
</ul>
</nav>
);
}
// 渲染卡片内容只返回图片部分结构在下方article里
function renderCardImage(post, idx) {
// 多图轮播
if (Array.isArray(post.images) && post.images.length > 1) {
return (
<Swiper
className="swiper"
modules={[Navigation, Pagination]}
pagination={{ clickable: true, el: `.spdb${idx}` }}
navigation={{ prevEl: `.snbpb${idx}`, nextEl: `.snbnb${idx}` }}
>
{post.images.map((img, i) => (
<SwiperSlide key={i} className="swiper-slide">
<Image
className="!transition-all !duration-[0.35s] !ease-in-out group-hover:scale-105 w-full h-auto"
alt={post.title || "image"}
src={img}
width={960}
height={600}
/>
</SwiperSlide>
))}
</Swiper>
);
}
// 视频
if (post.video_src || post.videoId) {
if (post.video_src && (post.video_src.endsWith('.mp4') || post.video_src.startsWith('/files/'))) {
return (
<video controls className="w-full h-56 rounded-xl object-cover">
<source src={post.video_src} type="video/mp4" />
您的浏览器不支持视频播放
</video>
);
}
const vid = post.videoId || (post.video_src && post.video_src.includes('youtube') ? post.video_src.split('embed/')[1] : null);
if (vid) {
return (
<iframe
className="w-full h-56 rounded-xl"
src={`https://www.youtube.com/embed/${vid}`}
title="YouTube video"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
}
}
// 单图
const img = post.image || (Array.isArray(post.images) && post.images[0]);
if (img) {
return (
<Image
className="!transition-all !duration-[0.35s] !ease-in-out group-hover:scale-105 w-full h-auto"
alt={post.title || "image"}
src={img}
width={960}
height={600}
/>
);
}
return null;
}
function getSummary(text, maxLen = 100) {
if (!text) return "";
// 中文:直接按字符截断
if (/[\u4e00-\u9fa5]/.test(text)) {
return text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
}
// 英文:按字符截断,但不截断单词
if (text.length <= maxLen) return text;
let cut = text.slice(0, maxLen);
// 如果最后一个字符不是空格,向前找到最近的空格
if (!/\s/.test(text[maxLen])) {
const lastSpace = cut.lastIndexOf(" ");
if (lastSpace > 0) cut = cut.slice(0, lastSpace);
}
return cut + "...";
}
return (
<div className="xl:w-10/12 lg:w-10/12 w-full flex-[0_0_auto] !px-[15px] max-w-full !mx-auto my-[150px]">
<div
className={`blog classic-view grid grid-cols-1 sm:grid-cols-2 ${colClass} gap-6`}
>
{loading ? (
<div className="col-span-4 text-center py-12 text-gray-400">加载中...</div>
) : posts.length === 0 ? (
<div className="col-span-4 text-center py-12 text-gray-400">暂无数据</div>
) : (
posts.map((post, idx) => (
<article key={post.slug || post.id || idx} className="post !mb-8">
<div className="card">
{/* 图片/轮播/视频部分 */}
<figure className="card-img-top overlay overlay-1 hover-scale group">
<Link href={post.slug ? `/${page_slug}/${post.slug}` : "#"} className="block relative">
{renderCardImage(post, idx)}
<span className="bg"></span>
<figcaption className="group-hover:opacity-100 absolute w-full h-full opacity-0 text-center px-4 py-3 inset-0 z-[5] pointer-events-none p-2">
<h5 className="from-top !mb-0 absolute w-full translate-y-[-80%] p-[.75rem_1rem] left-0 top-2/4">
Read More
</h5>
</figcaption>
</Link>
</figure>
{/* 文字内容 */}
<div className="card-body flex-[1_1_auto] p-[40px] xl:!p-[2rem_2.5rem_1.25rem] lg:!p-[2rem_2.5rem_1.25rem] md:!p-[2rem_2.5rem_1.25rem] max-md:pb-4">
<div className="post-header !mb-[.9rem]">
<div className="inline-flex !mb-[.4rem] uppercase !tracking-[0.02rem] text-[0.7rem] font-bold !text-[#aab0bc] relative align-top !pl-[1.4rem] before:content-[''] before:absolute before:inline-block before:translate-y-[-60%] before:w-3 before:h-[0.05rem] before:left-0 before:top-2/4 before:bg-[#3f78e0]">
<a href={`/${page_slug}`} className="hover" rel="category">
{post.category || ""}
</a>
</div>
<h2 className="post-title !mt-1 !leading-[1.35] !mb-0">
<Link
className="!text-[#343f52] hover:!text-[#3f78e0]"
href={post.slug ? `/${page_slug}/${post.slug}` : "#"}
>
{post.title}
</Link>
</h2>
</div>
<div className="!relative">
<p>{getSummary(post.subtitle)}</p>
</div>
</div>
{/* 底部信息 */}
<div className="card-footer xl:!p-[1.25rem_2.5rem_1.25rem] lg:!p-[1.25rem_2.5rem_1.25rem] md:!p-[1.25rem_2.5rem_1.25rem] p-[18px_40px]">
<ul className="!text-[0.7rem] !text-[#aab0bc] m-0 p-0 list-none flex !mb-0">
<li className="post-date inline-block">
<i className="uil uil-calendar-alt pr-[0.2rem] align-[-.05rem] before:content-['\\e9ba']" />
<span>{post.date || post.published_at || ""}</span>
</li>
{post.author && (
<li className="post-author inline-block before:content-[''] before:inline-block before:w-[0.2rem] before:h-[0.2rem] before:opacity-50 before:m-[0_.6rem_0] before:rounded-[100%] before:align-[.15rem] before:bg-[#aab0bc]">
<a className="!text-[#aab0bc] hover:!text-[#3f78e0] hover:!border-[#3f78e0]" href="#">
<i className="uil uil-user pr-[0.2rem] align-[-.05rem] before:content-['\\ed6f']" />
<span>{post.author}</span>
</a>
</li>
)}
{typeof post.comments === 'number' && (
<li className="post-comments inline-block before:content-[''] before:inline-block before:w-[0.2rem] before:h-[0.2rem] before:opacity-50 before:m-[0_.6rem_0] before:rounded-[100%] before:align-[.15rem] before:bg-[#aab0bc]">
<a className="!text-[#aab0bc] hover:!text-[#3f78e0] hover:!border-[#3f78e0]" href="#">
<i className="uil uil-comment pr-[0.2rem] align-[-.05rem] before:content-['\\ea54']" />
{post.comments}
</a>
</li>
)}
{typeof post.likes === 'number' && (
<li className="post-likes !ml-auto inline-block">
<a className="!text-[#aab0bc] hover:!text-[#3f78e0] hover:!border-[#3f78e0]" href="#">
<i className="uil uil-heart-alt pr-[0.2rem] align-[-.05rem] before:content-['\\eb60']" />
{post.likes}
</a>
</li>
)}
</ul>
</div>
</div>
</article>
))
)}
</div>
<PaginationComp />
</div>
);
}