base load viewer
This commit is contained in:
@@ -0,0 +1,539 @@
|
|||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"id": "page-1",
|
||||||
|
"name": "صفحه 1",
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"id": "text-1",
|
||||||
|
"type": "text",
|
||||||
|
"x": 100,
|
||||||
|
"y": 100,
|
||||||
|
"text": "خوش آمدید به صفحه اول",
|
||||||
|
"fontSize": 36,
|
||||||
|
"fontFamily": "irancell",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fill": "#1a1a1a",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "image-1",
|
||||||
|
"type": "image",
|
||||||
|
"x": 100,
|
||||||
|
"y": 200,
|
||||||
|
"width": 300,
|
||||||
|
"height": 300,
|
||||||
|
"imageUrl": "https://picsum.photos/200/300",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rectangle-1",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 450,
|
||||||
|
"y": 200,
|
||||||
|
"width": 200,
|
||||||
|
"height": 150,
|
||||||
|
"fill": "#3b82f6",
|
||||||
|
"stroke": "#1e40af",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"shapeType": "square",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "circle-1",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 500,
|
||||||
|
"y": 400,
|
||||||
|
"width": 150,
|
||||||
|
"height": 150,
|
||||||
|
"fill": "#ef4444",
|
||||||
|
"stroke": "#dc2626",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"shapeType": "circle",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "video-1",
|
||||||
|
"type": "video",
|
||||||
|
"x": 100,
|
||||||
|
"y": 550,
|
||||||
|
"width": 400,
|
||||||
|
"height": 300,
|
||||||
|
"videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4",
|
||||||
|
"opacity": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "page-2",
|
||||||
|
"name": "صفحه 2",
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"id": "text-2",
|
||||||
|
"type": "text",
|
||||||
|
"x": 100,
|
||||||
|
"y": 100,
|
||||||
|
"text": "فصل اول: مقدمه",
|
||||||
|
"fontSize": 32,
|
||||||
|
"fontFamily": "irancell",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fill": "#000000",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line-1",
|
||||||
|
"type": "line",
|
||||||
|
"x": 100,
|
||||||
|
"y": 180,
|
||||||
|
"width": 500,
|
||||||
|
"height": 2,
|
||||||
|
"stroke": "#64748b",
|
||||||
|
"strokeWidth": 3,
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "arrow-1",
|
||||||
|
"type": "arrow",
|
||||||
|
"x": 200,
|
||||||
|
"y": 250,
|
||||||
|
"width": 300,
|
||||||
|
"height": 100,
|
||||||
|
"stroke": "#f59e0b",
|
||||||
|
"strokeWidth": 4,
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "triangle-1",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 550,
|
||||||
|
"y": 250,
|
||||||
|
"width": 150,
|
||||||
|
"height": 150,
|
||||||
|
"fill": "#10b981",
|
||||||
|
"stroke": "#059669",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"shapeType": "triangle",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "link-1",
|
||||||
|
"type": "link",
|
||||||
|
"x": 100,
|
||||||
|
"y": 400,
|
||||||
|
"width": 200,
|
||||||
|
"height": 50,
|
||||||
|
"text": "لینک به صفحه وب",
|
||||||
|
"fontSize": 18,
|
||||||
|
"linkUrl": "https://example.com",
|
||||||
|
"fill": "#3b82f6",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "image-2",
|
||||||
|
"type": "image",
|
||||||
|
"x": 350,
|
||||||
|
"y": 400,
|
||||||
|
"width": 250,
|
||||||
|
"height": 200,
|
||||||
|
"imageUrl": "https://picsum.photos/200/300",
|
||||||
|
"opacity": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "page-3",
|
||||||
|
"name": "صفحه 3",
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"id": "text-3",
|
||||||
|
"type": "text",
|
||||||
|
"x": 100,
|
||||||
|
"y": 100,
|
||||||
|
"text": "صفحه سوم\nاین صفحه شامل جدول است",
|
||||||
|
"fontSize": 28,
|
||||||
|
"fontFamily": "irancell",
|
||||||
|
"fontWeight": "normal",
|
||||||
|
"fill": "#1a1a1a",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grid-1",
|
||||||
|
"type": "grid",
|
||||||
|
"x": 100,
|
||||||
|
"y": 250,
|
||||||
|
"width": 500,
|
||||||
|
"height": 300,
|
||||||
|
"tableData": {
|
||||||
|
"id": "table-1",
|
||||||
|
"rows": 4,
|
||||||
|
"cols": 5,
|
||||||
|
"cellWidth": 100,
|
||||||
|
"cellHeight": 75,
|
||||||
|
"cells": {
|
||||||
|
"cell-0-0": {
|
||||||
|
"id": "cell-0-0",
|
||||||
|
"text": "ردیف 1",
|
||||||
|
"background": "#f3f4f6",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-0-1": {
|
||||||
|
"id": "cell-0-1",
|
||||||
|
"text": "ستون 1",
|
||||||
|
"background": "#e5e7eb",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-0-2": {
|
||||||
|
"id": "cell-0-2",
|
||||||
|
"text": "ستون 2",
|
||||||
|
"background": "#e5e7eb",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-0-3": {
|
||||||
|
"id": "cell-0-3",
|
||||||
|
"text": "ستون 3",
|
||||||
|
"background": "#e5e7eb",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-0-4": {
|
||||||
|
"id": "cell-0-4",
|
||||||
|
"text": "ستون 4",
|
||||||
|
"background": "#e5e7eb",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-1-0": {
|
||||||
|
"id": "cell-1-0",
|
||||||
|
"text": "ردیف 2",
|
||||||
|
"background": "#f3f4f6",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-1-1": {
|
||||||
|
"id": "cell-1-1",
|
||||||
|
"text": "داده 1",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-1-2": {
|
||||||
|
"id": "cell-1-2",
|
||||||
|
"text": "داده 2",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-1-3": {
|
||||||
|
"id": "cell-1-3",
|
||||||
|
"text": "داده 3",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-1-4": {
|
||||||
|
"id": "cell-1-4",
|
||||||
|
"text": "داده 4",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-2-0": {
|
||||||
|
"id": "cell-2-0",
|
||||||
|
"text": "ردیف 3",
|
||||||
|
"background": "#f3f4f6",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-2-1": {
|
||||||
|
"id": "cell-2-1",
|
||||||
|
"text": "داده 5",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-2-2": {
|
||||||
|
"id": "cell-2-2",
|
||||||
|
"text": "داده 6",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-2-3": {
|
||||||
|
"id": "cell-2-3",
|
||||||
|
"text": "داده 7",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-2-4": {
|
||||||
|
"id": "cell-2-4",
|
||||||
|
"text": "داده 8",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-3-0": {
|
||||||
|
"id": "cell-3-0",
|
||||||
|
"text": "ردیف 4",
|
||||||
|
"background": "#f3f4f6",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-3-1": {
|
||||||
|
"id": "cell-3-1",
|
||||||
|
"text": "داده 9",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-3-2": {
|
||||||
|
"id": "cell-3-2",
|
||||||
|
"text": "داده 10",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-3-3": {
|
||||||
|
"id": "cell-3-3",
|
||||||
|
"text": "داده 11",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
},
|
||||||
|
"cell-3-4": {
|
||||||
|
"id": "cell-3-4",
|
||||||
|
"text": "داده 12",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"width": 100,
|
||||||
|
"height": 75
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stroke": "#808080",
|
||||||
|
"strokeWidth": 1
|
||||||
|
},
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "abstract-1",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 650,
|
||||||
|
"y": 250,
|
||||||
|
"width": 100,
|
||||||
|
"height": 200,
|
||||||
|
"fill": "#8b5cf6",
|
||||||
|
"stroke": "#7c3aed",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"shapeType": "abstract",
|
||||||
|
"opacity": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "page-4",
|
||||||
|
"name": "صفحه 4",
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"id": "text-4",
|
||||||
|
"type": "text",
|
||||||
|
"x": 100,
|
||||||
|
"y": 100,
|
||||||
|
"text": "صفحه چهارم\nشامل ویدیو و تصویر",
|
||||||
|
"fontSize": 30,
|
||||||
|
"fontFamily": "irancell",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fill": "#1a1a1a",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "video-2",
|
||||||
|
"type": "video",
|
||||||
|
"x": 100,
|
||||||
|
"y": 200,
|
||||||
|
"width": 350,
|
||||||
|
"height": 250,
|
||||||
|
"videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "image-3",
|
||||||
|
"type": "image",
|
||||||
|
"x": 500,
|
||||||
|
"y": 200,
|
||||||
|
"width": 250,
|
||||||
|
"height": 250,
|
||||||
|
"imageUrl": "https://picsum.photos/200/300",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line-2",
|
||||||
|
"type": "line",
|
||||||
|
"x": 100,
|
||||||
|
"y": 500,
|
||||||
|
"width": 600,
|
||||||
|
"height": 2,
|
||||||
|
"stroke": "#94a3b8",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "arrow-2",
|
||||||
|
"type": "arrow",
|
||||||
|
"x": 200,
|
||||||
|
"y": 550,
|
||||||
|
"width": 400,
|
||||||
|
"height": 80,
|
||||||
|
"stroke": "#ec4899",
|
||||||
|
"strokeWidth": 5,
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "circle-2",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 650,
|
||||||
|
"y": 550,
|
||||||
|
"width": 100,
|
||||||
|
"height": 100,
|
||||||
|
"fill": "#f59e0b",
|
||||||
|
"stroke": "#d97706",
|
||||||
|
"strokeWidth": 3,
|
||||||
|
"shapeType": "circle",
|
||||||
|
"opacity": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "page-5",
|
||||||
|
"name": "صفحه 5",
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"id": "text-5",
|
||||||
|
"type": "text",
|
||||||
|
"x": 100,
|
||||||
|
"y": 100,
|
||||||
|
"text": "صفحه آخر\nنمایش همه انواع object ها",
|
||||||
|
"fontSize": 32,
|
||||||
|
"fontFamily": "irancell",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fill": "#1a1a1a",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rectangle-2",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 100,
|
||||||
|
"y": 200,
|
||||||
|
"width": 150,
|
||||||
|
"height": 120,
|
||||||
|
"fill": "#06b6d4",
|
||||||
|
"stroke": "#0891b2",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"shapeType": "square",
|
||||||
|
"rotation": 15,
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "triangle-2",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 300,
|
||||||
|
"y": 200,
|
||||||
|
"width": 120,
|
||||||
|
"height": 120,
|
||||||
|
"fill": "#14b8a6",
|
||||||
|
"stroke": "#0d9488",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"shapeType": "triangle",
|
||||||
|
"rotation": 30,
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "abstract-2",
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 470,
|
||||||
|
"y": 200,
|
||||||
|
"width": 100,
|
||||||
|
"height": 150,
|
||||||
|
"fill": "#a855f7",
|
||||||
|
"stroke": "#9333ea",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"shapeType": "abstract",
|
||||||
|
"rotation": -20,
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "image-4",
|
||||||
|
"type": "image",
|
||||||
|
"x": 620,
|
||||||
|
"y": 200,
|
||||||
|
"width": 150,
|
||||||
|
"height": 150,
|
||||||
|
"imageUrl": "https://picsum.photos/200/300",
|
||||||
|
"rotation": 10,
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "video-3",
|
||||||
|
"type": "video",
|
||||||
|
"x": 100,
|
||||||
|
"y": 380,
|
||||||
|
"width": 300,
|
||||||
|
"height": 200,
|
||||||
|
"videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "link-2",
|
||||||
|
"type": "link",
|
||||||
|
"x": 450,
|
||||||
|
"y": 380,
|
||||||
|
"width": 250,
|
||||||
|
"height": 60,
|
||||||
|
"text": "لینک به صفحه دیگر",
|
||||||
|
"fontSize": 20,
|
||||||
|
"linkUrl": "https://example.com",
|
||||||
|
"fill": "#6366f1",
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line-3",
|
||||||
|
"type": "line",
|
||||||
|
"x": 100,
|
||||||
|
"y": 620,
|
||||||
|
"width": 200,
|
||||||
|
"height": 3,
|
||||||
|
"stroke": "#f97316",
|
||||||
|
"strokeWidth": 4,
|
||||||
|
"rotation": 45,
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "arrow-3",
|
||||||
|
"type": "arrow",
|
||||||
|
"x": 350,
|
||||||
|
"y": 620,
|
||||||
|
"width": 250,
|
||||||
|
"height": 100,
|
||||||
|
"stroke": "#84cc16",
|
||||||
|
"strokeWidth": 6,
|
||||||
|
"rotation": -30,
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "text-6",
|
||||||
|
"type": "text",
|
||||||
|
"x": 100,
|
||||||
|
"y": 750,
|
||||||
|
"text": "این یک متن نمونه است\nبا چند خط\nبرای نمایش بهتر",
|
||||||
|
"fontSize": 22,
|
||||||
|
"fontFamily": "irancell",
|
||||||
|
"fontWeight": "normal",
|
||||||
|
"fill": "#475569",
|
||||||
|
"letterSpacing": 1,
|
||||||
|
"opacity": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import Logo from "@/assets/images/logo.svg";
|
import Logo from "@/assets/images/logo.svg";
|
||||||
import { clx } from "@/helpers/utils";
|
import { clx } from "@/helpers/utils";
|
||||||
import { useSharedStore } from "@/shared/store/sharedStore";
|
import { useSharedStore } from "@/shared/store/sharedStore";
|
||||||
import { CloseSquare } from "iconsax-react";
|
import { CloseSquare, DocumentUpload } from "iconsax-react";
|
||||||
import { useEditorStore, type ToolType } from "../store/editorStore";
|
import { useEditorStore, type ToolType } from "../store/editorStore";
|
||||||
import { ToolsBar, ObjectSettings, ToolInstructions } from "./sidebar";
|
import { ToolsBar, ObjectSettings, ToolInstructions } from "./sidebar";
|
||||||
|
|
||||||
const EditorSidebar = () => {
|
const EditorSidebar = () => {
|
||||||
const { openSidebar, setOpenSidebar } = useSharedStore();
|
const { openSidebar, setOpenSidebar } = useSharedStore();
|
||||||
const { tool, setTool, selectedObjectId, objects, updateObject, deleteObject } = useEditorStore();
|
const { tool, setTool, selectedObjectId, objects, updateObject, deleteObject, loadPages } = useEditorStore();
|
||||||
|
|
||||||
const selectedObject = objects.find((obj) => obj.id === selectedObjectId);
|
const selectedObject = objects.find((obj) => obj.id === selectedObjectId);
|
||||||
|
|
||||||
@@ -15,6 +15,19 @@ const EditorSidebar = () => {
|
|||||||
setTool(selectedTool);
|
setTool(selectedTool);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLoadJSON = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/viewer-data.json');
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.pages && Array.isArray(data.pages)) {
|
||||||
|
loadPages(data.pages);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('خطا در لود JSON:', error);
|
||||||
|
alert('خطا در لود فایل JSON');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -29,12 +42,21 @@ const EditorSidebar = () => {
|
|||||||
>
|
>
|
||||||
<div className="mb-10 flex items-center justify-between">
|
<div className="mb-10 flex items-center justify-between">
|
||||||
<img src={Logo} alt="لوگو دانک" className="h-9" />
|
<img src={Logo} alt="لوگو دانک" className="h-9" />
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
onClick={() => setOpenSidebar(false)}
|
<button
|
||||||
className="xl:hidden p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
onClick={handleLoadJSON}
|
||||||
>
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
<CloseSquare color="black" size={24} />
|
title="لود JSON"
|
||||||
</button>
|
>
|
||||||
|
<DocumentUpload color="black" size={24} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setOpenSidebar(false)}
|
||||||
|
className="xl:hidden p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<CloseSquare color="black" size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-8 flex-1 min-h-0">
|
<div className="flex gap-8 flex-1 min-h-0">
|
||||||
|
|||||||
@@ -99,10 +99,6 @@ const VideoShape = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Group
|
<Group
|
||||||
@@ -130,14 +126,25 @@ const VideoShape = ({
|
|||||||
strokeWidth={isSelected ? 3 : (obj.strokeWidth ?? 0)}
|
strokeWidth={isSelected ? 3 : (obj.strokeWidth ?? 0)}
|
||||||
onClick={handleGroupClick}
|
onClick={handleGroupClick}
|
||||||
/>
|
/>
|
||||||
<KonvaImage
|
{image ? (
|
||||||
x={0}
|
<KonvaImage
|
||||||
y={0}
|
x={0}
|
||||||
width={containerWidth}
|
y={0}
|
||||||
height={containerHeight}
|
width={containerWidth}
|
||||||
image={image}
|
height={containerHeight}
|
||||||
onClick={handleGroupClick}
|
image={image}
|
||||||
/>
|
onClick={handleGroupClick}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Rect
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={containerWidth}
|
||||||
|
height={containerHeight}
|
||||||
|
fill="#1a1a1a"
|
||||||
|
onClick={handleGroupClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Group
|
<Group
|
||||||
name="playButton"
|
name="playButton"
|
||||||
onClick={handlePlayClick}
|
onClick={handlePlayClick}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ type EditorStoreType = {
|
|||||||
toggleObjectVisibility: (id: string) => void;
|
toggleObjectVisibility: (id: string) => void;
|
||||||
groupObjects: (objectIds: string[]) => void;
|
groupObjects: (objectIds: string[]) => void;
|
||||||
ungroupObjects: (groupId: string) => void;
|
ungroupObjects: (groupId: string) => void;
|
||||||
|
loadPages: (pages: Page[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createTableCellId = (row: number, col: number) => `cell-${row}-${col}`;
|
const createTableCellId = (row: number, col: number) => `cell-${row}-${col}`;
|
||||||
@@ -904,5 +905,16 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
|||||||
const state = get();
|
const state = get();
|
||||||
return state.objects.filter((obj) => obj.groupId === groupId);
|
return state.objects.filter((obj) => obj.groupId === groupId);
|
||||||
},
|
},
|
||||||
|
loadPages: (pages) => {
|
||||||
|
if (pages.length === 0) return;
|
||||||
|
const firstPage = pages[0];
|
||||||
|
set({
|
||||||
|
pages,
|
||||||
|
currentPageId: firstPage.id,
|
||||||
|
objects: firstPage.objects,
|
||||||
|
selectedObjectId: null,
|
||||||
|
selectedObjectIds: [],
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,63 @@
|
|||||||
import { type FC } from 'react';
|
import { type FC, useEffect, useState } from 'react';
|
||||||
import BookViewer from './components/BookViewer';
|
import BookViewer from './components/BookViewer';
|
||||||
import { mockPages } from './components/mockData';
|
import { transformViewerDataToPages } from './utils/dataTransformer';
|
||||||
|
import type { PageData } from './types';
|
||||||
|
|
||||||
const Viewer: FC = () => {
|
const Viewer: FC = () => {
|
||||||
|
const [pages, setPages] = useState<PageData[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadViewerData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/viewer-data.json');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('خطا در بارگذاری دادهها');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const transformedPages = transformViewerDataToPages(data);
|
||||||
|
setPages(transformedPages);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'خطای نامشخص');
|
||||||
|
console.error('خطا در بارگذاری viewer-data.json:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadViewerData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<div className="text-gray-600">در حال بارگذاری...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<div className="text-red-600">خطا: {error}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pages.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<div className="text-gray-600">هیچ صفحهای یافت نشد</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
<BookViewer pages={mockPages} />
|
<BookViewer pages={pages} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { type PageData } from '../types';
|
import { type PageData } from '../types';
|
||||||
|
import type { EditorObject } from '@/pages/editor/store/editorStore';
|
||||||
|
|
||||||
type BookPageProps = {
|
type BookPageProps = {
|
||||||
page: PageData;
|
page: PageData;
|
||||||
|
scale?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,7 +14,323 @@ type BookPageProps = {
|
|||||||
* این کامپوننت به عنوان child برای HTMLFlipBook استفاده میشود
|
* این کامپوننت به عنوان child برای HTMLFlipBook استفاده میشود
|
||||||
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
|
* نیاز به 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -22,52 +340,12 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page }, ref) => {
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
padding: '40px',
|
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
width: '100%',
|
width: `${794 * scale}px`,
|
||||||
height: '100%',
|
height: `${1123 * scale}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{page.elements.map((element, index) => {
|
{page.elements.map((element, index) => renderObject(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;
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,18 +56,18 @@ const BookViewer: FC<BookViewerProps> = ({ pages }) => {
|
|||||||
/>
|
/>
|
||||||
<HTMLFlipBook
|
<HTMLFlipBook
|
||||||
ref={bookRef}
|
ref={bookRef}
|
||||||
width={794}
|
width={397}
|
||||||
height={1123}
|
height={561}
|
||||||
size="stretch"
|
size="fixed"
|
||||||
minWidth={315}
|
minWidth={397}
|
||||||
minHeight={400}
|
minHeight={561}
|
||||||
maxWidth={1000}
|
maxWidth={397}
|
||||||
maxHeight={1533}
|
maxHeight={561}
|
||||||
drawShadow={true}
|
drawShadow={true}
|
||||||
flippingTime={800}
|
flippingTime={800}
|
||||||
usePortrait={true}
|
usePortrait={false}
|
||||||
startPage={0}
|
startPage={0}
|
||||||
autoSize={true}
|
autoSize={false}
|
||||||
maxShadowOpacity={0.5}
|
maxShadowOpacity={0.5}
|
||||||
showCover={true}
|
showCover={true}
|
||||||
mobileScrollSupport={true}
|
mobileScrollSupport={true}
|
||||||
@@ -83,9 +83,9 @@ const BookViewer: FC<BookViewerProps> = ({ pages }) => {
|
|||||||
}}
|
}}
|
||||||
onFlip={handlePageFlip}
|
onFlip={handlePageFlip}
|
||||||
>
|
>
|
||||||
{pages.map((page) => (
|
{pages.map((page) => (
|
||||||
<BookPage key={page.id} page={page} />
|
<BookPage key={page.id} page={page} scale={0.5} />
|
||||||
))}
|
))}
|
||||||
</HTMLFlipBook>
|
</HTMLFlipBook>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
export type PageElement =
|
import type { EditorObject } from '@/pages/editor/store/editorStore';
|
||||||
| { type: "text"; x: number; y: number; text: string; fontSize: number; color: string }
|
|
||||||
| { type: "image"; x: number; y: number; src: string; width: number; height: number };
|
|
||||||
|
|
||||||
export type PageData = {
|
export type PageData = {
|
||||||
id: number;
|
id: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
elements: PageElement[];
|
elements: EditorObject[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import type { EditorObject } from '@/pages/editor/store/editorStore';
|
||||||
|
import type { PageData } from '../types';
|
||||||
|
|
||||||
|
type ViewerDataPage = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
objects: Array<{
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
text?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontWeight?: string;
|
||||||
|
fill?: string;
|
||||||
|
stroke?: string;
|
||||||
|
strokeWidth?: number;
|
||||||
|
shapeType?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
videoUrl?: string;
|
||||||
|
linkUrl?: string;
|
||||||
|
rotation?: number;
|
||||||
|
opacity?: number;
|
||||||
|
letterSpacing?: number;
|
||||||
|
tableData?: any;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ViewerData = {
|
||||||
|
pages: ViewerDataPage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* تبدیل دادههای viewer-data.json به فرمت PageData
|
||||||
|
*/
|
||||||
|
export function transformViewerDataToPages(data: ViewerData): PageData[] {
|
||||||
|
return data.pages.map((page, index) => {
|
||||||
|
const objects: EditorObject[] = page.objects.map((obj) => {
|
||||||
|
const baseObject: EditorObject = {
|
||||||
|
id: obj.id,
|
||||||
|
type: obj.type as EditorObject['type'],
|
||||||
|
x: obj.x,
|
||||||
|
y: obj.y,
|
||||||
|
opacity: obj.opacity ?? 1,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// برای line و arrow، width و height در viewer-data.json به عنوان اندازه هستند
|
||||||
|
// اما در EditorObject به عنوان مختصات انتهایی استفاده میشوند
|
||||||
|
if (obj.type === 'line' || obj.type === 'arrow') {
|
||||||
|
if (obj.width !== undefined) {
|
||||||
|
baseObject.width = obj.x + obj.width;
|
||||||
|
}
|
||||||
|
if (obj.height !== undefined) {
|
||||||
|
// برای line، height ممکن است ضخامت باشد، اما برای arrow طول عمودی است
|
||||||
|
baseObject.height = obj.y + obj.height;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (obj.width !== undefined) baseObject.width = obj.width;
|
||||||
|
if (obj.height !== undefined) baseObject.height = obj.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// برای circle در viewer-data.json، x, y به عنوان گوشه بالا-چپ bounding box است
|
||||||
|
// اما در editor، circle با مرکز در x, y ذخیره میشود
|
||||||
|
// پس باید تبدیل کنیم
|
||||||
|
if (obj.type === 'rectangle' && obj.shapeType === 'circle') {
|
||||||
|
// تبدیل از گوشه بالا-چپ به مرکز
|
||||||
|
baseObject.x = obj.x + (obj.width || 100) / 2;
|
||||||
|
baseObject.y = obj.y + (obj.height || 100) / 2;
|
||||||
|
}
|
||||||
|
if (obj.fill !== undefined) baseObject.fill = obj.fill;
|
||||||
|
if (obj.stroke !== undefined) baseObject.stroke = obj.stroke;
|
||||||
|
if (obj.strokeWidth !== undefined) baseObject.strokeWidth = obj.strokeWidth;
|
||||||
|
if (obj.rotation !== undefined) baseObject.rotation = obj.rotation;
|
||||||
|
|
||||||
|
// ویژگیهای متنی
|
||||||
|
if (obj.text !== undefined) baseObject.text = obj.text;
|
||||||
|
if (obj.fontSize !== undefined) baseObject.fontSize = obj.fontSize;
|
||||||
|
if (obj.fontFamily !== undefined) baseObject.fontFamily = obj.fontFamily;
|
||||||
|
if (obj.fontWeight !== undefined) baseObject.fontWeight = obj.fontWeight;
|
||||||
|
if (obj.letterSpacing !== undefined) baseObject.letterSpacing = obj.letterSpacing;
|
||||||
|
|
||||||
|
// انواع خاص objectها
|
||||||
|
if (obj.type === 'rectangle' && obj.shapeType) {
|
||||||
|
baseObject.shapeType = obj.shapeType as EditorObject['shapeType'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.type === 'image' && obj.imageUrl) {
|
||||||
|
baseObject.imageUrl = obj.imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.type === 'video' && obj.videoUrl) {
|
||||||
|
baseObject.videoUrl = obj.videoUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.type === 'link' && obj.linkUrl) {
|
||||||
|
baseObject.linkUrl = obj.linkUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.type === 'grid' && obj.tableData) {
|
||||||
|
baseObject.tableData = obj.tableData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: index + 1,
|
||||||
|
width: 794,
|
||||||
|
height: 1123,
|
||||||
|
elements: objects,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user