sa2kit 1.6.89 → 1.6.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/{booking-473Db8Bo.d.mts → booking-BH7HM0D0.d.mts} +1 -0
  2. package/dist/{booking-473Db8Bo.d.ts → booking-BH7HM0D0.d.ts} +1 -0
  3. package/dist/{bookingAdminService-DqQ7hEGw.d.ts → bookingAdminService-nr1vOp6I.d.ts} +1 -1
  4. package/dist/{bookingAdminService-SBX4JA_U.d.mts → bookingAdminService-pvk2MY1r.d.mts} +1 -1
  5. package/dist/{client-Bkn6mRI7.d.ts → client-UDQ7uMFA.d.ts} +1 -1
  6. package/dist/{client-exYn2Qla.d.mts → client-jOToHJEx.d.mts} +1 -1
  7. package/dist/festivalCard/index.js +803 -212
  8. package/dist/festivalCard/index.js.map +1 -1
  9. package/dist/festivalCard/index.mjs +784 -193
  10. package/dist/festivalCard/index.mjs.map +1 -1
  11. package/dist/festivalCard/miniapp/index.js +162 -21
  12. package/dist/festivalCard/miniapp/index.js.map +1 -1
  13. package/dist/festivalCard/miniapp/index.mjs +153 -12
  14. package/dist/festivalCard/miniapp/index.mjs.map +1 -1
  15. package/dist/festivalCard/web/index.d.mts +17 -3
  16. package/dist/festivalCard/web/index.d.ts +17 -3
  17. package/dist/festivalCard/web/index.js +803 -212
  18. package/dist/festivalCard/web/index.js.map +1 -1
  19. package/dist/festivalCard/web/index.mjs +784 -193
  20. package/dist/festivalCard/web/index.mjs.map +1 -1
  21. package/dist/{index-z15F7afa.d.mts → index-Bs06cHTn.d.mts} +2 -2
  22. package/dist/{index-BJpxvH7X.d.ts → index-C-oNM7Gv.d.ts} +1 -1
  23. package/dist/{index-XTV6IU-M.d.ts → index-CUab5EBV.d.ts} +2 -2
  24. package/dist/{index-Cum2EknK.d.mts → index-CYDb3AKs.d.mts} +1 -1
  25. package/dist/{index-DyxLpkmm.d.mts → index-DBB4ad0S.d.mts} +2 -2
  26. package/dist/{index-CdTIsNsy.d.ts → index-DBHwbXrv.d.ts} +2 -2
  27. package/dist/index.js +575 -170
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +575 -170
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/showmasterpiece/core.d.mts +3 -3
  32. package/dist/showmasterpiece/core.d.ts +3 -3
  33. package/dist/showmasterpiece/db.d.mts +2 -0
  34. package/dist/showmasterpiece/db.d.ts +2 -0
  35. package/dist/showmasterpiece/db.js +4 -2
  36. package/dist/showmasterpiece/db.js.map +1 -1
  37. package/dist/showmasterpiece/db.mjs +4 -2
  38. package/dist/showmasterpiece/db.mjs.map +1 -1
  39. package/dist/showmasterpiece/index.js +18 -2
  40. package/dist/showmasterpiece/index.js.map +1 -1
  41. package/dist/showmasterpiece/index.mjs +18 -2
  42. package/dist/showmasterpiece/index.mjs.map +1 -1
  43. package/dist/showmasterpiece/logic/index.d.mts +2 -2
  44. package/dist/showmasterpiece/logic/index.d.ts +2 -2
  45. package/dist/showmasterpiece/server/index.js +4 -2
  46. package/dist/showmasterpiece/server/index.js.map +1 -1
  47. package/dist/showmasterpiece/server/index.mjs +4 -2
  48. package/dist/showmasterpiece/server/index.mjs.map +1 -1
  49. package/dist/showmasterpiece/service/api/index.d.mts +1 -1
  50. package/dist/showmasterpiece/service/api/index.d.ts +1 -1
  51. package/dist/showmasterpiece/service/client-business/index.d.mts +3 -3
  52. package/dist/showmasterpiece/service/client-business/index.d.ts +3 -3
  53. package/dist/showmasterpiece/service/index.d.mts +6 -6
  54. package/dist/showmasterpiece/service/index.d.ts +6 -6
  55. package/dist/showmasterpiece/service/miniapp/index.d.mts +2 -2
  56. package/dist/showmasterpiece/service/miniapp/index.d.ts +2 -2
  57. package/dist/showmasterpiece/service/web/index.d.mts +4 -4
  58. package/dist/showmasterpiece/service/web/index.d.ts +4 -4
  59. package/dist/showmasterpiece/ui/miniapp/index.d.mts +2 -2
  60. package/dist/showmasterpiece/ui/miniapp/index.d.ts +2 -2
  61. package/dist/showmasterpiece/ui/miniapp/index.js +4 -3
  62. package/dist/showmasterpiece/ui/miniapp/index.js.map +1 -1
  63. package/dist/showmasterpiece/ui/miniapp/index.mjs +4 -3
  64. package/dist/showmasterpiece/ui/miniapp/index.mjs.map +1 -1
  65. package/dist/showmasterpiece/ui/web/index.js +18 -2
  66. package/dist/showmasterpiece/ui/web/index.js.map +1 -1
  67. package/dist/showmasterpiece/ui/web/index.mjs +18 -2
  68. package/dist/showmasterpiece/ui/web/index.mjs.map +1 -1
  69. package/dist/showmasterpiece/web/index.js +18 -2
  70. package/dist/showmasterpiece/web/index.js.map +1 -1
  71. package/dist/showmasterpiece/web/index.mjs +18 -2
  72. package/dist/showmasterpiece/web/index.mjs.map +1 -1
  73. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
- import React3, { useState, useEffect, useCallback, useMemo } from 'react';
1
+ import React4, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { clsx } from 'clsx';
2
4
 
3
5
  // src/festivalCard/core/defaults.ts
4
6
  var DEFAULT_FESTIVAL_CARD_CONFIG = {
@@ -216,6 +218,190 @@ var useFestivalCardConfig = (options) => {
216
218
  [config, loading, save, saving]
217
219
  );
218
220
  };
