重构DynamicListPage
This commit is contained in:
parent
58e1794c9f
commit
a4e4de01d1
@ -54,7 +54,7 @@ export async function generateMetadata({ params }) {
|
||||
};
|
||||
}
|
||||
|
||||
export default async function DynamicPage({ params }) {
|
||||
export default async function DynamicPage({ params, searchParams }) {
|
||||
const resolvedParams = await params;
|
||||
const slugArr = resolvedParams.slug;
|
||||
|
||||
@ -96,6 +96,7 @@ export default async function DynamicPage({ params }) {
|
||||
columns={listColumns}
|
||||
pageSize={pageSize}
|
||||
totalItems={total}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
125
app/api/get-page-data/route.js
Normal file
125
app/api/get-page-data/route.js
Normal file
@ -0,0 +1,125 @@
|
||||
import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const JINGROW_SERVER_URL = process.env.JINGROW_SERVER_URL;
|
||||
const PUBLIC_FILES_DIR = path.join(process.cwd(), 'public/files');
|
||||
|
||||
if (!fs.existsSync(PUBLIC_FILES_DIR)) {
|
||||
fs.mkdirSync(PUBLIC_FILES_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function downloadToLocal(fileUrl) {
|
||||
try {
|
||||
let fullUrl = fileUrl;
|
||||
if (!/^https?:\/\//.test(fileUrl)) {
|
||||
fullUrl = `${JINGROW_SERVER_URL}${fileUrl}`;
|
||||
}
|
||||
const fileName = path.basename(fullUrl.split('?')[0]);
|
||||
const localPath = path.join(PUBLIC_FILES_DIR, fileName);
|
||||
const localUrl = `/files/${fileName}`;
|
||||
if (!fs.existsSync(localPath)) {
|
||||
const response = await axios.get(fullUrl, { responseType: 'stream' });
|
||||
await new Promise((resolve, reject) => {
|
||||
const writer = fs.createWriteStream(localPath);
|
||||
response.data.pipe(writer);
|
||||
writer.on('finish', resolve);
|
||||
writer.on('error', reject);
|
||||
});
|
||||
}
|
||||
return localUrl;
|
||||
} catch (e) {
|
||||
return fileUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// 提取富文本中的所有图片链接
|
||||
function extractImageUrlsFromHtml(html) {
|
||||
if (!html) return [];
|
||||
const regex = /<img[^>]+src=["']([^"'>]+)["']/g;
|
||||
const urls = [];
|
||||
let match;
|
||||
while ((match = regex.exec(html)) !== null) {
|
||||
urls.push(match[1]);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
// 替换富文本中的图片链接为本地路径
|
||||
async function replaceImageUrlsInHtml(html) {
|
||||
if (!html) return html;
|
||||
const regex = /<img([^>]+)src=["']([^"'>]+)["']/g;
|
||||
return await html.replace(regex, async (match, pre, url) => {
|
||||
const localUrl = await downloadToLocal(url);
|
||||
return `<img${pre}src="${localUrl}"`;
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const slug_list = body.slug_list;
|
||||
const page = body.page || 1;
|
||||
const page_size = body.page_size || undefined;
|
||||
if (!Array.isArray(slug_list)) {
|
||||
return Response.json({ error: 'slug_list参数必须为数组' }, { status: 400 });
|
||||
}
|
||||
const params = { slug_list: JSON.stringify(slug_list), page };
|
||||
if (page_size) params.page_size = page_size;
|
||||
const response = await axios.get(
|
||||
`${JINGROW_SERVER_URL}/api/method/jsite.api.v1.get_page_data`,
|
||||
{ params }
|
||||
);
|
||||
const message = response.data.message;
|
||||
let data = message?.data;
|
||||
if (Array.isArray(data)) {
|
||||
for (const item of data) {
|
||||
if (item.image) {
|
||||
item.image = await downloadToLocal(item.image);
|
||||
}
|
||||
for (const key of ['content', 'additional_content']) {
|
||||
if (item[key]) {
|
||||
const urls = extractImageUrlsFromHtml(item[key]);
|
||||
let html = item[key];
|
||||
for (const url of urls) {
|
||||
const localUrl = await downloadToLocal(url);
|
||||
html = html.replaceAll(url, localUrl);
|
||||
}
|
||||
item[key] = html;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (data) {
|
||||
if (data.image) {
|
||||
data.image = await downloadToLocal(data.image);
|
||||
}
|
||||
for (const key of ['content', 'additional_content']) {
|
||||
if (data[key]) {
|
||||
const urls = extractImageUrlsFromHtml(data[key]);
|
||||
let html = data[key];
|
||||
for (const url of urls) {
|
||||
const localUrl = await downloadToLocal(url);
|
||||
html = html.replaceAll(url, localUrl);
|
||||
}
|
||||
data[key] = html;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message?.data) {
|
||||
return Response.json({
|
||||
data: message.data,
|
||||
total: message.total,
|
||||
page_info: message.page_info
|
||||
});
|
||||
} else if (message?.error) {
|
||||
return Response.json({ error: message.error }, { status: 400 });
|
||||
} else {
|
||||
return Response.json({ error: '未知错误' }, { status: 500 });
|
||||
}
|
||||
} catch (error) {
|
||||
return Response.json(
|
||||
{ error: error.message, detail: error?.response?.data || null },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,140 +0,0 @@
|
||||
import Banner from "@/components/banner/Banner";
|
||||
import Category from "@/components/sidebar/Category";
|
||||
import Pagination1 from "@/components/common/Pagination1";
|
||||
import { getSiteSettings } from "@/utils/siteSettings";
|
||||
import ListPageTemplate from "@/components/common/ListPageTemplate";
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
const baseSlug = 'applications';
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const slugArr = [baseSlug, ...(params.slug ? (Array.isArray(params.slug) ? params.slug : [params.slug]) : [])];
|
||||
const res = await fetch(`${process.env.SITE_URL}/api/get-page-data`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ slug_list: slugArr })
|
||||
});
|
||||
const json = await res.json();
|
||||
const { data, error, page_info } = json;
|
||||
if (error) {
|
||||
return {
|
||||
title: error.title || 'Page Error',
|
||||
description: error.message || '',
|
||||
};
|
||||
}
|
||||
if (Array.isArray(data) && page_info) {
|
||||
return {
|
||||
title: page_info.meta_title || page_info.title || '',
|
||||
description: page_info.meta_description || '',
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: data?.meta_title || data?.title || '',
|
||||
description: data?.meta_description || '',
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({ params, searchParams }) {
|
||||
const slugArr = [baseSlug, ...(params.slug ? (Array.isArray(params.slug) ? params.slug : [params.slug]) : [])];
|
||||
const currentPage = Number(searchParams?.page) || 1;
|
||||
const siteSettings = await getSiteSettings(process.env.SITE_URL);
|
||||
const pageSize = Number(siteSettings.page_size) || 12;
|
||||
const res = await fetch(`${process.env.SITE_URL}/api/get-page-data`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ slug_list: slugArr, page: currentPage, page_size: pageSize })
|
||||
});
|
||||
const result = await res.json();
|
||||
const { data, error, total } = result;
|
||||
if (error) {
|
||||
notFound();
|
||||
}
|
||||
const bannerComponentName = 'Banner-' + baseSlug;
|
||||
const categoryComponentName = 'Category-' + baseSlug;
|
||||
if (Array.isArray(data)) {
|
||||
const currentPath = '/' + [baseSlug, ...(params.slug ? (Array.isArray(params.slug) ? params.slug : [params.slug]) : [])].join('/');
|
||||
const totalPages = Math.ceil((total || 0) / pageSize);
|
||||
const listItems = data.map(item => ({
|
||||
slug: item.slug,
|
||||
title: item.title,
|
||||
image: item.image || item.cover || item.img || '',
|
||||
content: item.content || item.description || '',
|
||||
}));
|
||||
return (
|
||||
<>
|
||||
<Banner componentName={bannerComponentName} />
|
||||
<div className="wrapper !bg-[#ffffff]">
|
||||
<div className="container py-[4.5rem] xl:!py-24 lg:!py-24 md:!py-24">
|
||||
<div className="flex flex-col md:flex-row mx-[-15px] xl:mx-[-35px] lg:mx-[-20px]">
|
||||
<Category componentName={categoryComponentName} />
|
||||
<div className="flex-1 min-w-0 !px-[15px] max-w-full md:!px-[20px] lg:!px-[20px] xl:!px-[35px]">
|
||||
<ListPageTemplate items={listItems} basePath={currentPath} />
|
||||
<div className="mt-8">
|
||||
<Pagination1
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
basePath={currentPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else if (data) {
|
||||
return (
|
||||
<>
|
||||
<Banner componentName={bannerComponentName} />
|
||||
<div className="wrapper !bg-[#ffffff]">
|
||||
<div className="container py-[4.5rem] xl:!py-24 lg:!py-24 md:!py-24">
|
||||
<div className="flex flex-col md:flex-row mx-[-15px] xl:mx-[-35px] lg:mx-[-20px]">
|
||||
<Category componentName={categoryComponentName} />
|
||||
<div className="flex-1 min-w-0 !px-[15px] max-w-full md:!px-[20px] lg:!px-[20px] xl:!px-[35px]">
|
||||
<div className="bg-white rounded-lg shadow-md p-6 md:p-10">
|
||||
{/* 图片和附加信息并排显示,响应式优化 */}
|
||||
{(data.image || data.subtitle) && (
|
||||
<div className="flex flex-col md:flex-row gap-4 md:gap-8 mb-6 md:mb-8 items-center md:items-start w-full max-w-full">
|
||||
{data.image && (
|
||||
<div className="flex-shrink-0 w-full md:w-72 flex justify-center md:justify-start mb-4 md:mb-0 max-w-full">
|
||||
<img
|
||||
src={data.image}
|
||||
alt={data.title}
|
||||
className="w-full max-w-[320px] md:max-w-full max-h-60 md:max-h-72 rounded-xl shadow-lg object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{data.subtitle && (
|
||||
<div className="bg-gray-50 rounded-md p-3 md:p-4 whitespace-pre-line text-gray-700 w-full shadow-sm max-w-full prose prose-sm">
|
||||
{data.subtitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Product Description 标题 */}
|
||||
<div className="mb-2 md:mb-4">
|
||||
<span className="inline-block bg-[#1fc76f] text-white text-lg md:text-xl font-semibold px-5 py-2 rounded-t-md shadow-sm">Product Description</span>
|
||||
</div>
|
||||
<div className="prose max-w-none text-gray-700 mb-6 md:mb-8 px-1 md:px-0 prose-img:max-w-full prose-img:h-auto prose-table:border prose-table:border-gray-300 prose-th:border prose-th:border-gray-300 prose-td:border prose-td:border-gray-300 prose-th:bg-gray-50 prose-table:rounded-lg prose-table:overflow-hidden" dangerouslySetInnerHTML={{ __html: data.content || '' }} />
|
||||
{data.additional_content && (
|
||||
<>
|
||||
{/* Testing Report 标题 */}
|
||||
<div className="mb-2 md:mb-4">
|
||||
<span className="inline-block bg-[#1fc76f] text-white text-lg md:text-xl font-semibold px-5 py-2 rounded-t-md shadow-sm">Testing Report</span>
|
||||
</div>
|
||||
<div className="prose max-w-none text-gray-700 mt-6 md:mt-8 px-1 md:px-0 prose-img:max-w-full prose-img:h-auto prose-table:border prose-table:border-gray-300 prose-th:border prose-th:border-gray-300 prose-td:border prose-td:border-gray-300 prose-th:bg-gray-50 prose-table:rounded-lg prose-table:overflow-hidden" dangerouslySetInnerHTML={{ __html: data.additional_content }} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
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 "@/utils/siteSettings";
|
||||
import ListPageTemplate from "@/components/common/ListPageTemplate";
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
const baseSlug = 'applications';
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
export async function generateMetadata() {
|
||||
// 频道首页只需要基础 meta 信息
|
||||
return {
|
||||
title: 'Applications',
|
||||
description: 'Applications Channel Home',
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }) {
|
||||
const currentPage = Number(searchParams?.page) || 1;
|
||||
const siteSettings = await getSiteSettings(process.env.SITE_URL);
|
||||
const pageSize = Number(siteSettings.page_size) || 12;
|
||||
// 频道首页只查根 slug
|
||||
const res = await fetch(`${process.env.SITE_URL}/api/get-page-data`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ slug_list: [baseSlug], page: currentPage, page_size: pageSize })
|
||||
});
|
||||
const result = await res.json();
|
||||
const { data, error, total } = result;
|
||||
if (error) notFound();
|
||||
const bannerComponentName = 'Banner-' + baseSlug;
|
||||
const categoryComponentName = 'Category-' + baseSlug;
|
||||
const galleryComponentName = 'Gallery-' + baseSlug;
|
||||
const currentPath = '/' + baseSlug;
|
||||
const totalPages = Math.ceil((total || 0) / pageSize);
|
||||
const listItems = Array.isArray(data) ? data.map(item => ({
|
||||
slug: item.slug,
|
||||
title: item.title,
|
||||
image: item.image || item.cover || item.img || '',
|
||||
content: item.content || item.description || '',
|
||||
})) : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Banner componentName={bannerComponentName} />
|
||||
<div className="wrapper !bg-[#ffffff]">
|
||||
<div className="container py-[4.5rem] xl:!py-24 lg:!py-24 md:!py-24">
|
||||
<div className="flex flex-col md:flex-row mx-[-15px] xl:mx-[-35px] lg:mx-[-20px]">
|
||||
<Category componentName={categoryComponentName} />
|
||||
<div className="flex-1 min-w-0 !px-[15px] max-w-full md:!px-[20px] lg:!px-[20px] xl:!px-[35px]">
|
||||
<Gallery componentName={galleryComponentName} />
|
||||
<ListPageTemplate items={listItems} basePath={currentPath} />
|
||||
<div className="mt-8">
|
||||
<Pagination1
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
basePath={currentPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -40,7 +40,7 @@ export async function generateMetadata({ params }) {
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({ params }) {
|
||||
export default async function Page({ params, searchParams }) {
|
||||
const resolvedParams = await params;
|
||||
const slug = resolvedParams.slug || [];
|
||||
const slugArr = [baseSlug, ...(Array.isArray(slug) ? slug : [slug])];
|
||||
@ -79,6 +79,7 @@ export default async function Page({ params }) {
|
||||
columns={listColumns}
|
||||
pageSize={pageSize}
|
||||
totalItems={total}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import ListPageTemplate from '@/components/common/ListPageTemplate';
|
||||
import Pagination1 from '@/components/common/Pagination1';
|
||||
import axios from 'axios';
|
||||
|
||||
const LoadingSpinner = () => (
|
||||
<div className="flex justify-center items-center p-8">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function DynamicListPage({ initialItems, slugArr, basePath, columns, pageSize, totalItems }) {
|
||||
const [items, setItems] = useState(initialItems);
|
||||
const [total, setTotal] = useState(totalItems);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const searchParams = useSearchParams();
|
||||
const page = searchParams.get('page');
|
||||
const currentPage = Number(page) || 1;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageData = async () => {
|
||||
if (currentPage === 1) {
|
||||
setItems(initialItems);
|
||||
setTotal(totalItems);
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await axios.post('/api/get-page-data', {
|
||||
slug_list: slugArr,
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
});
|
||||
setItems(res.data.data);
|
||||
setTotal(res.data.total);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch page data:', error);
|
||||
// Optionally, handle the error in the UI
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageData();
|
||||
}, [currentPage, slugArr, pageSize, initialItems, totalItems]);
|
||||
|
||||
const listItems = items.map(item => ({
|
||||
...item,
|
||||
slug: item.slug,
|
||||
title: item.title,
|
||||
image: item.image || item.cover || item.img || '',
|
||||
additional_title: item.additional_title || '',
|
||||
subtitle: item.subtitle || item.content || '',
|
||||
}));
|
||||
|
||||
const totalPages = Math.ceil((total || 0) / pageSize);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<ListPageTemplate items={listItems} basePath={basePath} columns={columns} />
|
||||
)}
|
||||
<div className="mt-8">
|
||||
<Pagination1
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
basePath={basePath}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
35
components/common/DynamicListPage/DynamicListPageUI.jsx
Normal file
35
components/common/DynamicListPage/DynamicListPageUI.jsx
Normal file
@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import ListPageTemplate from '@/components/common/ListPageTemplate';
|
||||
import Pagination1 from '@/components/common/Pagination1';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function DynamicListPageUI({ initialItems, basePath, columns, pageSize, totalItems }) {
|
||||
const searchParams = useSearchParams();
|
||||
const page = searchParams.get('page');
|
||||
const currentPage = Number(page) || 1;
|
||||
|
||||
const listItems = initialItems.map(item => ({
|
||||
...item,
|
||||
slug: item.slug,
|
||||
title: item.title,
|
||||
image: item.image || item.cover || item.img || '',
|
||||
additional_title: item.additional_title || '',
|
||||
subtitle: item.subtitle || item.content || '',
|
||||
}));
|
||||
|
||||
const totalPages = Math.ceil((totalItems || 0) / pageSize);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListPageTemplate items={listItems} basePath={basePath} columns={columns} />
|
||||
<div className="mt-8">
|
||||
<Pagination1
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
basePath={basePath}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
40
components/common/DynamicListPage/index.jsx
Normal file
40
components/common/DynamicListPage/index.jsx
Normal file
@ -0,0 +1,40 @@
|
||||
import DynamicListPageUI from './DynamicListPageUI';
|
||||
import { getPageData } from '@/utils/data';
|
||||
|
||||
export default async function DynamicListPage({ slugArr, basePath, columns, pageSize = 10, searchParams }) {
|
||||
// 先 await searchParams
|
||||
const resolvedSearchParams = await searchParams;
|
||||
// 解析当前页码
|
||||
const currentPage = Number(resolvedSearchParams?.page) || 1;
|
||||
|
||||
let items = [];
|
||||
let total = 0;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const result = await getPageData({
|
||||
slug_list: slugArr,
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
downloadFiles: true
|
||||
});
|
||||
items = result.data;
|
||||
total = result.total;
|
||||
error = result.error;
|
||||
} catch (e) {
|
||||
items = [];
|
||||
total = 0;
|
||||
error = e;
|
||||
}
|
||||
|
||||
return (
|
||||
<DynamicListPageUI
|
||||
initialItems={items}
|
||||
slugArr={slugArr}
|
||||
basePath={basePath}
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
totalItems={total}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user