增加Jsite Presentation Category
This commit is contained in:
parent
17c56bc8b5
commit
65abd70f23
@ -3,27 +3,41 @@ import { getPageData } from "@/utils/data";
|
||||
import { getLocalPageData } from "@/data/presentation";
|
||||
import Presentation from "@/components/presentation/Presentation";
|
||||
|
||||
export default async function PresentationPage({ params }) {
|
||||
const baseSlug = 'presentation';
|
||||
|
||||
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 async function Page({ params, searchParams }) {
|
||||
const resolvedParams = await params;
|
||||
const slugArr = resolvedParams.slug;
|
||||
const slug = resolvedParams.slug || [];
|
||||
const slugArr = [baseSlug, ...(Array.isArray(slug) ? slug : [slug])];
|
||||
|
||||
// 优先从本地markdown文件获取数据
|
||||
const localData = await getLocalPageData(slugArr);
|
||||
|
||||
if (localData.data) {
|
||||
// 如果本地文件存在,直接使用本地数据
|
||||
return <Presentation data={localData.data} />;
|
||||
}
|
||||
|
||||
// 如果本地文件不存在,从API获取数据
|
||||
const { data, error } = await getPageData({
|
||||
slug_list: slugArr,
|
||||
downloadFiles: true
|
||||
downloadFiles: true,
|
||||
});
|
||||
|
||||
if (error || !data) {
|
||||
if (error) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <Presentation data={data} />;
|
||||
}
|
||||
if (data) {
|
||||
// 优先从本地markdown文件获取数据
|
||||
const localData = await getLocalPageData(slugArr);
|
||||
|
||||
if (localData.data) {
|
||||
// 如果本地文件存在,直接使用本地数据
|
||||
return <Presentation data={localData.data} />;
|
||||
}
|
||||
|
||||
// 如果本地文件不存在,使用API数据
|
||||
return <Presentation data={data} />;
|
||||
} else {
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
|
||||
118
app/(presentation)/presentation/page.jsx
Normal file
118
app/(presentation)/presentation/page.jsx
Normal file
@ -0,0 +1,118 @@
|
||||
import Banner from "@/components/banner/Banner";
|
||||
import Category from "@/components/sidebar/Category";
|
||||
import { getSiteSettings } from "@/utils/data";
|
||||
import { notFound } from 'next/navigation';
|
||||
import DynamicListPage from "@/components/common/DynamicListPage";
|
||||
import { Suspense } from 'react';
|
||||
import { getPageData } from "@/utils/data";
|
||||
import { getLocalPageData } from "@/data/presentation";
|
||||
import Presentation from "@/components/presentation/Presentation";
|
||||
|
||||
const baseSlug = 'presentation';
|
||||
|
||||
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 const revalidate = 3600;
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const resolvedParams = await params;
|
||||
const slug = resolvedParams.slug || [];
|
||||
const slugArr = [baseSlug, ...(Array.isArray(slug) ? slug : [slug])];
|
||||
const { data, error, page_info } = await getPageData({ slug_list: slugArr });
|
||||
const siteSettings = await getSiteSettings();
|
||||
const siteTitle = siteSettings.site_title || '';
|
||||
const siteName = siteSettings.site_name || '';
|
||||
const siteNameInPageTitles = siteSettings.site_name_in_page_titles || 'None';
|
||||
|
||||
let title = '';
|
||||
if (error) {
|
||||
title = error.title || 'Page Error';
|
||||
return {
|
||||
title,
|
||||
description: error.message || '',
|
||||
};
|
||||
}
|
||||
if (Array.isArray(data) && page_info) {
|
||||
title = page_info.meta_title || page_info.title || '';
|
||||
if (siteName && title) {
|
||||
if (siteNameInPageTitles === 'After') {
|
||||
title = `${title} - ${siteName}`;
|
||||
} else if (siteNameInPageTitles === 'Before') {
|
||||
title = `${siteName} - ${title}`;
|
||||
}
|
||||
}
|
||||
return {
|
||||
title,
|
||||
description: page_info.meta_description || '',
|
||||
};
|
||||
}
|
||||
title = data?.meta_title || data?.title || '';
|
||||
if (siteName && title) {
|
||||
if (siteNameInPageTitles === 'After') {
|
||||
title = `${title} - ${siteName}`;
|
||||
} else if (siteNameInPageTitles === 'Before') {
|
||||
title = `${siteName} - ${title}`;
|
||||
}
|
||||
}
|
||||
return {
|
||||
title,
|
||||
description: data?.meta_description || '',
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({ params, searchParams }) {
|
||||
const resolvedParams = await params;
|
||||
const slug = resolvedParams.slug || [];
|
||||
const slugArr = [baseSlug, ...(Array.isArray(slug) ? slug : [slug])];
|
||||
|
||||
const siteSettings = await getSiteSettings();
|
||||
const pageSize = Number(siteSettings.page_size) || 12;
|
||||
|
||||
const { data, error, total } = await getPageData({
|
||||
slug_list: slugArr,
|
||||
page: 1,
|
||||
page_size: pageSize,
|
||||
downloadFiles: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
notFound();
|
||||
}
|
||||
const bannerComponentName = 'Banner-' + baseSlug;
|
||||
const categoryComponentName = 'Category-' + baseSlug;
|
||||
if (Array.isArray(data)) {
|
||||
const currentPath = '/' + slugArr.join('/');
|
||||
const listColumns = 4;
|
||||
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]">
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<DynamicListPage
|
||||
initialItems={data}
|
||||
slugArr={slugArr}
|
||||
basePath={currentPath}
|
||||
columns={listColumns}
|
||||
pageSize={pageSize}
|
||||
totalItems={total}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
@ -1468,6 +1468,8 @@ mark.doc {
|
||||
@apply inline-block;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
|
||||
background-clip: text;
|
||||
}
|
||||
.text-gradient em {
|
||||
@apply tracking-[normal] px-[0.05em];
|
||||
@ -1493,6 +1495,56 @@ mark.doc {
|
||||
.text-gradient.text-line.gradient-7:before {
|
||||
@apply bg-[#0093e9];
|
||||
}
|
||||
|
||||
/* 为gradient-1到gradient-7添加渐变背景 */
|
||||
.text-gradient.gradient-1 {
|
||||
background: linear-gradient(45deg, #f857a6 0%, #ff6b9d 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.text-gradient.gradient-2 {
|
||||
background: linear-gradient(45deg, #f5b161 0%, #ffa726 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.text-gradient.gradient-3 {
|
||||
background: linear-gradient(45deg, #fbda61 0%, #ffeb3b 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.text-gradient.gradient-4 {
|
||||
background: linear-gradient(45deg, #9040db 0%, #ab47bc 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.text-gradient.gradient-5 {
|
||||
background: linear-gradient(45deg, #4158d0 0%, #667eea 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.text-gradient.gradient-6 {
|
||||
background: linear-gradient(45deg, #08aeea 0%, #2af598 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.text-gradient.gradient-7 {
|
||||
background: linear-gradient(45deg, #0093e9 0%, #80d0c7 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
|
||||
@ -91,29 +91,56 @@ export default function Presentation({ data }) {
|
||||
}, []);
|
||||
|
||||
const parseContentSlides = (data) => {
|
||||
// 移除frontmatter部分,只处理内容部分
|
||||
let content = data.content;
|
||||
|
||||
// 如果内容以---开头,说明有frontmatter,需要移除
|
||||
if (content.startsWith('---')) {
|
||||
const frontmatterEnd = content.indexOf('---', 3);
|
||||
if (frontmatterEnd !== -1) {
|
||||
content = content.substring(frontmatterEnd + 3).trim();
|
||||
}
|
||||
}
|
||||
|
||||
const slideSeparators = /^---$/gm;
|
||||
const sections = data.content.split(slideSeparators);
|
||||
const sections = content.split(slideSeparators);
|
||||
|
||||
return sections.map((section, index) => {
|
||||
const trimmedSection = section.trim();
|
||||
if (!trimmedSection) return '';
|
||||
|
||||
const backgroundMatch = trimmedSection.match(/<!-- \.slide: data-background="([^"]+)"[^>]* -->/);
|
||||
// 匹配背景和类属性
|
||||
const slideCommentMatch = trimmedSection.match(/<!-- \.slide: ([^>]+) -->/);
|
||||
let backgroundAttr = '';
|
||||
let contentWithoutBackground = trimmedSection;
|
||||
let classAttr = '';
|
||||
let contentWithoutComment = trimmedSection;
|
||||
|
||||
if (backgroundMatch) {
|
||||
backgroundAttr = ` data-background="${backgroundMatch[1]}"`;
|
||||
contentWithoutBackground = trimmedSection.replace(/<!-- \.slide: data-background="[^"]+"[^>]* -->/g, '');
|
||||
if (slideCommentMatch) {
|
||||
const slideAttrs = slideCommentMatch[1];
|
||||
|
||||
// 提取data-background属性
|
||||
const backgroundMatch = slideAttrs.match(/data-background="([^"]+)"/);
|
||||
if (backgroundMatch) {
|
||||
backgroundAttr = ` data-background="${backgroundMatch[1]}"`;
|
||||
}
|
||||
|
||||
// 提取class属性
|
||||
const classMatch = slideAttrs.match(/class="([^"]+)"/);
|
||||
if (classMatch) {
|
||||
classAttr = ` class="${classMatch[1]}"`;
|
||||
}
|
||||
|
||||
// 移除整个slide注释
|
||||
contentWithoutComment = trimmedSection.replace(/<!-- \.slide: [^>]+ -->/g, '');
|
||||
}
|
||||
|
||||
const htmlContent = marked(contentWithoutBackground);
|
||||
const htmlContent = marked(contentWithoutComment);
|
||||
|
||||
if (index === 0 && !htmlContent.includes('<h1>') && data.title) {
|
||||
return `<section${backgroundAttr}><h1>${data.title}</h1>${htmlContent}</section>`;
|
||||
return `<section${backgroundAttr}${classAttr}><h1>${data.title}</h1>${htmlContent}</section>`;
|
||||
}
|
||||
|
||||
return `<section${backgroundAttr}>${htmlContent}</section>`;
|
||||
return `<section${backgroundAttr}${classAttr}>${htmlContent}</section>`;
|
||||
}).join('');
|
||||
};
|
||||
|
||||
|
||||
@ -87,22 +87,7 @@ section.has-dark-background, section.has-dark-background h1, section.has-dark-ba
|
||||
/*********************************************
|
||||
* HEADERS
|
||||
*********************************************/
|
||||
.reveal h1,
|
||||
.reveal h2,
|
||||
.reveal h3,
|
||||
.reveal h4,
|
||||
.reveal h5,
|
||||
.reveal h6 {
|
||||
margin: var(--r-heading-margin);
|
||||
color: var(--r-heading-color);
|
||||
font-family: var(--r-heading-font);
|
||||
font-weight: var(--r-heading-font-weight);
|
||||
line-height: var(--r-heading-line-height);
|
||||
letter-spacing: var(--r-heading-letter-spacing);
|
||||
text-transform: var(--r-heading-text-transform);
|
||||
text-shadow: var(--r-heading-text-shadow);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
|
||||
.reveal h1 {
|
||||
font-size: var(--r-heading1-size);
|
||||
@ -376,4 +361,151 @@ section.has-dark-background, section.has-dark-background h1, section.has-dark-ba
|
||||
.backgrounds {
|
||||
background-color: var(--r-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
/*********************************************
|
||||
* 字体颜色类
|
||||
*********************************************/
|
||||
/* 白色字体 - 适用于深色背景 */
|
||||
.text-white {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* 黑色字体 - 适用于浅色背景 */
|
||||
.text-black {
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
/* 深灰色字体 */
|
||||
.text-gray-dark {
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
/* 中灰色字体 */
|
||||
.text-gray {
|
||||
color: #666666 !important;
|
||||
}
|
||||
|
||||
/* 浅灰色字体 */
|
||||
.text-gray-light {
|
||||
color: #999999 !important;
|
||||
}
|
||||
|
||||
/* 红色字体 */
|
||||
.text-red {
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
/* 绿色字体 */
|
||||
.text-green {
|
||||
color: #28a745 !important;
|
||||
}
|
||||
|
||||
/* 蓝色字体 */
|
||||
.text-blue {
|
||||
color: #007bff !important;
|
||||
}
|
||||
|
||||
/* 黄色字体 */
|
||||
.text-yellow {
|
||||
color: #ffc107 !important;
|
||||
}
|
||||
|
||||
/* 橙色字体 */
|
||||
.text-orange {
|
||||
color: #fd7e14 !important;
|
||||
}
|
||||
|
||||
/* 紫色字体 */
|
||||
.text-purple {
|
||||
color: #6f42c1 !important;
|
||||
}
|
||||
|
||||
/* 青色字体 */
|
||||
.text-cyan {
|
||||
color: #17a2b8 !important;
|
||||
}
|
||||
|
||||
/* 粉色字体 */
|
||||
.text-pink {
|
||||
color: #e83e8c !important;
|
||||
}
|
||||
|
||||
/* 棕色字体 */
|
||||
.text-brown {
|
||||
color: #795548 !important;
|
||||
}
|
||||
|
||||
/* 金色字体 */
|
||||
.text-gold {
|
||||
color: #ffd700 !important;
|
||||
}
|
||||
|
||||
/* 银色字体 */
|
||||
.text-silver {
|
||||
color: #c0c0c0 !important;
|
||||
}
|
||||
|
||||
/* 半透明白色 - 适用于深色背景的次要文字 */
|
||||
.text-white-75 {
|
||||
color: rgba(255, 255, 255, 0.75) !important;
|
||||
}
|
||||
|
||||
.text-white-50 {
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
/* 半透明黑色 - 适用于浅色背景的次要文字 */
|
||||
.text-black-75 {
|
||||
color: rgba(0, 0, 0, 0.75) !important;
|
||||
}
|
||||
|
||||
.text-black-50 {
|
||||
color: rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
/* 渐变文字效果 */
|
||||
.text-gradient {
|
||||
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.text-gradient-primary {
|
||||
background: linear-gradient(45deg, #007bff 0%, #0056b3 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.text-gradient-success {
|
||||
background: linear-gradient(45deg, #28a745 0%, #1e7e34 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* 文字阴影效果 - 增强可读性 */
|
||||
.text-shadow {
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.text-shadow-light {
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.text-shadow-white {
|
||||
text-shadow: 2px 2px 4px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 高对比度文字类 */
|
||||
.text-high-contrast {
|
||||
color: #000000 !important;
|
||||
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.text-high-contrast-inverse {
|
||||
color: #ffffff !important;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
346
components/products/ProductImageGallery.jsx
Normal file
346
components/products/ProductImageGallery.jsx
Normal file
@ -0,0 +1,346 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import { Navigation, Pagination, Thumbs, FreeMode } from "swiper/modules";
|
||||
|
||||
import "swiper/css";
|
||||
import "swiper/css/navigation";
|
||||
import "swiper/css/pagination";
|
||||
import "swiper/css/thumbs";
|
||||
import "swiper/css/free-mode";
|
||||
|
||||
const ProductImageGallery = ({ data }) => {
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||
const [thumbsSwiper, setThumbsSwiper] = useState(null);
|
||||
const thumbsSwiperRef = useRef(null);
|
||||
|
||||
if (!data?.attachments || data.attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mainImageIndex = useMemo(() => {
|
||||
if (!data.image) return 0;
|
||||
|
||||
const mainImageIndex = data.attachments.findIndex(
|
||||
attachment => attachment.file_url === data.image
|
||||
);
|
||||
|
||||
return mainImageIndex >= 0 ? mainImageIndex : 0;
|
||||
}, [data.image, data.attachments]);
|
||||
|
||||
const currentImage = useMemo(() => {
|
||||
return data.attachments[currentImageIndex]?.file_url || data.attachments[0]?.file_url;
|
||||
}, [data.attachments, currentImageIndex]);
|
||||
|
||||
const scrollThumbnailToIndex = useCallback((index) => {
|
||||
if (thumbsSwiperRef.current?.swiper) {
|
||||
try {
|
||||
const swiper = thumbsSwiperRef.current.swiper;
|
||||
|
||||
if (index === 0) {
|
||||
swiper.scrollTo(0, 300);
|
||||
} else {
|
||||
swiper.slideTo(index, 300);
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const changeMainImage = useCallback((direction) => {
|
||||
if (data.attachments.length <= 1) return;
|
||||
|
||||
let newIndex;
|
||||
if (direction === 'next') {
|
||||
newIndex = (currentImageIndex + 1) % data.attachments.length;
|
||||
} else {
|
||||
newIndex = (currentImageIndex - 1 + data.attachments.length) % data.attachments.length;
|
||||
}
|
||||
|
||||
setCurrentImageIndex(newIndex);
|
||||
|
||||
setTimeout(() => {
|
||||
scrollThumbnailToIndex(newIndex);
|
||||
}, 100);
|
||||
}, [currentImageIndex, data.attachments.length, scrollThumbnailToIndex]);
|
||||
|
||||
const changeMainImageByIndex = useCallback((index) => {
|
||||
setCurrentImageIndex(index);
|
||||
|
||||
setTimeout(() => {
|
||||
scrollThumbnailToIndex(index);
|
||||
}, 100);
|
||||
}, [scrollThumbnailToIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentImageIndex(mainImageIndex);
|
||||
}, [mainImageIndex]);
|
||||
|
||||
const handleKeyDown = useCallback((event) => {
|
||||
if (data.attachments.length <= 1) return;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault();
|
||||
changeMainImage('prev');
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
event.preventDefault();
|
||||
changeMainImage('next');
|
||||
break;
|
||||
case 'Home':
|
||||
event.preventDefault();
|
||||
setCurrentImageIndex(0);
|
||||
setTimeout(() => scrollThumbnailToIndex(0), 100);
|
||||
break;
|
||||
case 'End':
|
||||
event.preventDefault();
|
||||
setCurrentImageIndex(data.attachments.length - 1);
|
||||
setTimeout(() => scrollThumbnailToIndex(data.attachments.length - 1), 100);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [data.attachments.length, changeMainImage, scrollThumbnailToIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [handleKeyDown]);
|
||||
|
||||
const thumbnailSlides = useMemo(() => {
|
||||
return data.attachments.map((attachment, index) => (
|
||||
<SwiperSlide
|
||||
key={index}
|
||||
className="!w-16 !h-16"
|
||||
onClick={() => changeMainImageByIndex(index)}
|
||||
>
|
||||
<div
|
||||
className={`w-full h-full rounded-lg overflow-hidden cursor-pointer transition-all duration-300 hover:scale-105 focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 ${
|
||||
index === currentImageIndex
|
||||
? 'border-2'
|
||||
: 'border border-[#eee] hover:border-gray-300'
|
||||
}`}
|
||||
style={index === currentImageIndex ? { borderColor: '#1A1A1A', borderWidth: '2px' } : {}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`View image ${index + 1} / ${data.attachments.length}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
changeMainImageByIndex(index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={attachment.file_url}
|
||||
alt={`${data?.title || 'Product'} - ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gray-100 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<div className="w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
));
|
||||
}, [data.attachments, currentImageIndex, data?.title, changeMainImageByIndex]);
|
||||
|
||||
return (
|
||||
<div className="w-full md:w-120 flex justify-center md:justify-start mb-4 md:mb-0 max-w-full">
|
||||
<div className="relative w-full max-w-[320px] md:max-w-full">
|
||||
<div className="relative overflow-hidden rounded-xl shadow-lg mb-6 group/main-image">
|
||||
<img
|
||||
src={currentImage}
|
||||
alt={data?.title || 'Product Image'}
|
||||
className="w-full max-w-[320px] md:max-w-full object-contain"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{data.attachments.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
className="absolute left-2 top-1/2 transform -translate-y-1/2 w-12 h-12 bg-white/40 backdrop-blur-md rounded-full border-0 flex items-center justify-center shadow-[0_8px_32px_rgba(0,0,0,0.08)] hover:bg-white/60 hover:shadow-[0_12px_40px_rgba(0,0,0,0.12)] transition-all duration-500 ease-out z-10 focus:outline-none focus:ring-2 focus:ring-white/30 focus:ring-offset-2 focus:ring-offset-transparent opacity-0 group-hover/main-image:opacity-100 scale-90 group-hover/main-image:scale-100"
|
||||
onClick={() => changeMainImage('prev')}
|
||||
aria-label={`Previous image (${currentImageIndex === 0 ? data.attachments.length : currentImageIndex} / ${data.attachments.length})`}
|
||||
title="Previous image (←)"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 transition-all duration-500 ease-out hover:scale-110"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="absolute right-2 top-1/2 transform -translate-y-1/2 w-12 h-12 bg-white/40 backdrop-blur-md rounded-full border-0 flex items-center justify-center shadow-[0_8px_32px_rgba(0,0,0,0.08)] hover:bg-white/60 hover:shadow-[0_12px_40px_rgba(0,0,0,0.12)] transition-all duration-500 ease-out z-10 focus:outline-none focus:ring-2 focus:ring-white/30 focus:ring-offset-2 focus:ring-offset-transparent opacity-0 group-hover/main-image:opacity-100 scale-90 group-hover/main-image:scale-100"
|
||||
onClick={() => changeMainImage('next')}
|
||||
aria-label={`Next image (${currentImageIndex === data.attachments.length - 1 ? 1 : currentImageIndex + 2} / ${data.attachments.length})`}
|
||||
title="Next image (→)"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 transition-all duration-500 ease-out hover:scale-110"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{data.attachments.length > 1 && (
|
||||
<div className="relative">
|
||||
<Swiper
|
||||
ref={thumbsSwiperRef}
|
||||
onSwiper={setThumbsSwiper}
|
||||
spaceBetween={16}
|
||||
slidesPerView="auto"
|
||||
freeMode={true}
|
||||
watchSlidesProgress={true}
|
||||
modules={[FreeMode, Navigation, Pagination, Thumbs]}
|
||||
className="thumbs-swiper"
|
||||
grabCursor={true}
|
||||
resistance={true}
|
||||
resistanceRatio={0.85}
|
||||
>
|
||||
{thumbnailSlides}
|
||||
</Swiper>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.thumbs-swiper .swiper-slide {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
width: 64px !important;
|
||||
margin-right: 16px !important;
|
||||
}
|
||||
|
||||
.thumbs-swiper .swiper-slide:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.thumbs-swiper .swiper-slide:last-child {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.thumbs-swiper .swiper-slide .border-2 {
|
||||
}
|
||||
|
||||
.backdrop-blur-sm {
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.group:hover .backdrop-blur-sm {
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.thumbs-swiper .swiper-slide {
|
||||
width: 4rem !important;
|
||||
height: 4rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbs-swiper .swiper-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.thumbs-swiper {
|
||||
touch-action: pan-y pinch-zoom;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.thumbs-swiper .swiper-slide {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.focus-within\\:ring-2:focus-within {
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
}
|
||||
|
||||
.focus-within\\:ring-blue-500:focus-within {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus-within\\:ring-offset-2:focus-within {
|
||||
--tw-ring-offset-width: 2px;
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.thumbs-swiper .swiper-slide:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.group\/main-image button {
|
||||
border-radius: 50% !important;
|
||||
-webkit-border-radius: 50% !important;
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
.border-gray-200 {
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
.border-blue-500 {
|
||||
border-color: #0066cc;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.thumbs-swiper .swiper-slide,
|
||||
.backdrop-blur-sm {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductImageGallery;
|
||||
@ -9,8 +9,8 @@ export async function getLocalPageData(slugArr) {
|
||||
return { data: null };
|
||||
}
|
||||
|
||||
// 构建markdown文件路径
|
||||
const fileName = slugArr.join('-') + '.md';
|
||||
// 构建markdown文件路径 - 只使用最后一个slug作为文件名
|
||||
const fileName = slugArr[slugArr.length - 1] + '.md';
|
||||
const filePath = path.join(process.cwd(), 'data', 'presentation', fileName);
|
||||
|
||||
// 检查文件是否存在
|
||||
@ -32,7 +32,6 @@ export async function getLocalPageData(slugArr) {
|
||||
|
||||
return { data: pageData };
|
||||
} catch (error) {
|
||||
console.error("Error reading local markdown file:", error);
|
||||
return { data: null };
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
---
|
||||
theme: jingrow
|
||||
backgroundTransition: slide
|
||||
transition: slide
|
||||
---
|
||||
|
||||
# <span class="jingrow-icon"></span> Jingrow
|
||||
## 一站式通用企业数字化平台
|
||||
### 下一代智能工作平台
|
||||
*让企业数字化变得简单而强大*
|
||||
<!-- .slide: data-background="radial-gradient(circle at 20% 20%, #093028 0%, transparent 50%), radial-gradient(circle at 80% 20%, #237A57 0%, transparent 50%), radial-gradient(circle at 50% 80%, #43C6AC 0%, transparent 50%)" class="text-white text-high-contrast-inverse" -->
|
||||
|
||||
# <span class="jingrow-icon"></span> Jingrow
|
||||
## 一站式通用数字化平台
|
||||
##### AI Agent智能体、可视化工作流、零代码可视化数据建模、自动化任务调度、实时协作与权限管理
|
||||
###### 注册即用/免部署/零运维
|
||||
*一种新的面向未来的工作方式*
|
||||
---
|
||||
|
||||
<!-- .slide: data-background="linear-gradient(135deg, #f093fb 0%, #f5576c 100%)" class="text-center" -->
|
||||
@ -326,59 +326,6 @@ Jingrow 采用独特的"页面化"管理理念,将所有业务对象统一抽
|
||||
|
||||
# 🎉 感谢聆听
|
||||
|
||||
## Jingrow - 让企业数字化变得简单而强大
|
||||
|
||||
### <span class="jingrow-icon"></span> 开启您的数字化之旅
|
||||
|
||||
---
|
||||
|
||||
<!-- .slide: data-background="linear-gradient(135deg, #fa709a 0%, #fee140 100%)" class="text-center" -->
|
||||
|
||||
## 📚 附录:Jingrow 系统开发指南
|
||||
|
||||
### 🔄 关键词替换规则
|
||||
|
||||
| 原始关键词 | 替换为关键词 |
|
||||
|---|----|
|
||||
| `frappe` | `jingrow` |
|
||||
| `doctype` | `pagetype` |
|
||||
| `DocType` | `PageType` |
|
||||
| `doc` | `pg` |
|
||||
| `get_doc` | `get_pg` |
|
||||
|
||||
---
|
||||
|
||||
<!-- .slide: data-background="linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)" class="text-center" -->
|
||||
|
||||
### ⚠️ 错误日志与异常处理
|
||||
|
||||
```python
|
||||
# 打印错误日志
|
||||
jingrow.log_error(title, message)
|
||||
|
||||
# 抛出异常
|
||||
jingrow.throw(title, message)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- .slide: data-background="linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)" class="text-center" -->
|
||||
|
||||
### 🔌 Jingrow 调用外部API开发规范
|
||||
|
||||
- **🔑 统一认证**:使用 `get_jingrow_api_headers` 发送认证
|
||||
- **📝 函数命名**:`call_<模块名>_api`
|
||||
- **🛡️ 安全校验**:包含鉴权校验、余额不足判断、错误提示弹窗
|
||||
- **🔄 完整闭环**:保证脚本具备完整闭环能力,提升用户体验
|
||||
|
||||
---
|
||||
|
||||
<!-- .slide: data-background="linear-gradient(135deg, #667eea 0%, #764ba2 100%)" class="text-center" -->
|
||||
|
||||
### 💡 最佳实践建议
|
||||
|
||||
- **🏗️ 架构遵循**:遵循系统架构与开发风格
|
||||
- **⚡ 简洁高效**:代码需简洁高效,避免重复
|
||||
- **📁 统一管理**:通用函数应统一存放于合适路径下的 `utils.py` 文件
|
||||
- **🔧 便于维护**:所有模块应便于维护与升级
|
||||
## Jingrow - Jingrow帮助您快速构建从内部管理到对外门户的一站式数字化解决方案
|
||||
|
||||
### <span class="jingrow-icon"></span> 开启您的数字化之旅
|
||||
Loading…
x
Reference in New Issue
Block a user