221
+ var FloatingMenu = ({
222
+ trigger,
223
+ menu,
224
+ initialPosition = { x: 20, y: 20 },
225
+ defaultOpen = false,
226
+ className = "",
227
+ menuClassName = "",
228
+ triggerClassName = "",
229
+ zIndex = 1e3
230
+ }) => {
231
+ const [position, setPosition] = useState(initialPosition);
232
+ const [isMenuOpen, setIsMenuOpen] = useState(defaultOpen);
233
+ const [menuDirection, setMenuDirection] = useState("right");
234
+ const [isDragging, setIsDragging] = useState(false);
235
+ const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
236
+ const containerRef = useRef(null);
237
+ const [mounted, setMounted] = useState(false);
238
+ const [hasDragged, setHasDragged] = useState(false);
239
+ const dragTimerRef = useRef(null);
240
+ const mouseDownPosRef = useRef(null);
241
+ useEffect(() => {
242
+ setMounted(true);
243
+ return () => setMounted(false);
244
+ }, []);
245
+ useEffect(() => {
246
+ if (!mounted || !containerRef.current) return;
247
+ const updateMenuDirection = () => {
248
+ const rect = containerRef.current?.getBoundingClientRect();
249
+ if (!rect) return;
250
+ const windowWidth = window.innerWidth;
251
+ const middlePoint = windowWidth / 2;
252
+ setMenuDirection(rect.left < middlePoint ? "right" : "left");
253
+ };
254
+ updateMenuDirection();
255
+ window.addEventListener("resize", updateMenuDirection);
256
+ window.addEventListener("scroll", updateMenuDirection);
257
+ return () => {
258
+ window.removeEventListener("resize", updateMenuDirection);
259
+ window.removeEventListener("scroll", updateMenuDirection);
260
+ };
261
+ }, [mounted]);
262
+ const handleMouseDown = (e) => {
263
+ if (!containerRef.current) return;
264
+ e.stopPropagation();
265
+ mouseDownPosRef.current = { x: e.clientX, y: e.clientY };
266
+ const rect = containerRef.current.getBoundingClientRect();
267
+ setDragOffset({
268
+ x: e.clientX - rect.left,
269
+ y: e.clientY - rect.top
270
+ });
271
+ setHasDragged(false);
272
+ setIsDragging(true);
273
+ };
274
+ useEffect(() => {
275
+ if (!isDragging) return;
276
+ const handleMouseMove = (e) => {
277
+ if (mouseDownPosRef.current) {
278
+ const dx = Math.abs(e.clientX - mouseDownPosRef.current.x);
279
+ const dy = Math.abs(e.clientY - mouseDownPosRef.current.y);
280
+ if (dx > 3 || dy > 3) {
281
+ setHasDragged(true);
282
+ }
283
+ }
284
+ const newX = e.clientX - dragOffset.x;
285
+ const newY = e.clientY - dragOffset.y;
286
+ const windowWidth = window.innerWidth;
287
+ const windowHeight = window.innerHeight;
288
+ setPosition({
289
+ x: Math.min(Math.max(newX, 0), windowWidth - 50),
290
+ y: Math.min(Math.max(newY, 0), windowHeight - 50)
291
+ });
292
+ };
293
+ const handleMouseUp = () => {
294
+ setIsDragging(false);
295
+ mouseDownPosRef.current = null;
296
+ if (dragTimerRef.current) {
297
+ window.clearTimeout(dragTimerRef.current);
298
+ }
299
+ dragTimerRef.current = window.setTimeout(() => {
300
+ setHasDragged(false);
301
+ }, 300);
302
+ };
303
+ document.addEventListener("mousemove", handleMouseMove);
304
+ document.addEventListener("mouseup", handleMouseUp);
305
+ return () => {
306
+ document.removeEventListener("mousemove", handleMouseMove);
307
+ document.removeEventListener("mouseup", handleMouseUp);
308
+ };
309
+ }, [isDragging, dragOffset]);
310
+ useEffect(() => {
311
+ return () => {
312
+ if (dragTimerRef.current) {
313
+ window.clearTimeout(dragTimerRef.current);
314
+ }
315
+ };
316
+ }, []);
317
+ const toggleMenu = (e) => {
318
+ e.stopPropagation();
319
+ if (hasDragged) {
320
+ return;
321
+ }
322
+ setIsMenuOpen(!isMenuOpen);
323
+ };
324
+ useEffect(() => {
325
+ if (!isMenuOpen) return;
326
+ const handleClickOutside = (e) => {
327
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
328
+ setIsMenuOpen(false);
329
+ }
330
+ };
331
+ document.addEventListener("mousedown", handleClickOutside);
332
+ return () => {
333
+ document.removeEventListener("mousedown", handleClickOutside);
334
+ };
335
+ }, [isMenuOpen]);
336
+ useEffect(() => {
337
+ if (!mounted) return;
338
+ const checkBoundaries = () => {
339
+ const windowWidth = window.innerWidth;
340
+ const windowHeight = window.innerHeight;
341
+ setPosition((prev) => {
342
+ const newX = Math.min(Math.max(prev.x, 0), windowWidth - 50);
343
+ const newY = Math.min(Math.max(prev.y, 0), windowHeight - 50);
344
+ if (newX !== prev.x || newY !== prev.y) {
345
+ return { x: newX, y: newY };
346
+ }
347
+ return prev;
348
+ });
349
+ };
350
+ window.addEventListener("resize", checkBoundaries);
351
+ return () => {
352
+ window.removeEventListener("resize", checkBoundaries);
353
+ };
354
+ }, [mounted]);
355
+ if (!mounted) return null;
356
+ return createPortal(
357
+ /* @__PURE__ */ React4.createElement(
358
+ "div",
359
+ {
360
+ ref: containerRef,
361
+ className: clsx("fixed select-none box-border", className),
362
+ style: {
363
+ left: position.x + "px",
364
+ top: position.y + "px",
365
+ zIndex
366
+ }
367
+ },
368
+ /* @__PURE__ */ React4.createElement(
369
+ "div",
370
+ {
371
+ className: clsx(
372
+ "flex items-center justify-center w-12 h-12 md:w-12 md:h-12 bg-white rounded-full shadow-md hover:shadow-lg cursor-grab active:cursor-grabbing transition-all duration-200 hover:scale-105 active:scale-95",
373
+ triggerClassName
374
+ ),
375
+ onMouseDown: handleMouseDown,
376
+ onClick: toggleMenu
377
+ },
378
+ trigger
379
+ ),
380
+ isMenuOpen && /* @__PURE__ */ React4.createElement(
381
+ "div",
382
+ {
383
+ className: clsx(
384
+ "absolute top-0 bg-white rounded-lg shadow-xl p-3 min-w-[200px] md:min-w-[200px] max-w-[300px] z-[1000] transition-all duration-200",
385
+ isMenuOpen ? "opacity-100 scale-100" : "opacity-0 scale-95",
386
+ menuDirection === "left" ? "right-[calc(100%+10px)]" : "left-[calc(100%+10px)]",
387
+ menuClassName
388
+ ),
389
+ onClick: (e) => e.stopPropagation(),
390
+ onMouseDown: (e) => e.stopPropagation(),
391
+ onMouseUp: (e) => e.stopPropagation(),
392
+ onTouchStart: (e) => e.stopPropagation(),
393
+ onTouchMove: (e) => e.stopPropagation(),
394
+ onTouchEnd: (e) => e.stopPropagation(),
395
+ onPointerDown: (e) => e.stopPropagation(),
396
+ onPointerUp: (e) => e.stopPropagation()
397
+ },
398
+ menu
399
+ )
400
+ ),
401
+ document.body
402
+ );
403
+ };
404
+ var FloatingMenu_default = FloatingMenu;
219
405
  var elementStyle = (element) => ({
220
406
  position: "absolute",
221
407
  zIndex: 2,
@@ -227,7 +413,7 @@ var elementStyle = (element) => ({
227
413
  });
228
414
  var renderElement = (element) => {
229
415
  if (element.type === "text") {
230
- return /* @__PURE__ */ React3.createElement(
416
+ return /* @__PURE__ */ React4.createElement(
231
417
  "div",
232
418
  {
233
419
  key: element.id,
@@ -245,7 +431,7 @@ var renderElement = (element) => {
245
431
  element.content
246
432
  );
247
433
  }
248
- return /* @__PURE__ */ React3.createElement(
434
+ return /* @__PURE__ */ React4.createElement(
249
435
  "img",
250
436
  {
251
437
  key: element.id,
@@ -261,15 +447,92 @@ var renderElement = (element) => {
261
447
  }
262
448
  );
263
449
  };
264
- var FestivalCardPageRenderer = ({ page }) => {
265
- const backgroundElement = page.elements.find(
266
- (element) => element.type === "image" && Boolean(element.isBackground)
450
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
451
+ var FestivalCardPageRenderer = ({
452
+ page,
453
+ editable = false,
454
+ selectedElementId = null,
455
+ onElementSelect,
456
+ onElementChange
457
+ }) => {
458
+ const [draggingElementId, setDraggingElementId] = useState(null);
459
+ const [resizingElementId, setResizingElementId] = useState(null);
460
+ const stageRef = useRef(null);
461
+ const interactionRef = useRef(null);
462
+ const backgroundElement = useMemo(
463
+ () => page.elements.find(
464
+ (element) => element.type === "image" && Boolean(element.isBackground)
465
+ ),
466
+ [page]
267
467
  );
268
- const foregroundElements = page.elements.filter((element) => !(element.type === "image" && element.isBackground));
269
- return /* @__PURE__ */ React3.createElement(
468
+ const foregroundElements = useMemo(
469
+ () => page.elements.filter((element) => !(element.type === "image" && element.isBackground)),
470
+ [page]
471
+ );
472
+ const updateElementByPointer = (element, interaction, clientX, clientY) => {
473
+ if (!onElementChange || interaction.rect.width <= 0 || interaction.rect.height <= 0) return;
474
+ const xPercent = clamp((clientX - interaction.rect.left) / interaction.rect.width * 100, 0, 100);
475
+ const yPercent = clamp((clientY - interaction.rect.top) / interaction.rect.height * 100, 0, 100);
476
+ if (interaction.mode === "move") {
477
+ onElementChange(element.id, { x: xPercent, y: yPercent });
478
+ return;
479
+ }
480
+ const nextWidth = clamp(Math.abs(xPercent - element.x) * 2, 4, 100);
481
+ if (element.type === "image") {
482
+ const nextHeight = clamp(Math.abs(yPercent - element.y) * 2, 4, 100);
483
+ onElementChange(element.id, { width: nextWidth, height: nextHeight });
484
+ return;
485
+ }
486
+ onElementChange(element.id, { width: nextWidth });
487
+ };
488
+ const beginInteraction = (event, elementId, mode) => {
489
+ if (!editable || !stageRef.current) return;
490
+ event.preventDefault();
491
+ event.stopPropagation();
492
+ const rect = stageRef.current.getBoundingClientRect();
493
+ interactionRef.current = {
494
+ pointerId: event.pointerId,
495
+ elementId,
496
+ mode,
497
+ rect
498
+ };
499
+ event.currentTarget.setPointerCapture(event.pointerId);
500
+ onElementSelect?.(elementId);
501
+ if (mode === "move") {
502
+ setDraggingElementId(elementId);
503
+ setResizingElementId(null);
504
+ } else {
505
+ setResizingElementId(elementId);
506
+ setDraggingElementId(null);
507
+ }
508
+ const element = foregroundElements.find((item) => item.id === elementId);
509
+ if (element) {
510
+ updateElementByPointer(element, interactionRef.current, event.clientX, event.clientY);
511
+ }
512
+ };
513
+ const handlePointerMove = (event) => {
514
+ const interaction = interactionRef.current;
515
+ if (!interaction || interaction.pointerId !== event.pointerId) return;
516
+ const element = foregroundElements.find((item) => item.id === interaction.elementId);
517
+ if (!element) return;
518
+ updateElementByPointer(element, interaction, event.clientX, event.clientY);
519
+ };
520
+ const endInteraction = (event) => {
521
+ const interaction = interactionRef.current;
522
+ if (!interaction || interaction.pointerId !== event.pointerId) return;
523
+ interactionRef.current = null;
524
+ setDraggingElementId(null);
525
+ setResizingElementId(null);
526
+ };
527
+ return /* @__PURE__ */ React4.createElement(
270
528
  "div",
271
529
  {
272
- className: "relative h-full w-full overflow-hidden rounded-2xl",
530
+ ref: stageRef,
531
+ onPointerMove: editable ? handlePointerMove : void 0,
532
+ onPointerUp: editable ? endInteraction : void 0,
533
+ onPointerCancel: editable ? endInteraction : void 0,
534
+ onClick: editable ? () => onElementSelect?.(null) : void 0,
535
+ className: `relative h-full w-full overflow-hidden rounded-2xl ${editable ? "touch-none" : ""}`,
273
536
  style: {
274
537
  backgroundColor: page.background?.color || "#0f172a",
275
538
  backgroundImage: backgroundElement ? `url(${backgroundElement.src})` : page.background?.image ? `url(${page.background.image})` : void 0,
@@ -277,19 +540,251 @@ var FestivalCardPageRenderer = ({ page }) => {
277
540
  backgroundPosition: "center"
278
541
  }
279
542
  },
280
- /* @__PURE__ */ React3.createElement("div", { className: "absolute inset-0 bg-slate-950/20" }),
281
- foregroundElements.map(renderElement)
543
+ /* @__PURE__ */ React4.createElement("div", { className: "absolute inset-0 bg-slate-950/20" }),
544
+ foregroundElements.map((element) => {
545
+ if (!editable) {
546
+ return renderElement(element);
547
+ }
548
+ const isSelected = selectedElementId === element.id;
549
+ const isDragging = draggingElementId === element.id;
550
+ const isResizing = resizingElementId === element.id;
551
+ return /* @__PURE__ */ React4.createElement(
552
+ "div",
553
+ {
554
+ key: element.id,
555
+ role: "button",
556
+ tabIndex: 0,
557
+ onClick: (event) => {
558
+ event.stopPropagation();
559
+ onElementSelect?.(element.id);
560
+ },
561
+ onPointerDown: (event) => beginInteraction(event, element.id, "move"),
562
+ className: `absolute select-none touch-none rounded-md ${isDragging ? "cursor-grabbing" : isResizing ? "cursor-se-resize" : "cursor-grab"} ${isSelected ? "ring-2 ring-sky-300" : "ring-1 ring-white/40"}`,
563
+ style: {
564
+ ...elementStyle(element),
565
+ zIndex: isSelected ? 4 : 2
566
+ }
567
+ },
568
+ element.type === "text" ? /* @__PURE__ */ React4.createElement(
569
+ "div",
570
+ {
571
+ className: "rounded-md bg-black/20 px-2 py-1",
572
+ style: {
573
+ color: element.color || "#f8fafc",
574
+ fontSize: element.fontSize || 18,
575
+ fontWeight: element.fontWeight || 500,
576
+ fontFamily: element.fontFamily || "inherit",
577
+ textAlign: element.align || "left",
578
+ lineHeight: 1.45,
579
+ whiteSpace: "pre-wrap"
580
+ }
581
+ },
582
+ element.content
583
+ ) : /* @__PURE__ */ React4.createElement(
584
+ "img",
585
+ {
586
+ src: element.src,
587
+ alt: element.alt || "festival-card-image",
588
+ draggable: false,
589
+ className: "pointer-events-none h-full w-full",
590
+ style: {
591
+ objectFit: element.fit || "cover",
592
+ borderRadius: element.borderRadius || 0,
593
+ overflow: "hidden",
594
+ boxShadow: "0 12px 30px rgba(2, 6, 23, 0.32)"
595
+ }
596
+ }
597
+ ),
598
+ /* @__PURE__ */ React4.createElement(
599
+ "button",
600
+ {
601
+ type: "button",
602
+ "aria-label": "resize",
603
+ onPointerDown: (event) => beginInteraction(event, element.id, "resize"),
604
+ className: "absolute -bottom-2 -right-2 h-4 w-4 rounded-full border border-white bg-sky-500 shadow"
605
+ }
606
+ )
607
+ );
608
+ })
282
609
  );
283
610
  };
284
611
 
285
612
  // src/festivalCard/components/FestivalCardBook3D.tsx
286
- var FestivalCardBook3D = ({ config, className }) => {
287
- const [currentPage, setCurrentPage] = useState(0);
613
+ var loadImage = (src) => new Promise((resolve, reject) => {
614
+ const image = new window.Image();
615
+ image.crossOrigin = "anonymous";
616
+ image.decoding = "async";
617
+ image.onload = () => resolve(image);
618
+ image.onerror = () => reject(new Error(`\u56FE\u7247\u52A0\u8F7D\u5931\u8D25: ${src}`));
619
+ image.src = src;
620
+ });
621
+ var drawImageWithFit = (ctx, image, left, top, width, height, fit) => {
622
+ const imageRatio = image.width / image.height;
623
+ const boxRatio = width / height;
624
+ let drawWidth = width;
625
+ let drawHeight = height;
626
+ let offsetX = left;
627
+ let offsetY = top;
628
+ if (fit === "cover") {
629
+ if (imageRatio > boxRatio) {
630
+ drawHeight = height;
631
+ drawWidth = height * imageRatio;
632
+ offsetX = left - (drawWidth - width) / 2;
633
+ } else {
634
+ drawWidth = width;
635
+ drawHeight = width / imageRatio;
636
+ offsetY = top - (drawHeight - height) / 2;
637
+ }
638
+ } else if (imageRatio > boxRatio) {
639
+ drawWidth = width;
640
+ drawHeight = width / imageRatio;
641
+ offsetY = top + (height - drawHeight) / 2;
642
+ } else {
643
+ drawHeight = height;
644
+ drawWidth = height * imageRatio;
645
+ offsetX = left + (width - drawWidth) / 2;
646
+ }
647
+ ctx.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
648
+ };
649
+ var withRoundedClip = (ctx, left, top, width, height, radius, draw) => {
650
+ const safeRadius = Math.max(0, Math.min(radius, Math.min(width, height) / 2));
651
+ if (safeRadius <= 0) {
652
+ draw();
653
+ return;
654
+ }
655
+ ctx.save();
656
+ ctx.beginPath();
657
+ ctx.moveTo(left + safeRadius, top);
658
+ ctx.lineTo(left + width - safeRadius, top);
659
+ ctx.quadraticCurveTo(left + width, top, left + width, top + safeRadius);
660
+ ctx.lineTo(left + width, top + height - safeRadius);
661
+ ctx.quadraticCurveTo(left + width, top + height, left + width - safeRadius, top + height);
662
+ ctx.lineTo(left + safeRadius, top + height);
663
+ ctx.quadraticCurveTo(left, top + height, left, top + height - safeRadius);
664
+ ctx.lineTo(left, top + safeRadius);
665
+ ctx.quadraticCurveTo(left, top, left + safeRadius, top);
666
+ ctx.closePath();
667
+ ctx.clip();
668
+ draw();
669
+ ctx.restore();
670
+ };
671
+ var drawMultilineText = (ctx, text, left, top, maxWidth, lineHeight) => {
672
+ const paragraphs = text.split("\n");
673
+ let currentY = top;
674
+ paragraphs.forEach((paragraph, index) => {
675
+ const words = paragraph.split("");
676
+ let line = "";
677
+ for (const word of words) {
678
+ const testLine = line + word;
679
+ if (ctx.measureText(testLine).width > maxWidth && line) {
680
+ ctx.fillText(line, left, currentY);
681
+ line = word;
682
+ currentY += lineHeight;
683
+ } else {
684
+ line = testLine;
685
+ }
686
+ }
687
+ ctx.fillText(line, left, currentY);
688
+ currentY += lineHeight;
689
+ if (index < paragraphs.length - 1) {
690
+ currentY += lineHeight * 0.2;
691
+ }
692
+ });
693
+ };
694
+ var exportPageToPng = async (page, fileName) => {
695
+ const width = 1080;
696
+ const height = 1440;
697
+ const canvas = document.createElement("canvas");
698
+ canvas.width = width;
699
+ canvas.height = height;
700
+ const ctx = canvas.getContext("2d");
701
+ if (!ctx) throw new Error("\u65E0\u6CD5\u521B\u5EFA Canvas \u4E0A\u4E0B\u6587");
702
+ ctx.fillStyle = page.background?.color || "#0f172a";
703
+ ctx.fillRect(0, 0, width, height);
704
+ const backgroundElement = page.elements.find(
705
+ (element) => element.type === "image" && Boolean(element.isBackground)
706
+ );
707
+ const backgroundImageSrc = backgroundElement?.src || page.background?.image;
708
+ if (backgroundImageSrc) {
709
+ const image = await loadImage(backgroundImageSrc);
710
+ drawImageWithFit(ctx, image, 0, 0, width, height, "cover");
711
+ }
712
+ const foregroundElements = page.elements.filter((element) => !(element.type === "image" && element.isBackground));
713
+ for (const element of foregroundElements) {
714
+ const elementWidth = width * (element.width ?? 70) / 100;
715
+ const elementHeight = element.height ? height * element.height / 100 : void 0;
716
+ const centerX = width * element.x / 100;
717
+ const centerY = height * element.y / 100;
718
+ const left = centerX - elementWidth / 2;
719
+ if (element.type === "image") {
720
+ const image = await loadImage(element.src);
721
+ const drawHeight = elementHeight ?? elementWidth;
722
+ const boxTop = centerY - drawHeight / 2;
723
+ withRoundedClip(ctx, left, boxTop, elementWidth, drawHeight, element.borderRadius ?? 0, () => {
724
+ drawImageWithFit(ctx, image, left, boxTop, elementWidth, drawHeight, element.fit || "cover");
725
+ });
726
+ continue;
727
+ }
728
+ const fontSize = (element.fontSize || 18) * 1.5;
729
+ ctx.fillStyle = element.color || "#f8fafc";
730
+ ctx.font = `${element.fontWeight || 500} ${fontSize}px ${element.fontFamily || "sans-serif"}`;
731
+ ctx.textBaseline = "top";
732
+ ctx.textAlign = element.align || "left";
733
+ const textX = element.align === "center" ? centerX : element.align === "right" ? left + elementWidth : left;
734
+ drawMultilineText(ctx, element.content || "", textX, centerY - fontSize * 0.72, elementWidth, fontSize * 1.45);
735
+ }
736
+ const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
737
+ if (!blob) throw new Error("\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
738
+ const url = URL.createObjectURL(blob);
739
+ const anchor = document.createElement("a");
740
+ anchor.href = url;
741
+ anchor.download = fileName;
742
+ anchor.click();
743
+ URL.revokeObjectURL(url);
744
+ };
745
+ var FestivalCardBook3D = ({
746
+ config,
747
+ className,
748
+ editable = false,
749
+ enableExportImage = !editable,
750
+ currentPage: currentPageProp,
751
+ onCurrentPageChange,
752
+ selectedElementId = null,
753
+ onSelectedElementChange,
754
+ onElementChange
755
+ }) => {
756
+ const [internalCurrentPage, setInternalCurrentPage] = useState(0);
757
+ const [exporting, setExporting] = useState(false);
288
758
  const normalized = useMemo(() => normalizeFestivalCardConfig(config), [config]);
289
759
  const pages = normalized.pages;
760
+ const currentPage = typeof currentPageProp === "number" ? currentPageProp : internalCurrentPage;
761
+ const setCurrentPage = (updater) => {
762
+ const prev = currentPage;
763
+ const nextValue = typeof updater === "function" ? updater(prev) : updater;
764
+ if (typeof currentPageProp === "number") {
765
+ onCurrentPageChange?.(nextValue);
766
+ return;
767
+ }
768
+ setInternalCurrentPage(nextValue);
769
+ onCurrentPageChange?.(nextValue);
770
+ };
290
771
  const canPrev = currentPage > 0;
291
772
  const canNext = currentPage < pages.length - 1;
292
- return /* @__PURE__ */ React3.createElement("div", { className }, /* @__PURE__ */ React3.createElement("div", { className: "w-full min-h-screen px-0 py-4" }, /* @__PURE__ */ React3.createElement("div", { className: "mx-auto w-full text-center text-slate-100" }, /* @__PURE__ */ React3.createElement("h3", { className: "mb-3 text-lg font-semibold" }, normalized.coverTitle || "Festival Card")), /* @__PURE__ */ React3.createElement("div", { className: "mx-auto w-full" }, /* @__PURE__ */ React3.createElement("div", { className: "relative h-[calc(100vh-170px)] min-h-[460px]" }, pages.map((page, index) => /* @__PURE__ */ React3.createElement(
773
+ const currentPageData = pages[currentPage];
774
+ const handleExportCurrentPage = async () => {
775
+ if (!currentPageData || exporting) return;
776
+ setExporting(true);
777
+ try {
778
+ const base = normalized.id || "festival-card";
779
+ const fileName = `${base}-page-${currentPage + 1}.png`;
780
+ await exportPageToPng(currentPageData, fileName);
781
+ } catch (error) {
782
+ window.alert(error.message || "\u5BFC\u51FA\u56FE\u7247\u5931\u8D25");
783
+ } finally {
784
+ setExporting(false);
785
+ }
786
+ };
787
+ return /* @__PURE__ */ React4.createElement("div", { className }, /* @__PURE__ */ React4.createElement("div", { className: "w-full min-h-screen px-0 py-4" }, /* @__PURE__ */ React4.createElement("div", { className: "mx-auto w-full text-center text-slate-100" }, /* @__PURE__ */ React4.createElement("h3", { className: "mb-3 text-lg font-semibold" }, normalized.coverTitle || "Festival Card")), /* @__PURE__ */ React4.createElement("div", { className: "mx-auto w-full" }, /* @__PURE__ */ React4.createElement("div", { className: "relative h-[calc(100vh-170px)] min-h-[460px]" }, pages.map((page, index) => /* @__PURE__ */ React4.createElement(
293
788
  "div",
294
789
  {
295
790
  key: page.id,
@@ -299,8 +794,17 @@ var FestivalCardBook3D = ({ config, className }) => {
299
794
  pointerEvents: index === currentPage ? "auto" : "none"
300
795
  }
301
796
  },
302
- /* @__PURE__ */ React3.createElement(FestivalCardPageRenderer, { page })
303
- )))), /* @__PURE__ */ React3.createElement("div", { className: "mt-4 flex justify-center gap-3" }, /* @__PURE__ */ React3.createElement(
797
+ /* @__PURE__ */ React4.createElement(
798
+ FestivalCardPageRenderer,
799
+ {
800
+ page,
801
+ editable: editable && index === currentPage,
802
+ selectedElementId,
803
+ onElementSelect: onSelectedElementChange,
804
+ onElementChange: (elementId, patch) => onElementChange?.(index, elementId, patch)
805
+ }
806
+ )
807
+ )))), /* @__PURE__ */ React4.createElement("div", { className: "mt-4 flex justify-center gap-3" }, /* @__PURE__ */ React4.createElement(
304
808
  "button",
305
809
  {
306
810
  type: "button",
@@ -309,7 +813,7 @@ var FestivalCardBook3D = ({ config, className }) => {
309
813
  className: "rounded-full bg-white px-5 py-2 text-sm font-medium text-slate-900 disabled:cursor-not-allowed disabled:opacity-45"
310
814
  },
311
815
  "\u4E0A\u4E00\u9875"
312
- ), /* @__PURE__ */ React3.createElement(
816
+ ), /* @__PURE__ */ React4.createElement(
313
817
  "button",
314
818
  {
315
819
  type: "button",
@@ -318,7 +822,7 @@ var FestivalCardBook3D = ({ config, className }) => {
318
822
  className: "rounded-full bg-sky-300 px-5 py-2 text-sm font-medium text-slate-900 disabled:cursor-not-allowed disabled:opacity-45"
319
823
  },
320
824
  "\u4E0B\u4E00\u9875"
321
- ))), normalized.backgroundMusic?.src ? /* @__PURE__ */ React3.createElement(
825
+ ))), normalized.backgroundMusic?.src ? /* @__PURE__ */ React4.createElement(
322
826
  "audio",
323
827
  {
324
828
  src: normalized.backgroundMusic.src,
@@ -327,6 +831,24 @@ var FestivalCardBook3D = ({ config, className }) => {
327
831
  controls: true,
328
832
  className: "mt-3 w-full"
329
833
  }
834
+ ) : null, enableExportImage ? /* @__PURE__ */ React4.createElement(
835
+ FloatingMenu_default,
836
+ {
837
+ initialPosition: { x: 24, y: 120 },
838
+ trigger: /* @__PURE__ */ React4.createElement("div", { className: "text-lg leading-none text-slate-700", "aria-hidden": true }, "\u2301"),
839
+ menu: /* @__PURE__ */ React4.createElement("div", { className: "grid gap-2" }, /* @__PURE__ */ React4.createElement("div", { className: "text-xs font-semibold tracking-wide text-slate-500" }, "\u8D3A\u5361\u5DE5\u5177"), /* @__PURE__ */ React4.createElement(
840
+ "button",
841
+ {
842
+ type: "button",
843
+ onClick: () => void handleExportCurrentPage(),
844
+ disabled: exporting,
845
+ className: "rounded-lg bg-sky-600 px-3 py-2 text-left text-sm font-medium text-white disabled:opacity-60"
846
+ },
847
+ exporting ? "\u5BFC\u51FA\u4E2D..." : `\u5BFC\u51FA\u7B2C ${currentPage + 1} \u9875 PNG`
848
+ )),
849
+ triggerClassName: "bg-white/95 backdrop-blur",
850
+ menuClassName: "bg-white/95 backdrop-blur"
851
+ }
330
852
  ) : null);
331
853
  };
332
854
  var createTextElement = (pageIndex) => ({
@@ -353,8 +875,22 @@ var createImageElement = (pageIndex) => ({
353
875
  fit: "cover",
354
876
  borderRadius: 12
355
877
  });
356
- var FestivalCardConfigEditor = ({ value, onChange }) => {
357
- const [activePageIndex, setActivePageIndex] = useState(0);
878
+ var FestivalCardConfigEditor = ({
879
+ value,
880
+ onChange,
881
+ activePageIndex: activePageIndexProp,
882
+ onActivePageIndexChange,
883
+ selectedElementId
884
+ }) => {
885
+ const [internalActivePageIndex, setInternalActivePageIndex] = useState(0);
886
+ const activePageIndex = activePageIndexProp ?? internalActivePageIndex;
887
+ const setActivePageIndex = (index) => {
888
+ if (typeof activePageIndexProp === "number") {
889
+ onActivePageIndexChange?.(index);
890
+ return;
891
+ }
892
+ setInternalActivePageIndex(index);
893
+ };
358
894
  const page = value.pages[activePageIndex];
359
895
  const canEditPage = Boolean(page);
360
896
  const pageOptions = useMemo(() => value.pages.map((_, index) => index), [value.pages]);
@@ -395,7 +931,7 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
395
931
  });
396
932
  };
397
933
  const numberFieldClassName = "w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none focus:border-sky-400 focus:ring-2 focus:ring-sky-100";
398
- return /* @__PURE__ */ React3.createElement("div", { className: "rounded-2xl border border-slate-200 bg-white p-4 text-slate-900 shadow-sm" }, /* @__PURE__ */ React3.createElement("div", { className: "grid gap-3" }, /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React3.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u6570\u91CF"), /* @__PURE__ */ React3.createElement(
934
+ return /* @__PURE__ */ React4.createElement("div", { className: "rounded-2xl border border-slate-200 bg-white p-4 text-slate-900 shadow-sm" }, /* @__PURE__ */ React4.createElement("div", { className: "grid gap-3" }, /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React4.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u6570\u91CF"), /* @__PURE__ */ React4.createElement(
399
935
  "input",
400
936
  {
401
937
  type: "number",
@@ -405,7 +941,7 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
405
941
  onChange: (event) => handlePageCountChange(Number(event.target.value)),
406
942
  className: "rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-900 outline-none focus:border-sky-400 focus:ring-2 focus:ring-sky-100"
407
943
  }
408
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React3.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u80CC\u666F\u97F3\u4E50 URL"), /* @__PURE__ */ React3.createElement(
944
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React4.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u80CC\u666F\u97F3\u4E50 URL"), /* @__PURE__ */ React4.createElement(
409
945
  "input",
410
946
  {
411
947
  type: "url",
@@ -419,7 +955,7 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
419
955
  }),
420
956
  className: "rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-900 outline-none focus:border-sky-400 focus:ring-2 focus:ring-sky-100"
421
957
  }
422
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React3.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u6807\u9898"), /* @__PURE__ */ React3.createElement(
958
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React4.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u6807\u9898"), /* @__PURE__ */ React4.createElement(
423
959
  "input",
424
960
  {
425
961
  type: "text",
@@ -427,7 +963,7 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
427
963
  onChange: (event) => updatePage({ title: event.target.value }),
428
964
  className: "rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-900 outline-none focus:border-sky-400 focus:ring-2 focus:ring-sky-100"
429
965
  }
430
- )), /* @__PURE__ */ React3.createElement("div", { className: "grid gap-2 sm:grid-cols-2" }, /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React3.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u80CC\u666F\u8272"), /* @__PURE__ */ React3.createElement(
966
+ )), /* @__PURE__ */ React4.createElement("div", { className: "grid gap-2 sm:grid-cols-2" }, /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React4.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u80CC\u666F\u8272"), /* @__PURE__ */ React4.createElement(
431
967
  "input",
432
968
  {
433
969
  type: "color",
@@ -440,7 +976,7 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
440
976
  }),
441
977
  className: "h-10 w-full rounded-lg border border-slate-300 bg-white p-1"
442
978
  }
443
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React3.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u80CC\u666F\u56FE URL"), /* @__PURE__ */ React3.createElement(
979
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React4.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u80CC\u666F\u56FE URL"), /* @__PURE__ */ React4.createElement(
444
980
  "input",
445
981
  {
446
982
  type: "url",
@@ -453,15 +989,15 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
453
989
  }),
454
990
  className: "rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-900 outline-none focus:border-sky-400 focus:ring-2 focus:ring-sky-100"
455
991
  }
456
- ))), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React3.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u7F16\u8F91\u9875\u9762"), /* @__PURE__ */ React3.createElement(
992
+ ))), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React4.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u7F16\u8F91\u9875\u9762"), /* @__PURE__ */ React4.createElement(
457
993
  "select",
458
994
  {
459
995
  value: activePageIndex,
460
996
  onChange: (event) => setActivePageIndex(Number(event.target.value)),
461
997
  className: "rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-900 outline-none focus:border-sky-400 focus:ring-2 focus:ring-sky-100"
462
998
  },
463
- pageOptions.map((index) => /* @__PURE__ */ React3.createElement("option", { key: index, value: index }, "\u7B2C ", index + 1, " \u9875"))
464
- ))), canEditPage ? /* @__PURE__ */ React3.createElement("div", { className: "mt-4" }, /* @__PURE__ */ React3.createElement("div", { className: "mb-3 flex gap-2" }, /* @__PURE__ */ React3.createElement(
999
+ pageOptions.map((index) => /* @__PURE__ */ React4.createElement("option", { key: index, value: index }, "\u7B2C ", index + 1, " \u9875"))
1000
+ ))), canEditPage ? /* @__PURE__ */ React4.createElement("div", { className: "mt-4" }, /* @__PURE__ */ React4.createElement("div", { className: "mb-3 flex gap-2" }, /* @__PURE__ */ React4.createElement(
465
1001
  "button",
466
1002
  {
467
1003
  type: "button",
@@ -474,7 +1010,7 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
474
1010
  className: "rounded-lg bg-slate-900 px-3 py-2 text-sm font-medium text-white"
475
1011
  },
476
1012
  "+ \u6587\u5B57"
477
- ), /* @__PURE__ */ React3.createElement(
1013
+ ), /* @__PURE__ */ React4.createElement(
478
1014
  "button",
479
1015
  {
480
1016
  type: "button",
@@ -487,166 +1023,174 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
487
1023
  className: "rounded-lg bg-sky-600 px-3 py-2 text-sm font-medium text-white"
488
1024
  },
489
1025
  "+ \u56FE\u7247"
490
- )), /* @__PURE__ */ React3.createElement("div", { className: "grid max-h-[340px] gap-2.5 overflow-auto pr-1" }, (page?.elements ?? []).map((element) => /* @__PURE__ */ React3.createElement("div", { key: element.id, className: "rounded-xl border border-slate-200 bg-slate-50 p-3" }, /* @__PURE__ */ React3.createElement("div", { className: "mb-2 flex items-center justify-between" }, /* @__PURE__ */ React3.createElement("div", { className: "text-xs font-semibold tracking-wide text-slate-500" }, element.type.toUpperCase()), /* @__PURE__ */ React3.createElement(
491
- "button",
492
- {
493
- type: "button",
494
- onClick: () => removeElement(element.id),
495
- className: "rounded-md border border-rose-300 bg-rose-50 px-2 py-1 text-xs font-medium text-rose-700"
496
- },
497
- "\u5220\u9664"
498
- )), element.type === "text" ? /* @__PURE__ */ React3.createElement("div", { className: "grid gap-2" }, /* @__PURE__ */ React3.createElement(
499
- "textarea",
500
- {
501
- value: element.content,
502
- onChange: (event) => updateElement(element.id, { content: event.target.value }),
503
- rows: 3,
504
- className: "w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none focus:border-sky-400 focus:ring-2 focus:ring-sky-100"
505
- }
506
- ), /* @__PURE__ */ React3.createElement("div", { className: "grid gap-2 sm:grid-cols-2" }, /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "X(%)", /* @__PURE__ */ React3.createElement(
507
- "input",
508
- {
509
- type: "number",
510
- value: element.x,
511
- onChange: (event) => updateElement(element.id, { x: Number(event.target.value) }),
512
- className: numberFieldClassName
513
- }
514
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "Y(%)", /* @__PURE__ */ React3.createElement(
515
- "input",
516
- {
517
- type: "number",
518
- value: element.y,
519
- onChange: (event) => updateElement(element.id, { y: Number(event.target.value) }),
520
- className: numberFieldClassName
521
- }
522
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5BBD\u5EA6(%)", /* @__PURE__ */ React3.createElement(
523
- "input",
524
- {
525
- type: "number",
526
- value: element.width ?? 70,
527
- onChange: (event) => updateElement(element.id, { width: Number(event.target.value) }),
528
- className: numberFieldClassName
529
- }
530
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5B57\u53F7(px)", /* @__PURE__ */ React3.createElement(
531
- "input",
532
- {
533
- type: "number",
534
- value: element.fontSize ?? 18,
535
- onChange: (event) => updateElement(element.id, { fontSize: Number(event.target.value) }),
536
- className: numberFieldClassName
537
- }
538
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5B57\u91CD", /* @__PURE__ */ React3.createElement(
539
- "input",
540
- {
541
- type: "number",
542
- min: 100,
543
- max: 900,
544
- step: 100,
545
- value: element.fontWeight ?? 500,
546
- onChange: (event) => updateElement(element.id, { fontWeight: Number(event.target.value) }),
547
- className: numberFieldClassName
548
- }
549
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5BF9\u9F50", /* @__PURE__ */ React3.createElement(
550
- "select",
551
- {
552
- value: element.align || "left",
553
- onChange: (event) => updateElement(element.id, { align: event.target.value }),
554
- className: numberFieldClassName
555
- },
556
- /* @__PURE__ */ React3.createElement("option", { value: "left" }, "left"),
557
- /* @__PURE__ */ React3.createElement("option", { value: "center" }, "center"),
558
- /* @__PURE__ */ React3.createElement("option", { value: "right" }, "right")
559
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600 sm:col-span-2" }, "\u5B57\u4F53", /* @__PURE__ */ React3.createElement(
560
- "input",
561
- {
562
- type: "text",
563
- value: element.fontFamily || "",
564
- onChange: (event) => updateElement(element.id, { fontFamily: event.target.value }),
565
- placeholder: "inherit / serif / sans-serif / PingFang SC",
566
- className: numberFieldClassName
567
- }
568
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600 sm:col-span-2" }, "\u6587\u5B57\u989C\u8272", /* @__PURE__ */ React3.createElement("div", { className: "grid grid-cols-[64px_1fr] gap-2" }, /* @__PURE__ */ React3.createElement(
569
- "input",
570
- {
571
- type: "color",
572
- value: element.color || "#ffffff",
573
- onChange: (event) => updateElement(element.id, { color: event.target.value }),
574
- className: "h-10 rounded-lg border border-slate-300 bg-white p-1"
575
- }
576
- ), /* @__PURE__ */ React3.createElement(
577
- "input",
578
- {
579
- type: "text",
580
- value: element.color || "#ffffff",
581
- onChange: (event) => updateElement(element.id, { color: event.target.value }),
582
- className: numberFieldClassName
583
- }
584
- ))))) : /* @__PURE__ */ React3.createElement("div", { className: "grid gap-2" }, /* @__PURE__ */ React3.createElement(
585
- "input",
586
- {
587
- type: "url",
588
- value: element.src,
589
- onChange: (event) => updateElement(element.id, { src: event.target.value }),
590
- className: "w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none focus:border-sky-400 focus:ring-2 focus:ring-sky-100"
591
- }
592
- ), /* @__PURE__ */ React3.createElement("div", { className: "grid gap-2 sm:grid-cols-2" }, /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "X(%)", /* @__PURE__ */ React3.createElement(
593
- "input",
594
- {
595
- type: "number",
596
- value: element.x,
597
- onChange: (event) => updateElement(element.id, { x: Number(event.target.value) }),
598
- className: numberFieldClassName
599
- }
600
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "Y(%)", /* @__PURE__ */ React3.createElement(
601
- "input",
602
- {
603
- type: "number",
604
- value: element.y,
605
- onChange: (event) => updateElement(element.id, { y: Number(event.target.value) }),
606
- className: numberFieldClassName
607
- }
608
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5BBD\u5EA6(%)", /* @__PURE__ */ React3.createElement(
609
- "input",
610
- {
611
- type: "number",
612
- value: element.width ?? 60,
613
- onChange: (event) => updateElement(element.id, { width: Number(event.target.value) }),
614
- className: numberFieldClassName
615
- }
616
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u9AD8\u5EA6(%)", /* @__PURE__ */ React3.createElement(
617
- "input",
618
- {
619
- type: "number",
620
- value: element.height ?? 40,
621
- onChange: (event) => updateElement(element.id, { height: Number(event.target.value) }),
622
- className: numberFieldClassName
623
- }
624
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5706\u89D2(px)", /* @__PURE__ */ React3.createElement(
625
- "input",
626
- {
627
- type: "number",
628
- value: element.borderRadius ?? 0,
629
- onChange: (event) => updateElement(element.id, { borderRadius: Number(event.target.value) }),
630
- className: numberFieldClassName
631
- }
632
- )), /* @__PURE__ */ React3.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u586B\u5145", /* @__PURE__ */ React3.createElement(
633
- "select",
1026
+ )), /* @__PURE__ */ React4.createElement("div", { className: "grid max-h-[340px] gap-2.5 overflow-auto pr-1" }, (page?.elements ?? []).map((element) => /* @__PURE__ */ React4.createElement(
1027
+ "div",
634
1028
  {
635
- value: element.fit || "cover",
636
- onChange: (event) => updateElement(element.id, { fit: event.target.value }),
637
- className: numberFieldClassName
1029
+ key: element.id,
1030
+ className: `rounded-xl border bg-slate-50 p-3 ${selectedElementId === element.id ? "border-sky-400 ring-2 ring-sky-100" : "border-slate-200"}`
638
1031
  },
639
- /* @__PURE__ */ React3.createElement("option", { value: "cover" }, "cover"),
640
- /* @__PURE__ */ React3.createElement("option", { value: "contain" }, "contain")
641
- ))), /* @__PURE__ */ React3.createElement("label", { className: "inline-flex items-center gap-2 text-sm text-slate-700" }, /* @__PURE__ */ React3.createElement(
642
- "input",
643
- {
644
- type: "checkbox",
645
- checked: Boolean(element.isBackground),
646
- onChange: (event) => updateElement(element.id, { isBackground: event.target.checked }),
647
- className: "h-4 w-4 rounded border-slate-300 text-sky-600"
648
- }
649
- ), "\u4F5C\u4E3A\u672C\u9875\u80CC\u666F\u56FE")))))) : null);
1032
+ /* @__PURE__ */ React4.createElement("div", { className: "mb-2 flex items-center justify-between" }, /* @__PURE__ */ React4.createElement("div", { className: "text-xs font-semibold tracking-wide text-slate-500" }, element.type.toUpperCase()), /* @__PURE__ */ React4.createElement(
1033
+ "button",
1034
+ {
1035
+ type: "button",
1036
+ onClick: () => removeElement(element.id),
1037
+ className: "rounded-md border border-rose-300 bg-rose-50 px-2 py-1 text-xs font-medium text-rose-700"
1038
+ },
1039
+ "\u5220\u9664"
1040
+ )),
1041
+ element.type === "text" ? /* @__PURE__ */ React4.createElement("div", { className: "grid gap-2" }, /* @__PURE__ */ React4.createElement(
1042
+ "textarea",
1043
+ {
1044
+ value: element.content,
1045
+ onChange: (event) => updateElement(element.id, { content: event.target.value }),
1046
+ rows: 3,
1047
+ className: "w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none focus:border-sky-400 focus:ring-2 focus:ring-sky-100"
1048
+ }
1049
+ ), /* @__PURE__ */ React4.createElement("div", { className: "grid gap-2 sm:grid-cols-2" }, /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "X(%)", /* @__PURE__ */ React4.createElement(
1050
+ "input",
1051
+ {
1052
+ type: "number",
1053
+ value: element.x,
1054
+ onChange: (event) => updateElement(element.id, { x: Number(event.target.value) }),
1055
+ className: numberFieldClassName
1056
+ }
1057
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "Y(%)", /* @__PURE__ */ React4.createElement(
1058
+ "input",
1059
+ {
1060
+ type: "number",
1061
+ value: element.y,
1062
+ onChange: (event) => updateElement(element.id, { y: Number(event.target.value) }),
1063
+ className: numberFieldClassName
1064
+ }
1065
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5BBD\u5EA6(%)", /* @__PURE__ */ React4.createElement(
1066
+ "input",
1067
+ {
1068
+ type: "number",
1069
+ value: element.width ?? 70,
1070
+ onChange: (event) => updateElement(element.id, { width: Number(event.target.value) }),
1071
+ className: numberFieldClassName
1072
+ }
1073
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5B57\u53F7(px)", /* @__PURE__ */ React4.createElement(
1074
+ "input",
1075
+ {
1076
+ type: "number",
1077
+ value: element.fontSize ?? 18,
1078
+ onChange: (event) => updateElement(element.id, { fontSize: Number(event.target.value) }),
1079
+ className: numberFieldClassName
1080
+ }
1081
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5B57\u91CD", /* @__PURE__ */ React4.createElement(
1082
+ "input",
1083
+ {
1084
+ type: "number",
1085
+ min: 100,
1086
+ max: 900,
1087
+ step: 100,
1088
+ value: element.fontWeight ?? 500,
1089
+ onChange: (event) => updateElement(element.id, { fontWeight: Number(event.target.value) }),
1090
+ className: numberFieldClassName
1091
+ }
1092
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5BF9\u9F50", /* @__PURE__ */ React4.createElement(
1093
+ "select",
1094
+ {
1095
+ value: element.align || "left",
1096
+ onChange: (event) => updateElement(element.id, { align: event.target.value }),
1097
+ className: numberFieldClassName
1098
+ },
1099
+ /* @__PURE__ */ React4.createElement("option", { value: "left" }, "left"),
1100
+ /* @__PURE__ */ React4.createElement("option", { value: "center" }, "center"),
1101
+ /* @__PURE__ */ React4.createElement("option", { value: "right" }, "right")
1102
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600 sm:col-span-2" }, "\u5B57\u4F53", /* @__PURE__ */ React4.createElement(
1103
+ "input",
1104
+ {
1105
+ type: "text",
1106
+ value: element.fontFamily || "",
1107
+ onChange: (event) => updateElement(element.id, { fontFamily: event.target.value }),
1108
+ placeholder: "inherit / serif / sans-serif / PingFang SC",
1109
+ className: numberFieldClassName
1110
+ }
1111
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600 sm:col-span-2" }, "\u6587\u5B57\u989C\u8272", /* @__PURE__ */ React4.createElement("div", { className: "grid grid-cols-[64px_1fr] gap-2" }, /* @__PURE__ */ React4.createElement(
1112
+ "input",
1113
+ {
1114
+ type: "color",
1115
+ value: element.color || "#ffffff",
1116
+ onChange: (event) => updateElement(element.id, { color: event.target.value }),
1117
+ className: "h-10 rounded-lg border border-slate-300 bg-white p-1"
1118
+ }
1119
+ ), /* @__PURE__ */ React4.createElement(
1120
+ "input",
1121
+ {
1122
+ type: "text",
1123
+ value: element.color || "#ffffff",
1124
+ onChange: (event) => updateElement(element.id, { color: event.target.value }),
1125
+ className: numberFieldClassName
1126
+ }
1127
+ ))))) : /* @__PURE__ */ React4.createElement("div", { className: "grid gap-2" }, /* @__PURE__ */ React4.createElement(
1128
+ "input",
1129
+ {
1130
+ type: "url",
1131
+ value: element.src,
1132
+ onChange: (event) => updateElement(element.id, { src: event.target.value }),
1133
+ className: "w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none focus:border-sky-400 focus:ring-2 focus:ring-sky-100"
1134
+ }
1135
+ ), /* @__PURE__ */ React4.createElement("div", { className: "grid gap-2 sm:grid-cols-2" }, /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "X(%)", /* @__PURE__ */ React4.createElement(
1136
+ "input",
1137
+ {
1138
+ type: "number",
1139
+ value: element.x,
1140
+ onChange: (event) => updateElement(element.id, { x: Number(event.target.value) }),
1141
+ className: numberFieldClassName
1142
+ }
1143
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "Y(%)", /* @__PURE__ */ React4.createElement(
1144
+ "input",
1145
+ {
1146
+ type: "number",
1147
+ value: element.y,
1148
+ onChange: (event) => updateElement(element.id, { y: Number(event.target.value) }),
1149
+ className: numberFieldClassName
1150
+ }
1151
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5BBD\u5EA6(%)", /* @__PURE__ */ React4.createElement(
1152
+ "input",
1153
+ {
1154
+ type: "number",
1155
+ value: element.width ?? 60,
1156
+ onChange: (event) => updateElement(element.id, { width: Number(event.target.value) }),
1157
+ className: numberFieldClassName
1158
+ }
1159
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u9AD8\u5EA6(%)", /* @__PURE__ */ React4.createElement(
1160
+ "input",
1161
+ {
1162
+ type: "number",
1163
+ value: element.height ?? 40,
1164
+ onChange: (event) => updateElement(element.id, { height: Number(event.target.value) }),
1165
+ className: numberFieldClassName
1166
+ }
1167
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5706\u89D2(px)", /* @__PURE__ */ React4.createElement(
1168
+ "input",
1169
+ {
1170
+ type: "number",
1171
+ value: element.borderRadius ?? 0,
1172
+ onChange: (event) => updateElement(element.id, { borderRadius: Number(event.target.value) }),
1173
+ className: numberFieldClassName
1174
+ }
1175
+ )), /* @__PURE__ */ React4.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u586B\u5145", /* @__PURE__ */ React4.createElement(
1176
+ "select",
1177
+ {
1178
+ value: element.fit || "cover",
1179
+ onChange: (event) => updateElement(element.id, { fit: event.target.value }),
1180
+ className: numberFieldClassName
1181
+ },
1182
+ /* @__PURE__ */ React4.createElement("option", { value: "cover" }, "cover"),
1183
+ /* @__PURE__ */ React4.createElement("option", { value: "contain" }, "contain")
1184
+ ))), /* @__PURE__ */ React4.createElement("label", { className: "inline-flex items-center gap-2 text-sm text-slate-700" }, /* @__PURE__ */ React4.createElement(
1185
+ "input",
1186
+ {
1187
+ type: "checkbox",
1188
+ checked: Boolean(element.isBackground),
1189
+ onChange: (event) => updateElement(element.id, { isBackground: event.target.checked }),
1190
+ className: "h-4 w-4 rounded border-slate-300 text-sky-600"
1191
+ }
1192
+ ), "\u4F5C\u4E3A\u672C\u9875\u80CC\u666F\u56FE"))
1193
+ )))) : null);
650
1194
  };
