diff --git a/app/[...slug]/page.jsx b/app/[...slug]/page.jsx
index 9568683..df784b1 100644
--- a/app/[...slug]/page.jsx
+++ b/app/[...slug]/page.jsx
@@ -1,10 +1,10 @@
import Banner from "@/components/banner/Banner";
import Category from "@/components/sidebar/Category";
-import { getSiteSettings } from "@/utlis/siteSettings";
+import { getSiteSettings } from "@/utils/siteSettings";
import { notFound } from 'next/navigation';
import DynamicListPage from "@/components/common/DynamicListPage";
import { Suspense } from 'react';
-import { getPageData, getAllSlugs } from "@/utlis/data";
+import { getPageData, getAllSlugs } from "@/utils/data";
const LoadingSpinner = () => (
diff --git a/app/applications/[...slug]/page.jsx b/app/applications/[...slug]/page.jsx
index 8886f97..261ebf6 100644
--- a/app/applications/[...slug]/page.jsx
+++ b/app/applications/[...slug]/page.jsx
@@ -1,7 +1,7 @@
import Banner from "@/components/banner/Banner";
import Category from "@/components/sidebar/Category";
import Pagination1 from "@/components/common/Pagination1";
-import { getSiteSettings } from "@/utlis/siteSettings";
+import { getSiteSettings } from "@/utils/siteSettings";
import ListPageTemplate from "@/components/common/ListPageTemplate";
import { notFound } from 'next/navigation';
diff --git a/app/applications/page.jsx b/app/applications/page.jsx
index 5688554..6d3efe8 100644
--- a/app/applications/page.jsx
+++ b/app/applications/page.jsx
@@ -2,7 +2,7 @@ import Banner from "@/components/banner/Banner";
import Category from "@/components/sidebar/Category";
import Gallery from "@/components/common/Gallery";
import Pagination1 from "@/components/common/Pagination1";
-import { getSiteSettings } from "@/utlis/siteSettings";
+import { getSiteSettings } from "@/utils/siteSettings";
import ListPageTemplate from "@/components/common/ListPageTemplate";
import { notFound } from 'next/navigation';
diff --git a/app/layout.jsx b/app/layout.jsx
index e2b37bd..d2cdb7a 100644
--- a/app/layout.jsx
+++ b/app/layout.jsx
@@ -4,10 +4,10 @@ import "../public/assets/style.css";
import "photoswipe/dist/photoswipe.css";
import iTooltip from "itooltip";
import { usePathname } from "next/navigation";
-import scrollQue from "../utlis/scrollCue.min.js";
+import scrollQue from "../utils/scrollCue.min.js";
import Context from "@/context/Context";
import ProgressWrap from "@/components/common/ProgressWrap";
-import initPlayer from "@/utlis/initPlayer";
+import initPlayer from "@/utils/initPlayer";
import SearchModal from "@/components/modals/SearchModal";
import InfoModal from "@/components/modals/InfoModal";
import Header from "@/components/headers/Header";
diff --git a/app/page.jsx b/app/page.jsx
index 1febd35..5de8d45 100644
--- a/app/page.jsx
+++ b/app/page.jsx
@@ -1,7 +1,7 @@
import Hero from "@/components/homes/home-15/Hero";
import SwiperItems from "@/components/homes/home-15/SwiperItems";
import React from "react";
-import { getSiteSettings } from "@/utlis/siteSettings";
+import { getSiteSettings } from "@/utils/siteSettings";
export const revalidate = 3600;
diff --git a/app/products/[...slug]/page.jsx b/app/products/[...slug]/page.jsx
index ad942be..7a6c421 100644
--- a/app/products/[...slug]/page.jsx
+++ b/app/products/[...slug]/page.jsx
@@ -1,6 +1,6 @@
import Banner from "@/components/banner/Banner";
import Category from "@/components/sidebar/Category";
-import { getSiteSettings } from "@/utlis/siteSettings";
+import { getSiteSettings } from "@/utils/siteSettings";
import { notFound } from 'next/navigation';
import DynamicListPage from "@/components/common/DynamicListPage";
import { Suspense } from 'react';
diff --git a/components/footers/Footer.jsx b/components/footers/Footer.jsx
index 66c63ac..c6a8ea9 100644
--- a/components/footers/Footer.jsx
+++ b/components/footers/Footer.jsx
@@ -1,7 +1,7 @@
'use client';
import React, { useEffect, useState } from "react";
import SocialLinks from "@/components/contact/SocialLinks";
-import { getSiteSettings } from "@/utlis/siteSettings";
+import { getSiteSettings } from "@/utils/siteSettings";
import MenuList from "./MenuList";
import Contact from "@/components/contact/Contact";
import Image from "next/image";
diff --git a/components/footers/Footer15.jsx b/components/footers/Footer15.jsx
index f74ed71..ff1accd 100644
--- a/components/footers/Footer15.jsx
+++ b/components/footers/Footer15.jsx
@@ -1,7 +1,7 @@
'use client';
import React, { useEffect, useState } from "react";
import SocialLinks from "@/components/contact/SocialLinks";
-import { getSiteSettings } from "@/utlis/siteSettings";
+import { getSiteSettings } from "@/utils/siteSettings";
import MenuList from "./MenuList";
import Contact from "@/components/contact/Contact";
diff --git a/components/headers/Header.jsx b/components/headers/Header.jsx
index 5bcd442..bea7940 100644
--- a/components/headers/Header.jsx
+++ b/components/headers/Header.jsx
@@ -5,7 +5,7 @@ import Link from "next/link";
import Image from "next/image";
import LoginButton from "./LoginButton";
import SocialLinks from "../contact/SocialLinks";
-import { getSiteSettings } from "@/utlis/siteSettings";
+import { getSiteSettings } from "@/utils/siteSettings";
export default function Header15() {
const [siteSettings, setSiteSettings] = useState({});
diff --git a/components/headers/Header15.jsx b/components/headers/Header15.jsx
index b4b8db0..30f0b85 100644
--- a/components/headers/Header15.jsx
+++ b/components/headers/Header15.jsx
@@ -5,7 +5,7 @@ import Link from "next/link";
import Image from "next/image";
import LanguageSelect from "./LanguageSelect";
import SocialLinks from "../contact/SocialLinks";
-import { getSiteSettings } from "@/utlis/siteSettings";
+import { getSiteSettings } from "@/utils/siteSettings";
export default function Header15() {
const [siteSettings, setSiteSettings] = useState({});
diff --git a/components/homes/home-15/SwiperItems.jsx b/components/homes/home-15/SwiperItems.jsx
deleted file mode 100644
index 62169a6..0000000
--- a/components/homes/home-15/SwiperItems.jsx
+++ /dev/null
@@ -1,282 +0,0 @@
-"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 (
-
- {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 (
-
VIDEO
- );
- }
- }
- // 单图
- 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;
- }
-
- return (
-
- {/* 新增模块标题和副标题显示 */}
- {(title || subtitle) && (
-
- {title &&
{title} }
- {subtitle &&
{subtitle}
}
-
- )}
-
- {loading ? (
-
加载中...
- ) : posts.length === 0 ? (
-
暂无数据
- ) : (
-
-
1 ? 2 : 1 } },
- 992: { slidesPerView: columns, slidesPerGroup: columns * rows, grid: { rows } },
- }}
- >
- {posts.map((post, idx) => (
-
-
-
- {/* 图片/轮播/视频部分 */}
-
-
- {renderCardImage(post, idx)}
-
-
-
- 查看详情
-
-
-
-
- {/* 文字内容 */}
-
-
-
- {post.additional_title || ""}
-
-
-
- {post.title}
-
-
-
-
-
{getSummary(post.subtitle)}
-
-
-
-
-
- ))}
-
- {/* 美化后的导航按钮 */}
-
-
-
-
-
-
-
-
-
-
-
- )}
-
-
-
- );
-}
-
-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,
-};
diff --git a/components/homes/home-15/SwiperItems/SwiperItemsUI.jsx b/components/homes/home-15/SwiperItems/SwiperItemsUI.jsx
new file mode 100644
index 0000000..e0bccef
--- /dev/null
+++ b/components/homes/home-15/SwiperItems/SwiperItemsUI.jsx
@@ -0,0 +1,262 @@
+"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 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 (
+
VIDEO
+ );
+ }
+ }
+ // 单图
+ 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 (
+
+ {/* 新增模块标题和副标题显示 */}
+ {(title || subtitle) && (
+
+ {title &&
{title} }
+ {subtitle &&
{subtitle}
}
+
+ )}
+
+
+
1 ? 2 : 1 } },
+ 992: { slidesPerView: columns, slidesPerGroup: columns * rows, grid: { rows } },
+ }}
+ >
+ {items.map((post, idx) => (
+
+
+
+ {/* 图片/轮播/视频部分 */}
+
+
+ {renderCardImage(post, idx)}
+
+
+
+ 查看详情
+
+
+
+
+ {/* 文字内容 */}
+
+
+
+ {post.additional_title || ""}
+
+
+
+ {post.title}
+
+
+
+
+
{getSummary(post.subtitle)}
+
+
+
+
+
+ ))}
+
+ {/* 美化后的导航按钮 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+SwiperItemsUI.propTypes = {
+ data: PropTypes.object,
+ items: PropTypes.array,
+ pagetype: PropTypes.string,
+ category: PropTypes.string,
+ count: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number
+ ]),
+ columns: PropTypes.number,
+ category_slug: PropTypes.string,
+ rows: PropTypes.number,
+};
\ No newline at end of file
diff --git a/components/homes/home-15/SwiperItems/index.jsx b/components/homes/home-15/SwiperItems/index.jsx
new file mode 100644
index 0000000..3208177
--- /dev/null
+++ b/components/homes/home-15/SwiperItems/index.jsx
@@ -0,0 +1,30 @@
+import SwiperItemsUI from "./SwiperItemsUI";
+import { fetchComponentData, fetchListViewData } from "@/utils/data";
+
+export default async function SwiperItems(props) {
+ // 1. Fetch component settings from the CMS/backend using the centralized utility.
+ const { data: componentData } = await fetchComponentData("SwiperItems");
+
+ // If there are no settings, we can't proceed.
+ if (!componentData) {
+ return null;
+ }
+
+ // 2. Determine parameters for fetching the list of items, using props as a fallback.
+ const pagetype = componentData?.t1 || props.pagetype || "";
+ const category = componentData?.t2 || props.category || "";
+ const count = componentData?.t4 || props.count || 12;
+
+ let items = [];
+ // Only fetch items if a pagetype is defined.
+ if (pagetype) {
+ const { data: listViewData } = await fetchListViewData({ pagetype, category, count });
+ if (Array.isArray(listViewData)) {
+ items = listViewData;
+ }
+ }
+
+ // 3. Pass the server-fetched data to the client component for rendering.
+ // This component is now lean and only concerned with orchestrating data flow.
+ return
;
+}
\ No newline at end of file
diff --git a/utlis/data.js b/utils/data.js
similarity index 76%
rename from utlis/data.js
rename to utils/data.js
index 3991c63..28d1eff 100644
--- a/utlis/data.js
+++ b/utils/data.js
@@ -170,4 +170,42 @@ export async function getAllSlugs() {
console.error('Error fetching slugs:', error);
return [];
}
+}
+
+export async function fetchComponentData(componentName) {
+ try {
+ // We use no-store to ensure data is fresh, suitable for dynamic component settings.
+ // For more static data, you might adjust the cache policy.
+ const res = await fetch(`${JINGROW_SERVER_URL}/api/method/jsite.api.v1.get_component_data?component_name=${componentName}`, { cache: 'no-store' });
+ if (!res.ok) {
+ const errorText = await res.text();
+ console.error(`Failed to fetch component data for ${componentName}: ${res.statusText}`, errorText);
+ return { data: null };
+ }
+ const result = await res.json();
+ return { data: result.message?.data || null };
+ } catch (error) {
+ console.error(`Error fetching component data for ${componentName}:`, error);
+ return { data: null };
+ }
+}
+
+export async function fetchListViewData({ pagetype, category, count }) {
+ try {
+ const queryParams = new URLSearchParams({ pagetype });
+ if (category) queryParams.append("category", category);
+ if (count !== undefined && count !== null) queryParams.append("count", String(count));
+
+ const res = await fetch(`${JINGROW_SERVER_URL}/api/method/jsite.api.v1.get_listview_data?${queryParams.toString()}`, { cache: 'no-store' });
+ if (!res.ok) {
+ const errorText = await res.text();
+ console.error(`Failed to fetch list view data: ${res.statusText}`, errorText);
+ return { data: [] };
+ }
+ const result = await res.json();
+ return { data: result.message?.data || [] };
+ } catch (error) {
+ console.error(`Error fetching list view data:`, error);
+ return { data: [] };
+ }
}
\ No newline at end of file
diff --git a/utlis/initPlayer.js b/utils/initPlayer.js
similarity index 100%
rename from utlis/initPlayer.js
rename to utils/initPlayer.js
diff --git a/utlis/initPrism.js b/utils/initPrism.js
similarity index 100%
rename from utlis/initPrism.js
rename to utils/initPrism.js
diff --git a/utlis/scrollCue.min.js b/utils/scrollCue.min.js
similarity index 100%
rename from utlis/scrollCue.min.js
rename to utils/scrollCue.min.js
diff --git a/utlis/siteSettings.js b/utils/siteSettings.js
similarity index 100%
rename from utlis/siteSettings.js
rename to utils/siteSettings.js