jingrow/components/homes/home-15/SwiperItems.jsx

283 lines
12 KiB
JavaScript
Raw 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, Grid } from "swiper/modules";
import Link from "next/link";
import Image from "next/image";
import PropTypes from "prop-types";
import axios from "axios";
export default function SwiperItems(props) {
const [data, setData] = useState(null);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// 统一字段映射后台优先props兜底
const title = data?.title || props.title;
const subtitle = data?.subtitle || props.subtitle;
const pagetype = data?.t1 || props.pagetype || "";
const category = data?.t2 || props.category || "";
const category_slug = data?.t3 || props.category_slug || "";
const count = data?.t4 || props.count || 8;
const columns = data?.t5 || props.columns || 4;
const rows = (data?.p1 && !isNaN(Number(data.p1))) ? Number(data.p1) : (props.rows || 1);
const button_text = data?.button_text || props.button_text;
useEffect(() => {
async function fetchComponentData() {
try {
setLoading(true);
const res = await axios.get("/api/get-component-data", {
params: { component_name: "SwiperItems" },
});
setData(res.data.data);
} catch (err) {
setError("获取SwiperItems数据失败");
} finally {
setLoading(false);
}
}
fetchComponentData();
}, []);
useEffect(() => {
async function fetchPosts() {
if (!pagetype) return;
setLoading(true);
try {
const params = new URLSearchParams({ pagetype });
if (category) params.append("category", category);
if (count !== undefined && count !== null) params.append("count", count);
const res = await fetch(`/api/get-listview-data?${params.toString()}`);
const json = await res.json();
if (Array.isArray(json.data)) {
setPosts(json.data);
} else {
setPosts([]);
}
} catch (e) {
setPosts([]);
}
setLoading(false);
}
fetchPosts();
}, [pagetype, category, count]);
if (!data) return null;
// 渲染卡片内容
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 = 58) {
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 + "...";
}
// 日期格式化函数,支持自定义是否显示时分秒
function formatDate(dateStr, options = {}) {
if (!dateStr) return "";
const d = new Date(dateStr);
if (isNaN(d.getTime())) return dateStr;
const { showTime = false } = options;
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
let result = `${yyyy}-${mm}-${dd}`;
if (showTime) {
const hh = String(d.getHours()).padStart(2, '0');
const min = String(d.getMinutes()).padStart(2, '0');
const ss = String(d.getSeconds()).padStart(2, '0');
result += ` ${hh}:${min}:${ss}`;
}
return result;
}
return (
<div className="w-full max-w-full mx-auto my-[150px] px-4 sm:px-6 md:px-8 xl:w-10/12 xl:px-0">
{/* 新增模块标题和副标题显示 */}
{(title || subtitle) && (
<div className="relative z-20 text-center mb-10 select-text">
{title && <h2 className="text-3xl font-bold mb-2">{title}</h2>}
{subtitle && <p className="text-lg text-gray-500">{subtitle}</p>}
</div>
)}
<div className="swiper-container blog classic-view !mb-[7rem] xl:!mb-[10rem] lg:!mb-[10rem] md:!mb-[10rem]">
{loading ? (
<div className={`text-center py-12 text-gray-400`}>加载中...</div>
) : posts.length === 0 ? (
<div className={`text-center py-12 text-gray-400`}>暂无数据</div>
) : (
<div className="relative">
<Swiper
className="swiper w-full"
modules={[Navigation, Pagination, Grid]}
spaceBetween={16}
slidesPerView={columns}
slidesPerGroup={columns * rows}
grid={{ rows }}
pagination={{ clickable: true, el: ".spdb15" }}
navigation={{
prevEl: '.swiper-nav-prev-swiperitems',
nextEl: '.swiper-nav-next-swiperitems'
}}
breakpoints={{
0: { slidesPerView: 1, slidesPerGroup: 1, grid: { rows: 1 } },
575: { slidesPerView: 1, slidesPerGroup: 1, grid: { rows: 1 } },
768: { slidesPerView: 2, slidesPerGroup: 2, grid: { rows: rows > 1 ? 2 : 1 } },
992: { slidesPerView: columns, slidesPerGroup: columns * rows, grid: { rows } },
}}
>
{posts.map((post, idx) => (
<SwiperSlide key={post.slug || post.id || idx} className="swiper-slide">
<article className="post !mb-8">
<div className="card">
{/* 图片/轮播/视频部分 */}
<figure className="card-img-top overlay overlay-1 hover-scale group">
<Link href={post.slug ? `/${category_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">
查看详情
</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-[#1fc76f]">
{post.additional_title || ""}
</div>
<h2 className="post-title !mt-1 !leading-[1.35] !mb-0 text-base md:!text-[0.85rem]">
<Link
className="!text-[#333333] hover:!text-[#1fc76f]"
href={post.slug ? `/${category_slug}/${post.slug}` : "#"}
>
{post.title}
</Link>
</h2>
</div>
<div className="!relative !text-[0.7rem]">
<p>{getSummary(post.subtitle)}</p>
</div>
</div>
</div>
</article>
</SwiperSlide>
))}
</Swiper>
{/* 美化后的导航按钮 */}
<button
className="swiper-nav-prev-swiperitems absolute left-[-32px] top-1/2 z-20 -translate-y-1/2 w-10 h-10 flex items-center justify-center !rounded-full border-2 border-[#e5e7eb] bg-transparent transition-all duration-200 hover:border-[#1fc76f] group"
style={{ color: '#b0b7c3' }}
aria-label="上一页"
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
<path d="M15.5 19L9.5 12L15.5 5" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" className="transition-all duration-200 group-hover:stroke-[#1fc76f]"/>
</svg>
</button>
<button
className="swiper-nav-next-swiperitems absolute right-[-32px] top-1/2 z-20 -translate-y-1/2 w-10 h-10 flex items-center justify-center !rounded-full border-2 border-[#e5e7eb] bg-transparent transition-all duration-200 hover:border-[#1fc76f] group"
style={{ color: '#b0b7c3' }}
aria-label="下一页"
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
<path d="M8.5 5L14.5 12L8.5 19" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" className="transition-all duration-200 group-hover:stroke-[#1fc76f]"/>
</svg>
</button>
</div>
)}
<div className="swiper-controls">
<div className="swiper-pagination spdb15"></div>
</div>
</div>
</div>
);
}
SwiperItems.propTypes = {
pagetype: PropTypes.string,
category: PropTypes.string,
count: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
columns: PropTypes.number,
category_slug: PropTypes.string,
rows: PropTypes.number,
};