651
1195
 
652
1196
  // src/festivalCard/components/FestivalCardStudio.tsx
@@ -656,8 +1200,55 @@ var FestivalCardStudio = ({ initialConfig, fetchConfig, onSave }) => {
656
1200
  fetchConfig,
657
1201
  onSave
658
1202
  });
659
- if (loading) return /* @__PURE__ */ React3.createElement("div", null, "\u52A0\u8F7D\u4E2D...");
660
- return /* @__PURE__ */ React3.createElement("div", { className: "grid items-start gap-4 lg:grid-cols-[1.45fr_1fr]" }, /* @__PURE__ */ React3.createElement(FestivalCardBook3D, { config, className: "h-full" }), /* @__PURE__ */ React3.createElement("div", { className: "lg:sticky lg:top-4" }, /* @__PURE__ */ React3.createElement(FestivalCardConfigEditor, { value: config, onChange: setConfig }), onSave ? /* @__PURE__ */ React3.createElement(
1203
+ const [activePageIndex, setActivePageIndex] = useState(0);
1204
+ const [selectedElementId, setSelectedElementId] = useState(null);
1205
+ useEffect(() => {
1206
+ if (config.pages.length === 0) return;
1207
+ if (activePageIndex <= config.pages.length - 1) return;
1208
+ setActivePageIndex(config.pages.length - 1);
1209
+ }, [activePageIndex, config.pages.length]);
1210
+ const updateElementByPreview = (pageIndex, elementId, patch) => {
1211
+ setConfig((prev) => ({
1212
+ ...prev,
1213
+ pages: prev.pages.map(
1214
+ (page, index) => index === pageIndex ? {
1215
+ ...page,
1216
+ elements: page.elements.map(
1217
+ (element) => element.id === elementId ? { ...element, ...patch } : element
1218
+ )
1219
+ } : page
1220
+ )
1221
+ }));
1222
+ };
1223
+ if (loading) return /* @__PURE__ */ React4.createElement("div", null, "\u52A0\u8F7D\u4E2D...");
1224
+ return /* @__PURE__ */ React4.createElement("div", { className: "grid items-start gap-4 lg:grid-cols-[1.45fr_1fr]" }, /* @__PURE__ */ React4.createElement(
1225
+ FestivalCardBook3D,
1226
+ {
1227
+ config,
1228
+ className: "h-full",
1229
+ editable: true,
1230
+ currentPage: activePageIndex,
1231
+ onCurrentPageChange: (index) => {
1232
+ setActivePageIndex(index);
1233
+ setSelectedElementId(null);
1234
+ },
1235
+ selectedElementId,
1236
+ onSelectedElementChange: setSelectedElementId,
1237
+ onElementChange: updateElementByPreview
1238
+ }
1239
+ ), /* @__PURE__ */ React4.createElement("div", { className: "lg:sticky lg:top-4" }, /* @__PURE__ */ React4.createElement(
1240
+ FestivalCardConfigEditor,
1241
+ {
1242
+ value: config,
1243
+ onChange: setConfig,
1244
+ activePageIndex,
1245
+ onActivePageIndexChange: (index) => {
1246
+ setActivePageIndex(index);
1247
+ setSelectedElementId(null);
1248
+ },
1249
+ selectedElementId
1250
+ }
1251
+ ), onSave ? /* @__PURE__ */ React4.createElement(
661
1252
  "button",
662
1253
  {
663
1254
  type: "button",
@@ -763,15 +1354,15 @@ var FestivalCardConfigPage = ({
763
1354
  }
764
1355
  };
765
1356
  const mainLink = useMemo(() => `${mainPagePath}?cardId=${encodeURIComponent(selectedId)}`, [mainPagePath, selectedId]);
766
- return /* @__PURE__ */ React3.createElement("div", { className: "grid gap-4" }, /* @__PURE__ */ React3.createElement("div", { className: "flex flex-wrap items-center gap-2 rounded-xl border border-slate-800 bg-slate-900/50 p-3" }, /* @__PURE__ */ React3.createElement(
1357
+ return /* @__PURE__ */ React4.createElement("div", { className: "grid gap-4" }, /* @__PURE__ */ React4.createElement("div", { className: "flex flex-wrap items-center gap-2 rounded-xl border border-slate-800 bg-slate-900/50 p-3" }, /* @__PURE__ */ React4.createElement(
767
1358
  "select",
768
1359
  {
769
1360
  value: selectedId,
770
1361
  onChange: (event) => setSelectedId(event.target.value),
771
1362
  className: "min-w-[240px] rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none focus:border-sky-400 focus:ring-2 focus:ring-sky-100"
772
1363
  },
773
- list.map((item) => /* @__PURE__ */ React3.createElement("option", { key: item.id, value: item.id }, item.name || item.id))
774
- ), /* @__PURE__ */ React3.createElement("button", { type: "button", onClick: () => void createNew(), className: "rounded-lg bg-slate-900 px-3 py-2 text-sm font-medium text-white" }, "\u65B0\u5EFA\u5361\u7247"), /* @__PURE__ */ React3.createElement("a", { href: mainLink, className: "rounded-lg border border-sky-200 bg-sky-50 px-3 py-2 text-sm text-sky-700" }, "\u6253\u5F00\u4E3B\u9875\u9762")), /* @__PURE__ */ React3.createElement(FestivalCardStudio, { fetchConfig, onSave: saveConfig }));
1364
+ list.map((item) => /* @__PURE__ */ React4.createElement("option", { key: item.id, value: item.id }, item.name || item.id))
1365
+ ), /* @__PURE__ */ React4.createElement("button", { type: "button", onClick: () => void createNew(), className: "rounded-lg bg-slate-900 px-3 py-2 text-sm font-medium text-white" }, "\u65B0\u5EFA\u5361\u7247"), /* @__PURE__ */ React4.createElement("a", { href: mainLink, className: "rounded-lg border border-sky-200 bg-sky-50 px-3 py-2 text-sm text-sky-700" }, "\u6253\u5F00\u4E3B\u9875\u9762")), /* @__PURE__ */ React4.createElement(FestivalCardStudio, { fetchConfig, onSave: saveConfig }));
775
1366
  };
776
1367
  var isSummary = (value) => {
777
1368
  if (!value || typeof value !== "object") return false;
@@ -814,7 +1405,7 @@ var FestivalCardManagedPage = ({
814
1405
  setLoading(true);
815
1406
  void fetch(`${apiBase}/${encodeURIComponent(currentCardId)}`, { cache: "no-store" }).then((res) => res.json()).then((data) => setConfig(parseConfigResponse(data))).finally(() => setLoading(false));
816
1407
  }, [apiBase, currentCardId]);
817
- return /* @__PURE__ */ React3.createElement("div", null, loading || !config ? /* @__PURE__ */ React3.createElement("div", { className: "py-12 text-center text-slate-400" }, "\u52A0\u8F7D\u4E2D...") : /* @__PURE__ */ React3.createElement(FestivalCardBook3D, { config }));
1408
+ return /* @__PURE__ */ React4.createElement("div", null, loading || !config ? /* @__PURE__ */ React4.createElement("div", { className: "py-12 text-center text-slate-400" }, "\u52A0\u8F7D\u4E2D...") : /* @__PURE__ */ React4.createElement(FestivalCardBook3D, { config }));
818
1409
  };
819
1410
 
820
1411
  // src/festivalCard/server/db.ts