From 7a2bdfba56a96405e6499275d284c474384f2cbf Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 7 Jan 2026 15:02:44 +0330 Subject: [PATCH] link to another pager --- public/viewer-data.json | 117 +++++++++++++++++ src/pages/viewer/components/BookPage.tsx | 110 ++++++++++++---- src/pages/viewer/components/BookViewer.tsx | 142 ++++++++++++++++----- 3 files changed, 312 insertions(+), 57 deletions(-) diff --git a/public/viewer-data.json b/public/viewer-data.json index fc87a17..97ff8e1 100644 --- a/public/viewer-data.json +++ b/public/viewer-data.json @@ -128,6 +128,71 @@ "fill": "#3b82f6", "opacity": 100 }, + { + "id": "link-next-page", + "type": "link", + "x": 100, + "y": 500, + "width": 150, + "height": 50, + "text": "صفحه بعدی", + "fontSize": 18, + "linkUrl": "page://next", + "fill": "#3b82f6", + "opacity": 100 + }, + { + "id": "link-prev-page", + "type": "link", + "x": 280, + "y": 500, + "width": 150, + "height": 50, + "text": "صفحه قبلی", + "fontSize": 18, + "linkUrl": "page://prev", + "fill": "#3b82f6", + "opacity": 100 + }, + { + "id": "link-first-page", + "type": "link", + "x": 460, + "y": 500, + "width": 150, + "height": 50, + "text": "اولین صفحه", + "fontSize": 18, + "linkUrl": "page://first", + "fill": "#3b82f6", + "opacity": 100 + }, + { + "id": "link-last-page", + "type": "link", + "x": 100, + "y": 570, + "width": 150, + "height": 50, + "text": "آخرین صفحه", + "fontSize": 18, + "linkUrl": "page://last", + "fill": "#3b82f6", + "opacity": 100 + }, + { + "id": "link-to-page", + "type": "link", + "x": 280, + "y": 570, + "width": 150, + "height": 50, + "text": "صفحه", + "fontSize": 18, + "linkUrl": "page://3", + "fill": "#3b82f6", + "opacity": 100 + }, { "id": "image-2", "type": "image", @@ -328,6 +393,58 @@ "strokeWidth": 2, "shapeType": "abstract", "opacity": 100 + }, + { + "id": "link-nav-next-3", + "type": "link", + "x": 100, + "y": 600, + "width": 140, + "height": 45, + "text": "صفحه بعدی", + "fontSize": 16, + "linkUrl": "page://next", + "fill": "#3b82f6", + "opacity": 100 + }, + { + "id": "link-nav-prev-3", + "type": "link", + "x": 260, + "y": 600, + "width": 140, + "height": 45, + "text": "صفحه قبلی", + "fontSize": 16, + "linkUrl": "page://prev", + "fill": "#3b82f6", + "opacity": 100 + }, + { + "id": "link-nav-first-3", + "type": "link", + "x": 420, + "y": 600, + "width": 140, + "height": 45, + "text": "اولین صفحه", + "fontSize": 16, + "linkUrl": "page://first", + "fill": "#3b82f6", + "opacity": 100 + }, + { + "id": "link-nav-last-3", + "type": "link", + "x": 580, + "y": 600, + "width": 140, + "height": 45, + "text": "آخرین صفحه", + "fontSize": 16, + "linkUrl": "page://last", + "fill": "#3b82f6", + "opacity": 100 } ] }, diff --git a/src/pages/viewer/components/BookPage.tsx b/src/pages/viewer/components/BookPage.tsx index e8bdddb..bb56305 100644 --- a/src/pages/viewer/components/BookPage.tsx +++ b/src/pages/viewer/components/BookPage.tsx @@ -5,6 +5,7 @@ import type { EditorObject } from '@/pages/editor/store/editorStore'; type BookPageProps = { page: PageData; scale?: number; + onLinkClick?: (linkUrl: string) => void; }; /** @@ -14,7 +15,7 @@ type BookPageProps = { * این کامپوننت به عنوان child برای HTMLFlipBook استفاده می‌شود * نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند */ -const BookPage = forwardRef(({ page, scale = 1 }, ref) => { +const BookPage = forwardRef(({ page, scale = 1, onLinkClick }, ref) => { // تابع برای تبدیل opacity به رنگ (مثل editor) // در editor، getColorWithOpacity انتظار opacity 0-100 دارد // در dataTransformer، opacity از 0-1 به 0-100 تبدیل شده است @@ -108,33 +109,94 @@ const BookPage = forwardRef(({ page, scale = 1 }, width: obj.width ? `${obj.width * scale}px` : 'auto', height: obj.height ? `${obj.height * scale}px` : 'auto', objectFit: 'contain', + zIndex: index, + pointerEvents: 'auto', + }} + onClick={(e) => { + e.stopPropagation(); + }} + onMouseDown={(e) => { + e.stopPropagation(); + }} + onTouchStart={(e) => { + e.stopPropagation(); }} /> ); - case 'link': - return ( - - {obj.text || obj.linkUrl || ''} - - ); + case 'link': { + const isInternalLink = obj.linkUrl?.startsWith('page://'); + const linkStyle = { + ...baseStyle, + width: obj.width ? `${obj.width * scale}px` : 'auto', + height: obj.height ? `${obj.height * scale}px` : 'auto', + color: obj.fill || '#3b82f6', + fontSize: `${(obj.fontSize || 16) * scale}px`, + fontFamily: obj.fontFamily || 'irancell, sans-serif', + textDecoration: 'underline', + display: 'flex', + alignItems: 'center', + whiteSpace: 'pre-wrap' as const, + cursor: 'pointer', + }; + + if (isInternalLink) { + // لینک داخلی - از button با onClick استفاده می‌کنیم + return ( + + ); + } else { + // لینک خارجی - از با target="_blank" استفاده می‌کنیم + return ( + { + e.stopPropagation(); + }} + onMouseDown={(e) => { + e.stopPropagation(); + }} + > + {obj.text || obj.linkUrl || ''} + + ); + } + } case 'rectangle': { if (obj.shapeType === 'circle') { diff --git a/src/pages/viewer/components/BookViewer.tsx b/src/pages/viewer/components/BookViewer.tsx index 219d860..d438e4e 100644 --- a/src/pages/viewer/components/BookViewer.tsx +++ b/src/pages/viewer/components/BookViewer.tsx @@ -11,6 +11,7 @@ type BookViewerProps = { interface PageFlipAPI { flipNext: () => void; flipPrev: () => void; + turnToPage: (page: number) => void; } interface FlipBookInstance { @@ -43,14 +44,14 @@ const BookViewer: FC = ({ pages }) => { // react-pageflip شماره صفحه را به صورت 0-based برمی‌گرداند // اما باید مطمئن شویم که در محدوده معتبر است let pageNum = e.data; - + // اگر pageNum منفی است یا بزرگتر از تعداد صفحات، آن را محدود می‌کنیم if (pageNum < 0) { pageNum = 0; } else if (pageNum >= pages.length) { pageNum = pages.length - 1; } - + setCurrentPage(pageNum); }, [pages.length]); @@ -66,6 +67,81 @@ const BookViewer: FC = ({ pages }) => { } }, [currentPage]); + const handleLinkClick = useCallback((linkUrl: string) => { + if (linkUrl.startsWith('page://')) { + const action = linkUrl.replace('page://', ''); + const pageFlipAPI = bookRef.current?.pageFlip(); + + if (!pageFlipAPI) return; + + if (action === 'next') { + goToNextPage(); + } else if (action === 'prev') { + goToPrevPage(); + } else if (action === 'first') { + // رفتن به صفحه اول + const flipToFirst = () => { + if (currentPage > 0) { + pageFlipAPI.flipPrev(); + setTimeout(() => { + setCurrentPage(prev => { + if (prev > 1) { + flipToFirst(); + } + return prev - 1; + }); + }, 300); + } + }; + flipToFirst(); + } else if (action === 'last') { + // رفتن به صفحه آخر + const lastPageIndex = pages.length - 1; + const flipToLast = () => { + if (currentPage < lastPageIndex) { + pageFlipAPI.flipNext(); + setTimeout(() => { + setCurrentPage(prev => { + if (prev < lastPageIndex - 1) { + flipToLast(); + } + return prev + 1; + }); + }, 300); + } + }; + flipToLast(); + } else { + // شماره صفحه مشخص (مثل page://3) + const pageNum = parseInt(action, 10); + if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= pages.length) { + const targetIndex = pageNum - 1; + const diff = targetIndex - currentPage; + + const flipToTarget = (remaining: number) => { + if (remaining === 0) return; + + if (remaining > 0) { + pageFlipAPI.flipNext(); + setTimeout(() => { + setCurrentPage(prev => prev + 1); + flipToTarget(remaining - 1); + }, 300); + } else { + pageFlipAPI.flipPrev(); + setTimeout(() => { + setCurrentPage(prev => prev - 1); + flipToTarget(remaining + 1); + }, 300); + } + }; + + flipToTarget(diff); + } + } + } + }, [currentPage, pages.length, goToNextPage, goToPrevPage]); + return (
{/* نمایش کتاب */} @@ -78,39 +154,39 @@ const BookViewer: FC = ({ pages }) => { }} />
- + {pages.map((page) => ( - + ))} - +