"use client";
import { useState, useEffect } 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";
// This is now a "dumb" component. It receives all data via props and is only
// responsible for rendering the UI. All data fetching logic has been moved
// to the parent server component.
export default function SwiperItemsUI({ data, items, ...props }) {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
// This effect runs only on the client, after the component has mounted.
setIsMounted(true);
}, []);
// Field mapping from data, with props as fallback.
const title = data?.title || props.title;
const subtitle = data?.subtitle || props.subtitle;
const category_slug = data?.t3 || props.category_slug || "";
const columns = data?.t5 || props.columns || 4;
const button_text = data?.button_text || props.button_text || "Read More";
const rows = (data?.p1 && !isNaN(Number(data.p1))) ? Number(data.p1) : (props.rows || 1);
// Loading and error states are now handled by the server component and React Suspense.
// We only need to handle the case where there are no items to show.
if (!items || items.length === 0) {
return (
{(title || subtitle) && (
{title &&
{title}
}
{subtitle &&
{subtitle}
}
)}
暂无数据
);
}
// Helper functions for rendering. They are pure and belong with the UI.
function renderCardImage(post, idx) {
// 多图轮播
if (Array.isArray(post.images) && post.images.length > 1) {
return (
{post.images.map((img, i) => (
))}
);
}
// 视频
if (post.video_src || post.videoId) {
if (post.video_src && (post.video_src.endsWith('.mp4') || post.video_src.startsWith('/files/'))) {
return (
);
}
const vid = post.videoId || (post.video_src && post.video_src.includes('youtube') ? post.video_src.split('embed/')[1] : null);
if (vid) {
return (
);
}
}
// 单图
const img = post.image || (Array.isArray(post.images) && post.images[0]);
if (img) {
return (
);
}
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;
}
// By default, the component is rendered with opacity-0.
// Once the client-side JS loads and the component mounts, isMounted becomes true,
// and the component smoothly fades in, preventing any layout flash.
const containerClasses = `
w-full max-w-full mx-auto my-[150px] px-4 sm:px-6 md:px-8 xl:w-10/12 xl:px-0
transition-opacity duration-500 ease-in-out
${isMounted ? 'opacity-100' : 'opacity-0'}
`;
return (