previewer
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
import { type FC, useMemo } from 'react'
|
||||||
|
import BookPage from '@/pages/viewer/components/BookPage'
|
||||||
|
import { transformViewerDataToPages } from '@/pages/viewer/utils/dataTransformer'
|
||||||
|
import type { CatalogItemType } from '../types/Types'
|
||||||
|
import type { PageData } from '@/pages/viewer/types'
|
||||||
|
|
||||||
|
const PREVIEW_WIDTH = 64
|
||||||
|
const PREVIEW_HEIGHT = 89
|
||||||
|
const PAGE_BASE_WIDTH = 794
|
||||||
|
const PAGE_BASE_HEIGHT = 1123
|
||||||
|
|
||||||
|
const PREVIEW_SCALE = Math.min(
|
||||||
|
PREVIEW_WIDTH / PAGE_BASE_WIDTH,
|
||||||
|
PREVIEW_HEIGHT / PAGE_BASE_HEIGHT
|
||||||
|
)
|
||||||
|
|
||||||
|
function getFirstPageFromContent(content: string | undefined): PageData | null {
|
||||||
|
if (!content) return null
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(content)
|
||||||
|
const pagesArray = Array.isArray(parsed) ? parsed : (parsed?.pages ?? [])
|
||||||
|
const transformed = transformViewerDataToPages({ pages: pagesArray })
|
||||||
|
return transformed[0] ?? null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
item: CatalogItemType
|
||||||
|
}
|
||||||
|
|
||||||
|
const CatalogPreview: FC<Props> = ({ item }) => {
|
||||||
|
const firstPage = useMemo(
|
||||||
|
() => getFirstPageFromContent(item.content),
|
||||||
|
[item.content]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!firstPage) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='bg-gray-200 rounded-lg flex items-center justify-center shrink-0'
|
||||||
|
style={{ width: PREVIEW_WIDTH, height: PREVIEW_HEIGHT }}
|
||||||
|
>
|
||||||
|
<span className='text-[10px] text-gray-400'>بدون پیشنمایش</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='rounded-lg overflow-hidden shrink-0 bg-white border border-gray-200 relative'
|
||||||
|
style={{ width: PREVIEW_WIDTH, height: PREVIEW_HEIGHT }}
|
||||||
|
dir='rtl'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='absolute'
|
||||||
|
style={{
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
transform: `scale(${PREVIEW_SCALE})`,
|
||||||
|
transformOrigin: 'top right',
|
||||||
|
width: PAGE_BASE_WIDTH,
|
||||||
|
height: PAGE_BASE_HEIGHT,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BookPage
|
||||||
|
page={firstPage}
|
||||||
|
scale={1}
|
||||||
|
pageWidth={PAGE_BASE_WIDTH}
|
||||||
|
pageHeight={PAGE_BASE_HEIGHT}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CatalogPreview
|
||||||
@@ -4,6 +4,7 @@ import type { CatalogItemType } from '../types/Types'
|
|||||||
import moment from 'moment-jalaali'
|
import moment from 'moment-jalaali'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Paths } from '@/config/Paths'
|
import { Paths } from '@/config/Paths'
|
||||||
|
import CatalogPreview from './CatalogPreview'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: CatalogItemType
|
item: CatalogItemType
|
||||||
@@ -13,7 +14,7 @@ const CatalogueItem: FC<Props> = ({ item }) => {
|
|||||||
return (
|
return (
|
||||||
<div className='bg-white p-6 rounded-3xl w-full'>
|
<div className='bg-white p-6 rounded-3xl w-full'>
|
||||||
<div className='flex gap-4 items-center'>
|
<div className='flex gap-4 items-center'>
|
||||||
<div className='w-[64px] h-[89px] bg-gray-200'></div>
|
<CatalogPreview item={item} />
|
||||||
<div>
|
<div>
|
||||||
<h6>
|
<h6>
|
||||||
{item.name}
|
{item.name}
|
||||||
@@ -23,7 +24,7 @@ const CatalogueItem: FC<Props> = ({ item }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-end gap-1 items-center'>
|
<div className='flex justify-end gap-1 items-center mt-1'>
|
||||||
<Link to={Paths.viewer + `/${item.id}`}>
|
<Link to={Paths.viewer + `/${item.id}`}>
|
||||||
<div className='size-6 bg-[#EAECF4] rounded-md flex justify-center items-center'>
|
<div className='size-6 bg-[#EAECF4] rounded-md flex justify-center items-center'>
|
||||||
<Eye size={18} color='#0047FF' />
|
<Eye size={18} color='#0047FF' />
|
||||||
|
|||||||
@@ -19,151 +19,104 @@ type BookPageProps = {
|
|||||||
*/
|
*/
|
||||||
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
||||||
({ page, scale = 1, pageWidth, pageHeight, onLinkClick }, ref) => {
|
({ page, scale = 1, pageWidth, pageHeight, onLinkClick }, ref) => {
|
||||||
// تابع برای تبدیل opacity به رنگ (مثل editor)
|
// تابع برای تبدیل opacity به رنگ (مثل editor)
|
||||||
// در editor، getColorWithOpacity انتظار opacity 0-100 دارد
|
// در editor، getColorWithOpacity انتظار opacity 0-100 دارد
|
||||||
// در dataTransformer، opacity از 0-1 به 0-100 تبدیل شده است
|
// در dataTransformer، opacity از 0-1 به 0-100 تبدیل شده است
|
||||||
// پس opacity را مستقیماً استفاده میکنیم
|
// پس opacity را مستقیماً استفاده میکنیم
|
||||||
const getColorWithOpacity = (fill?: string, opacity?: number): string => {
|
const getColorWithOpacity = (fill?: string, opacity?: number): string => {
|
||||||
if (!fill) return '#000000';
|
if (!fill) return '#000000';
|
||||||
|
|
||||||
// opacity در EditorObject به صورت 0-100 است (بعد از تبدیل در dataTransformer)
|
// opacity در EditorObject به صورت 0-100 است (بعد از تبدیل در dataTransformer)
|
||||||
// در editor getColorWithOpacity انتظار 0-100 دارد
|
// در editor getColorWithOpacity انتظار 0-100 دارد
|
||||||
const opacityValue = opacity !== undefined ? opacity : 100;
|
const opacityValue = opacity !== undefined ? opacity : 100;
|
||||||
|
|
||||||
// اگر opacity 100% باشد، رنگ را بدون تغییر برگردان (مثل editor)
|
// اگر opacity 100% باشد، رنگ را بدون تغییر برگردان (مثل editor)
|
||||||
if (opacityValue >= 100) return fill;
|
if (opacityValue >= 100) return fill;
|
||||||
|
|
||||||
const hex = fill.replace('#', '');
|
const hex = fill.replace('#', '');
|
||||||
// اگر hex کوتاهتر از 6 کاراکتر باشد، padding اضافه کن
|
// اگر hex کوتاهتر از 6 کاراکتر باشد، padding اضافه کن
|
||||||
const paddedHex = hex.length === 3
|
const paddedHex = hex.length === 3
|
||||||
? hex.split('').map(char => char + char).join('')
|
? hex.split('').map(char => char + char).join('')
|
||||||
: hex;
|
: hex;
|
||||||
|
|
||||||
const r = parseInt(paddedHex.substring(0, 2), 16);
|
const r = parseInt(paddedHex.substring(0, 2), 16);
|
||||||
const g = parseInt(paddedHex.substring(2, 4), 16);
|
const g = parseInt(paddedHex.substring(2, 4), 16);
|
||||||
const b = parseInt(paddedHex.substring(4, 6), 16);
|
const b = parseInt(paddedHex.substring(4, 6), 16);
|
||||||
// opacityValue را به alpha تبدیل کن (0-100 -> 0-1) مثل editor
|
// opacityValue را به alpha تبدیل کن (0-100 -> 0-1) مثل editor
|
||||||
const alpha = opacityValue / 100;
|
const alpha = opacityValue / 100;
|
||||||
|
|
||||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
};
|
|
||||||
|
|
||||||
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) {
|
const renderObject = (obj: EditorObject, index: number) => {
|
||||||
return null;
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
switch (obj.type) {
|
if (obj.visible === false) {
|
||||||
case 'text': {
|
return null;
|
||||||
// برای text، opacity در رنگ اعمال میشود، پس باید opacity را از baseStyle حذف کنیم
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { opacity, ...textBaseStyle } = baseStyle;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={obj.id || index}
|
|
||||||
style={{
|
|
||||||
...textBaseStyle,
|
|
||||||
fontSize: `${(obj.fontSize || 16) * scale}px`,
|
|
||||||
fontFamily: obj.fontFamily || 'irancell, sans-serif',
|
|
||||||
fontWeight: obj.fontWeight || 'normal',
|
|
||||||
color: getColorWithOpacity(obj.fill, obj.opacity),
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
letterSpacing: obj.letterSpacing ? `${obj.letterSpacing * scale}px` : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{obj.text || ''}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'image':
|
switch (obj.type) {
|
||||||
return (
|
case 'text': {
|
||||||
<img
|
// برای text، opacity در رنگ اعمال میشود، پس باید opacity را از baseStyle حذف کنیم
|
||||||
key={obj.id || index}
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
src={obj.imageUrl || ''}
|
const { opacity, ...textBaseStyle } = baseStyle;
|
||||||
alt=""
|
|
||||||
style={{
|
|
||||||
...baseStyle,
|
|
||||||
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
|
||||||
height: obj.height ? `${obj.height * scale}px` : 'auto',
|
|
||||||
objectFit: 'fill', // مشابه Konva که image را با width و height مشخص شده رندر میکند
|
|
||||||
zIndex: index, // برای حفظ ترتیب رندر
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
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',
|
|
||||||
zIndex: index,
|
|
||||||
pointerEvents: 'auto',
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<button
|
<div
|
||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
type="button"
|
|
||||||
style={{
|
style={{
|
||||||
...linkStyle,
|
...textBaseStyle,
|
||||||
zIndex: 9999,
|
fontSize: `${(obj.fontSize || 16) * scale}px`,
|
||||||
|
fontFamily: obj.fontFamily || 'irancell, sans-serif',
|
||||||
|
fontWeight: obj.fontWeight || 'normal',
|
||||||
|
color: getColorWithOpacity(obj.fill, obj.opacity),
|
||||||
|
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: 'fill', // مشابه Konva که image را با width و height مشخص شده رندر میکند
|
||||||
|
zIndex: index, // برای حفظ ترتیب رندر
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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',
|
||||||
|
zIndex: index,
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
border: 'none',
|
|
||||||
background: 'none',
|
|
||||||
padding: 0,
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (obj.linkUrl && onLinkClick) {
|
|
||||||
onLinkClick(obj.linkUrl);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -171,323 +124,370 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
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') {
|
|
||||||
// در editor، circle با مرکز در x, y رندر میشود (radius = width/2)
|
|
||||||
// در dataTransformer از گوشه بالا-چپ به مرکز تبدیل شده است
|
|
||||||
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',
|
|
||||||
zIndex: index, // برای حفظ ترتیب رندر
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj.shapeType === 'triangle') {
|
case 'rectangle': {
|
||||||
// در editor، triangle با مرکز در x + width/2, y + height/2 رندر میشود
|
if (obj.shapeType === 'circle') {
|
||||||
// RegularPolygon در Konva یک مثلث متساویالاضلاع است با radius از مرکز تا رأس
|
// در editor، circle با مرکز در x, y رندر میشود (radius = width/2)
|
||||||
// radius باید بر اساس ابعاد اصلی (قبل از scale) محاسبه شود
|
// در dataTransformer از گوشه بالا-چپ به مرکز تبدیل شده است
|
||||||
const baseWidth = obj.width || 100;
|
const radius = ((obj.width || 100) / 2) * scale;
|
||||||
const baseHeight = obj.height || 100;
|
const centerX = (obj.x || 0) * scale;
|
||||||
const baseRadius = Math.min(baseWidth, baseHeight) / 2;
|
const centerY = (obj.y || 0) * scale;
|
||||||
const radius = baseRadius * scale;
|
|
||||||
|
|
||||||
// محاسبه مرکز مثلث (مثل editor)
|
return (
|
||||||
const centerX = ((obj.x || 0) + baseWidth / 2) * scale;
|
<div
|
||||||
const centerY = ((obj.y || 0) + baseHeight / 2) * scale;
|
key={obj.id || index}
|
||||||
|
style={{
|
||||||
// برای مثلث متساویالاضلاع، size = radius * 2
|
position: 'absolute',
|
||||||
const size = radius * 2;
|
left: `${centerX - radius}px`,
|
||||||
const halfSize = size / 2;
|
top: `${centerY - radius}px`,
|
||||||
|
width: `${radius * 2}px`,
|
||||||
// نقاط مثلث متساویالاضلاع: رأس بالا (50%, 0%), پایین چپ (0%, 100%), پایین راست (100%, 100%)
|
height: `${radius * 2}px`,
|
||||||
// اما برای مثلث متساویالاضلاع واقعی، باید ارتفاع را محاسبه کنیم
|
borderRadius: '50%',
|
||||||
// ارتفاع مثلث متساویالاضلاع = size * sqrt(3) / 2 ≈ size * 0.866
|
backgroundColor: obj.fill || 'transparent',
|
||||||
const triangleHeight = size * 0.8660254; // sqrt(3) / 2
|
border: obj.stroke
|
||||||
const topY = (size - triangleHeight) / 2;
|
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
|
||||||
const bottomY = size - topY;
|
: 'none',
|
||||||
const halfWidth = triangleHeight * 0.5773503; // tan(30°) = 1/sqrt(3)
|
opacity: obj.opacity ?? 1,
|
||||||
|
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
|
||||||
const topX = halfSize;
|
transformOrigin: 'center center',
|
||||||
const bottomLeftX = halfSize - halfWidth;
|
zIndex: index, // برای حفظ ترتیب رندر
|
||||||
const bottomRightX = halfSize + halfWidth;
|
}}
|
||||||
|
|
||||||
const strokeWidth = obj.stroke ? (obj.strokeWidth || 1) * scale : 0;
|
|
||||||
const fillColor = obj.fill || '#000000';
|
|
||||||
const strokeColor = obj.stroke || 'transparent';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
key={obj.id || index}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: `${centerX - radius}px`,
|
|
||||||
top: `${centerY - radius}px`,
|
|
||||||
width: `${size}px`,
|
|
||||||
height: `${size}px`,
|
|
||||||
opacity: obj.opacity ?? 1,
|
|
||||||
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
|
|
||||||
transformOrigin: 'center center',
|
|
||||||
zIndex: index,
|
|
||||||
overflow: 'visible',
|
|
||||||
}}
|
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
|
||||||
>
|
|
||||||
<polygon
|
|
||||||
points={`${topX},${topY} ${bottomLeftX},${bottomY} ${bottomRightX},${bottomY}`}
|
|
||||||
fill={fillColor}
|
|
||||||
stroke={strokeColor}
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
if (obj.shapeType === 'triangle') {
|
||||||
|
// در editor، triangle با مرکز در x + width/2, y + height/2 رندر میشود
|
||||||
|
// RegularPolygon در Konva یک مثلث متساویالاضلاع است با radius از مرکز تا رأس
|
||||||
|
// radius باید بر اساس ابعاد اصلی (قبل از scale) محاسبه شود
|
||||||
|
const baseWidth = obj.width || 100;
|
||||||
|
const baseHeight = obj.height || 100;
|
||||||
|
const baseRadius = Math.min(baseWidth, baseHeight) / 2;
|
||||||
|
const radius = baseRadius * scale;
|
||||||
|
|
||||||
|
// محاسبه مرکز مثلث (مثل editor)
|
||||||
|
const centerX = ((obj.x || 0) + baseWidth / 2) * scale;
|
||||||
|
const centerY = ((obj.y || 0) + baseHeight / 2) * scale;
|
||||||
|
|
||||||
|
// برای مثلث متساویالاضلاع، size = radius * 2
|
||||||
|
const size = radius * 2;
|
||||||
|
const halfSize = size / 2;
|
||||||
|
|
||||||
|
// نقاط مثلث متساویالاضلاع: رأس بالا (50%, 0%), پایین چپ (0%, 100%), پایین راست (100%, 100%)
|
||||||
|
// اما برای مثلث متساویالاضلاع واقعی، باید ارتفاع را محاسبه کنیم
|
||||||
|
// ارتفاع مثلث متساویالاضلاع = size * sqrt(3) / 2 ≈ size * 0.866
|
||||||
|
const triangleHeight = size * 0.8660254; // sqrt(3) / 2
|
||||||
|
const topY = (size - triangleHeight) / 2;
|
||||||
|
const bottomY = size - topY;
|
||||||
|
const halfWidth = triangleHeight * 0.5773503; // tan(30°) = 1/sqrt(3)
|
||||||
|
|
||||||
|
const topX = halfSize;
|
||||||
|
const bottomLeftX = halfSize - halfWidth;
|
||||||
|
const bottomRightX = halfSize + halfWidth;
|
||||||
|
|
||||||
|
const strokeWidth = obj.stroke ? (obj.strokeWidth || 1) * scale : 0;
|
||||||
|
const fillColor = obj.fill || '#000000';
|
||||||
|
const strokeColor = obj.stroke || 'transparent';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
key={obj.id || index}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${centerX - radius}px`,
|
||||||
|
top: `${centerY - radius}px`,
|
||||||
|
width: `${size}px`,
|
||||||
|
height: `${size}px`,
|
||||||
|
opacity: obj.opacity ?? 1,
|
||||||
|
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
|
||||||
|
transformOrigin: 'center center',
|
||||||
|
zIndex: index,
|
||||||
|
overflow: 'visible',
|
||||||
|
}}
|
||||||
|
viewBox={`0 0 ${size} ${size}`}
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points={`${topX},${topY} ${bottomLeftX},${bottomY} ${bottomRightX},${bottomY}`}
|
||||||
|
fill={fillColor}
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
zIndex: index, // برای حفظ ترتیب رندر
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div key={obj.id || index} style={shapeStyle} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj.shapeType === 'abstract') {
|
case 'line': {
|
||||||
// در editor، abstract (star) با مرکز در x + width/2, y + height/2 رندر میشود
|
// در EditorObject، width و height به عنوان مختصات انتهایی استفاده میشوند
|
||||||
const width = (obj.width || 100) * scale;
|
// در Konva Line، points به صورت [0, 0, endX - startX, endY - startY] است
|
||||||
const height = (obj.height || 100) * scale;
|
const startX = (obj.x || 0) * scale;
|
||||||
const baseRadius = Math.min(width, height) / 2;
|
const startY = (obj.y || 0) * scale;
|
||||||
const abstractScale = 0.85;
|
// width و height مستقیماً مختصات نقطه پایان هستند
|
||||||
const radius = baseRadius * abstractScale;
|
const endX = (obj.width ?? obj.x ?? 0) * scale;
|
||||||
const centerX = (obj.x || 0) * scale + width / 2;
|
const endY = (obj.height ?? obj.y ?? 0) * scale;
|
||||||
const centerY = (obj.y || 0) * scale + height / 2;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: `${centerX - radius}px`,
|
left: `${startX}px`,
|
||||||
top: `${centerY - radius}px`,
|
top: `${startY}px`,
|
||||||
width: `${radius * 2}px`,
|
width: `${lineLength}px`,
|
||||||
height: `${radius * 2}px`,
|
height: `${(obj.strokeWidth || 2) * scale}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.stroke || '#000000',
|
||||||
backgroundColor: obj.fill || 'transparent',
|
|
||||||
border: obj.stroke
|
|
||||||
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
|
|
||||||
: 'none',
|
|
||||||
opacity: obj.opacity ?? 1,
|
opacity: obj.opacity ?? 1,
|
||||||
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
|
transform: `rotate(${lineAngle}deg)`,
|
||||||
transformOrigin: 'center center',
|
transformOrigin: 'left center',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rectangle عادی
|
case 'arrow': {
|
||||||
const shapeStyle: React.CSSProperties = {
|
// در EditorObject، width و height به عنوان مختصات انتهایی استفاده میشوند
|
||||||
...baseStyle,
|
// در Konva Arrow، points به صورت [0, 0, endX - startX, endY - startY] است
|
||||||
width: `${(obj.width || 100) * scale}px`,
|
const startX = (obj.x || 0) * scale;
|
||||||
height: `${(obj.height || 100) * scale}px`,
|
const startY = (obj.y || 0) * scale;
|
||||||
backgroundColor: obj.fill || 'transparent',
|
// width و height مستقیماً مختصات نقطه پایان هستند
|
||||||
border: obj.stroke
|
const endX = (obj.width ?? obj.x ?? 0) * scale;
|
||||||
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
|
const endY = (obj.height ?? obj.y ?? 0) * scale;
|
||||||
: 'none',
|
const dx = endX - startX;
|
||||||
zIndex: index, // برای حفظ ترتیب رندر
|
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={shapeStyle} />;
|
return (
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
// width و height مستقیماً مختصات نقطه پایان هستند
|
|
||||||
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;
|
|
||||||
// width و height مستقیماً مختصات نقطه پایان هستند
|
|
||||||
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
|
<div
|
||||||
|
key={obj.id || index}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: `-${strokeWidth * 2}px`,
|
left: `${startX}px`,
|
||||||
top: '50%',
|
top: `${startY}px`,
|
||||||
transform: 'translateY(-50%)',
|
width: `${arrowLength}px`,
|
||||||
width: 0,
|
height: `${strokeWidth}px`,
|
||||||
height: 0,
|
backgroundColor: obj.stroke || '#000000',
|
||||||
borderTop: `${strokeWidth * 2}px solid transparent`,
|
opacity: obj.opacity ?? 1,
|
||||||
borderBottom: `${strokeWidth * 2}px solid transparent`,
|
transform: `rotate(${arrowAngle}deg)`,
|
||||||
borderLeft: `${strokeWidth * 2}px solid ${obj.stroke || '#000000'}`,
|
transformOrigin: 'left center',
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</div>
|
<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;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
case 'grid': {
|
const width = pageWidth ?? 794 * scale;
|
||||||
if (!obj.tableData) return null;
|
const height = pageHeight ?? 1123 * scale;
|
||||||
|
|
||||||
const { rows, cols, cells, cellWidth, cellHeight, stroke, strokeWidth } =
|
return (
|
||||||
obj.tableData;
|
<div
|
||||||
|
ref={ref}
|
||||||
return (
|
className="page"
|
||||||
<div
|
data-density="soft"
|
||||||
key={obj.id || index}
|
style={{
|
||||||
style={{
|
position: 'relative',
|
||||||
...baseStyle,
|
backgroundColor: '#ffffff',
|
||||||
display: 'grid',
|
overflow: 'hidden',
|
||||||
gridTemplateRows: `repeat(${rows}, ${cellHeight * scale}px)`,
|
boxSizing: 'border-box',
|
||||||
gridTemplateColumns: `repeat(${cols}, ${cellWidth * scale}px)`,
|
width: `${width}px`,
|
||||||
border: stroke ? `${(strokeWidth || 1) * scale}px solid ${stroke}` : 'none',
|
height: `${height}px`,
|
||||||
}}
|
isolation: 'isolate',
|
||||||
>
|
zIndex: 1,
|
||||||
{Array.from({ length: rows }).map((_, rowIndex) =>
|
}}
|
||||||
Array.from({ length: cols }).map((_, colIndex) => {
|
>
|
||||||
const cellId = `cell-${rowIndex}-${colIndex}`;
|
{page.elements.map((element, index) => renderObject(element, index))}
|
||||||
const cell = cells[cellId];
|
</div>
|
||||||
|
);
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const width = pageWidth ?? 794 * scale;
|
|
||||||
const height = pageHeight ?? 1123 * scale;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className="page"
|
|
||||||
data-density="soft"
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
overflow: 'hidden',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
width: `${width}px`,
|
|
||||||
height: `${height}px`,
|
|
||||||
isolation: 'isolate',
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{page.elements.map((element, index) => renderObject(element, index))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
BookPage.displayName = 'BookPage';
|
BookPage.displayName = 'BookPage';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user