link to another pager

This commit is contained in:
hamid zarghami
2026-01-07 15:02:44 +03:30
parent 9e1dd09097
commit 7a2bdfba56
3 changed files with 312 additions and 57 deletions
+117
View File
@@ -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
}
]
},
+86 -24
View File
@@ -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<HTMLDivElement, BookPageProps>(({ page, scale = 1 }, ref) => {
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ 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<HTMLDivElement, BookPageProps>(({ 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 (
<a
key={obj.id || index}
href={obj.linkUrl || '#'}
target="_blank"
rel="noopener noreferrer"
style={{
...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',
}}
>
{obj.text || obj.linkUrl || ''}
</a>
);
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 (
<button
key={obj.id || index}
type="button"
style={{
...linkStyle,
zIndex: 9999,
pointerEvents: 'auto',
border: 'none',
background: 'none',
padding: 0,
cursor: 'pointer',
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (obj.linkUrl && onLinkClick) {
onLinkClick(obj.linkUrl);
}
}}
onMouseDown={(e) => {
e.stopPropagation();
}}
onTouchStart={(e) => {
e.stopPropagation();
}}
>
{obj.text || obj.linkUrl || ''}
</button>
);
} else {
// لینک خارجی - از <a> با target="_blank" استفاده می‌کنیم
return (
<a
key={obj.id || index}
href={obj.linkUrl || '#'}
target="_blank"
rel="noopener noreferrer"
style={{
...linkStyle,
zIndex: 9999,
pointerEvents: 'auto',
}}
onClick={(e) => {
e.stopPropagation();
}}
onMouseDown={(e) => {
e.stopPropagation();
}}
>
{obj.text || obj.linkUrl || ''}
</a>
);
}
}
case 'rectangle': {
if (obj.shapeType === 'circle') {
+109 -33
View File
@@ -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<BookViewerProps> = ({ 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<BookViewerProps> = ({ 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 (
<div className="flex flex-col items-center gap-4 md:gap-6 w-full h-full" dir="rtl">
{/* نمایش کتاب */}
@@ -78,39 +154,39 @@ const BookViewer: FC<BookViewerProps> = ({ pages }) => {
}}
/>
<div style={{ overflow: 'hidden', isolation: 'isolate' }}>
<HTMLFlipBook
ref={bookRef}
width={397}
height={561}
size="fixed"
minWidth={397}
minHeight={561}
maxWidth={397}
maxHeight={561}
drawShadow={true}
flippingTime={800}
usePortrait={false}
startPage={0}
autoSize={false}
maxShadowOpacity={0.5}
showCover={true}
mobileScrollSupport={true}
clickEventForward={true}
useMouseEvents={true}
swipeDistance={30}
startZIndex={1}
showPageCorners={true}
disableFlipByClick={false}
className="flipbook-container mx-auto"
style={{
direction: 'rtl',
}}
onFlip={handlePageFlip}
>
<HTMLFlipBook
ref={bookRef}
width={397}
height={561}
size="fixed"
minWidth={397}
minHeight={561}
maxWidth={397}
maxHeight={561}
drawShadow={true}
flippingTime={800}
usePortrait={false}
startPage={0}
autoSize={false}
maxShadowOpacity={0.5}
showCover={true}
mobileScrollSupport={true}
clickEventForward={true}
useMouseEvents={true}
swipeDistance={30}
startZIndex={1}
showPageCorners={true}
disableFlipByClick={true}
className="flipbook-container mx-auto"
style={{
direction: 'rtl',
}}
onFlip={handlePageFlip}
>
{pages.map((page) => (
<BookPage key={page.id} page={page} scale={0.5} />
<BookPage key={page.id} page={page} scale={0.5} onLinkClick={handleLinkClick} />
))}
</HTMLFlipBook>
</HTMLFlipBook>
</div>
</div>