base load viewer

This commit is contained in:
hamid zarghami
2026-01-07 09:25:59 +03:30
parent a4861eab9b
commit 9479a3425b
9 changed files with 1109 additions and 83 deletions
+322 -44
View File
@@ -1,8 +1,10 @@
import { forwardRef } from 'react';
import { type PageData } from '../types';
import type { EditorObject } from '@/pages/editor/store/editorStore';
type BookPageProps = {
page: PageData;
scale?: number;
};
/**
@@ -12,7 +14,323 @@ type BookPageProps = {
* این کامپوننت به عنوان child برای HTMLFlipBook استفاده می‌شود
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
*/
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page }, ref) => {
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 }, ref) => {
const renderObject = (obj: EditorObject, index: number) => {
const baseStyle: React.CSSProperties = {
position: 'absolute',
left: `${(obj.x || 0) * scale}px`,
top: `${(obj.y || 0) * scale}px`,
opacity: obj.opacity ?? 1,
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
transformOrigin: 'center center',
};
if (obj.visible === false) {
return null;
}
switch (obj.type) {
case 'text':
return (
<div
key={obj.id || index}
style={{
...baseStyle,
fontSize: `${(obj.fontSize || 16) * scale}px`,
fontFamily: obj.fontFamily || 'irancell, sans-serif',
fontWeight: obj.fontWeight || 'normal',
color: obj.fill || '#000000',
whiteSpace: 'pre-wrap',
letterSpacing: obj.letterSpacing ? `${obj.letterSpacing * scale}px` : undefined,
}}
>
{obj.text || ''}
</div>
);
case 'image':
return (
<img
key={obj.id || index}
src={obj.imageUrl || ''}
alt=""
style={{
...baseStyle,
width: obj.width ? `${obj.width * scale}px` : 'auto',
height: obj.height ? `${obj.height * scale}px` : 'auto',
objectFit: 'contain',
}}
/>
);
case 'video':
return (
<video
key={obj.id || index}
src={obj.videoUrl || ''}
controls
style={{
...baseStyle,
width: obj.width ? `${obj.width * scale}px` : 'auto',
height: obj.height ? `${obj.height * scale}px` : 'auto',
objectFit: 'contain',
}}
/>
);
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 'rectangle':
if (obj.shapeType === 'circle') {
// در editor، circle با مرکز در x, y رندر می‌شود (radius = width/2)
// اما در viewer-data.json ممکن است x, y به عنوان گوشه بالا-چپ bounding box باشد
// برای سازگاری با editor، فرض می‌کنیم x, y مرکز است
const radius = ((obj.width || 100) / 2) * scale;
const centerX = (obj.x || 0) * scale;
const centerY = (obj.y || 0) * scale;
return (
<div
key={obj.id || index}
style={{
position: 'absolute',
left: `${centerX - radius}px`,
top: `${centerY - radius}px`,
width: `${radius * 2}px`,
height: `${radius * 2}px`,
borderRadius: '50%',
backgroundColor: obj.fill || 'transparent',
border: obj.stroke
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
: 'none',
opacity: obj.opacity ?? 1,
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
transformOrigin: 'center center',
}}
/>
);
}
if (obj.shapeType === 'triangle') {
// در editor، triangle با مرکز در x + width/2, y + height/2 رندر می‌شود
const width = (obj.width || 100) * scale;
const height = (obj.height || 100) * scale;
const radius = Math.min(width, height) / 2;
const centerX = (obj.x || 0) * scale + width / 2;
const centerY = (obj.y || 0) * scale + height / 2;
return (
<div
key={obj.id || index}
style={{
position: 'absolute',
left: `${centerX - radius}px`,
top: `${centerY - radius}px`,
width: 0,
height: 0,
borderLeft: `${radius}px solid transparent`,
borderRight: `${radius}px solid transparent`,
borderBottom: `${radius * 2}px solid ${obj.fill || '#000000'}`,
backgroundColor: 'transparent',
borderTop: 'none',
opacity: obj.opacity ?? 1,
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
transformOrigin: 'center center',
}}
/>
);
}
if (obj.shapeType === 'abstract') {
// در editor، abstract (star) با مرکز در x + width/2, y + height/2 رندر می‌شود
const width = (obj.width || 100) * scale;
const height = (obj.height || 100) * scale;
const baseRadius = Math.min(width, height) / 2;
const abstractScale = 0.85;
const radius = baseRadius * abstractScale;
const centerX = (obj.x || 0) * scale + width / 2;
const centerY = (obj.y || 0) * scale + height / 2;
return (
<div
key={obj.id || index}
style={{
position: 'absolute',
left: `${centerX - radius}px`,
top: `${centerY - radius}px`,
width: `${radius * 2}px`,
height: `${radius * 2}px`,
clipPath: 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)',
backgroundColor: obj.fill || 'transparent',
border: obj.stroke
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
: 'none',
opacity: obj.opacity ?? 1,
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
transformOrigin: 'center center',
}}
/>
);
}
// Rectangle عادی
const shapeStyle: React.CSSProperties = {
...baseStyle,
width: `${(obj.width || 100) * scale}px`,
height: `${(obj.height || 100) * scale}px`,
backgroundColor: obj.fill || 'transparent',
border: obj.stroke
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
: 'none',
};
return <div key={obj.id || index} style={shapeStyle} />;
case 'line': {
// در EditorObject، width و height به عنوان مختصات انتهایی استفاده می‌شوند
// در Konva Line، points به صورت [0, 0, endX - startX, endY - startY] است
const startX = (obj.x || 0) * scale;
const startY = (obj.y || 0) * scale;
const endX = (obj.width ?? obj.x ?? 0) * scale;
const endY = (obj.height ?? obj.y ?? 0) * scale;
const dx = endX - startX;
const dy = endY - startY;
const lineLength = Math.sqrt(dx * dx + dy * dy);
const lineAngle = Math.atan2(dy, dx) * (180 / Math.PI);
return (
<div
key={obj.id || index}
style={{
position: 'absolute',
left: `${startX}px`,
top: `${startY}px`,
width: `${lineLength}px`,
height: `${(obj.strokeWidth || 2) * scale}px`,
backgroundColor: obj.stroke || '#000000',
opacity: obj.opacity ?? 1,
transform: `rotate(${lineAngle}deg)`,
transformOrigin: 'left center',
}}
/>
);
}
case 'arrow': {
// در EditorObject، width و height به عنوان مختصات انتهایی استفاده می‌شوند
// در Konva Arrow، points به صورت [0, 0, endX - startX, endY - startY] است
const startX = (obj.x || 0) * scale;
const startY = (obj.y || 0) * scale;
const endX = (obj.width ?? obj.x ?? 0) * scale;
const endY = (obj.height ?? obj.y ?? 0) * scale;
const dx = endX - startX;
const dy = endY - startY;
const arrowLength = Math.sqrt(dx * dx + dy * dy);
const arrowAngle = Math.atan2(dy, dx) * (180 / Math.PI);
const strokeWidth = (obj.strokeWidth || 4) * scale;
return (
<div
key={obj.id || index}
style={{
position: 'absolute',
left: `${startX}px`,
top: `${startY}px`,
width: `${arrowLength}px`,
height: `${strokeWidth}px`,
backgroundColor: obj.stroke || '#000000',
opacity: obj.opacity ?? 1,
transform: `rotate(${arrowAngle}deg)`,
transformOrigin: 'left center',
}}
>
<div
style={{
position: 'absolute',
right: `-${strokeWidth * 2}px`,
top: '50%',
transform: 'translateY(-50%)',
width: 0,
height: 0,
borderTop: `${strokeWidth * 2}px solid transparent`,
borderBottom: `${strokeWidth * 2}px solid transparent`,
borderLeft: `${strokeWidth * 2}px solid ${obj.stroke || '#000000'}`,
}}
/>
</div>
);
}
case 'grid':
if (!obj.tableData) return null;
const { rows, cols, cells, cellWidth, cellHeight, stroke, strokeWidth } =
obj.tableData;
return (
<div
key={obj.id || index}
style={{
...baseStyle,
display: 'grid',
gridTemplateRows: `repeat(${rows}, ${cellHeight * scale}px)`,
gridTemplateColumns: `repeat(${cols}, ${cellWidth * scale}px)`,
border: stroke ? `${(strokeWidth || 1) * scale}px solid ${stroke}` : 'none',
}}
>
{Array.from({ length: rows }).map((_, rowIndex) =>
Array.from({ length: cols }).map((_, colIndex) => {
const cellId = `cell-${rowIndex}-${colIndex}`;
const cell = cells[cellId];
return (
<div
key={cellId}
style={{
border: stroke ? `${(strokeWidth || 1) * scale}px solid ${stroke}` : `${1 * scale}px solid #ccc`,
backgroundColor: cell?.background || '#ffffff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: `${4 * scale}px`,
fontSize: `${14 * scale}px`,
}}
>
{cell?.text || ''}
</div>
);
})
)}
</div>
);
default:
return null;
}
};
return (
<div
ref={ref}
@@ -22,52 +340,12 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page }, ref) => {
position: 'relative',
backgroundColor: '#ffffff',
overflow: 'hidden',
padding: '40px',
boxSizing: 'border-box',
width: '100%',
height: '100%',
width: `${794 * scale}px`,
height: `${1123 * scale}px`,
}}
>
{page.elements.map((element, index) => {
if (element.type === 'text') {
return (
<div
key={index}
style={{
position: 'absolute',
left: `${element.x}px`,
top: `${element.y}px`,
fontSize: `${element.fontSize}px`,
color: element.color,
fontFamily: 'inherit',
whiteSpace: 'pre-wrap',
}}
>
{element.text}
</div>
);
}
if (element.type === 'image') {
return (
<img
key={index}
src={element.src}
alt=""
style={{
position: 'absolute',
left: `${element.x}px`,
top: `${element.y}px`,
width: `${element.width}px`,
height: `${element.height}px`,
objectFit: 'contain',
}}
/>
);
}
return null;
})}
{page.elements.map((element, index) => renderObject(element, index))}
</div>
);
});