sa2kit 1.6.90 → 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.
@@ -1,10 +1,12 @@
1
1
  'use strict';
2
2
 
3
- var React3 = require('react');
3
+ var React4 = require('react');
4
+ var reactDom = require('react-dom');
5
+ var clsx = require('clsx');
4
6
 
5
7
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
8
 
7
- var React3__default = /*#__PURE__*/_interopDefault(React3);
9
+ var React4__default = /*#__PURE__*/_interopDefault(React4);
8
10
 
9
11
  // src/festivalCard/core/defaults.ts
10
12
  var DEFAULT_FESTIVAL_CARD_CONFIG = {
@@ -182,12 +184,12 @@ var resizeFestivalCardPages = (config, pageCount) => {
182
184
  };
183
185
  };
184
186
  var useFestivalCardConfig = (options) => {
185
- const [config, setConfig] = React3.useState(
187
+ const [config, setConfig] = React4.useState(
186
188
  () => normalizeFestivalCardConfig(options?.initialConfig || DEFAULT_FESTIVAL_CARD_CONFIG)
187
189
  );
188
- const [loading, setLoading] = React3.useState(Boolean(options?.fetchConfig));
189
- const [saving, setSaving] = React3.useState(false);
190
- React3.useEffect(() => {
190
+ const [loading, setLoading] = React4.useState(Boolean(options?.fetchConfig));
191
+ const [saving, setSaving] = React4.useState(false);
192
+ React4.useEffect(() => {
191
193
  const fetchConfig = options?.fetchConfig;
192
194
  if (!fetchConfig) return;
193
195
  let active = true;
@@ -201,7 +203,7 @@ var useFestivalCardConfig = (options) => {
201
203
  active = false;
202
204
  };
203
205
  }, [options?.fetchConfig]);
204
- const save = React3.useCallback(async () => {
206
+ const save = React4.useCallback(async () => {
205
207
  const onSave = options?.onSave;
206
208
  if (!onSave) return;
207
209
  setSaving(true);
@@ -211,7 +213,7 @@ var useFestivalCardConfig = (options) => {
211
213
  setSaving(false);
212
214
  }
213
215
  }, [config, options?.onSave]);
214
- return React3.useMemo(
216
+ return React4.useMemo(
215
217
  () => ({
216
218
  config,
217
219
  setConfig,
@@ -222,6 +224,190 @@ var useFestivalCardConfig = (options) => {
222
224
  [config, loading, save, saving]
223
225
  );
224
226
  };
227
+ var FloatingMenu = ({
228
+ trigger,
229
+ menu,
230
+ initialPosition = { x: 20, y: 20 },
231
+ defaultOpen = false,
232
+ className = "",
233
+ menuClassName = "",
234
+ triggerClassName = "",
235
+ zIndex = 1e3
236
+ }) => {
237
+ const [position, setPosition] = React4.useState(initialPosition);
238
+ const [isMenuOpen, setIsMenuOpen] = React4.useState(defaultOpen);
239
+ const [menuDirection, setMenuDirection] = React4.useState("right");
240
+ const [isDragging, setIsDragging] = React4.useState(false);
241
+ const [dragOffset, setDragOffset] = React4.useState({ x: 0, y: 0 });
242
+ const containerRef = React4.useRef(null);
243
+ const [mounted, setMounted] = React4.useState(false);
244
+ const [hasDragged, setHasDragged] = React4.useState(false);
245
+ const dragTimerRef = React4.useRef(null);
246
+ const mouseDownPosRef = React4.useRef(null);
247
+ React4.useEffect(() => {
248
+ setMounted(true);
249
+ return () => setMounted(false);
250
+ }, []);
251
+ React4.useEffect(() => {
252
+ if (!mounted || !containerRef.current) return;
253
+ const updateMenuDirection = () => {
254
+ const rect = containerRef.current?.getBoundingClientRect();
255
+ if (!rect) return;
256
+ const windowWidth = window.innerWidth;
257
+ const middlePoint = windowWidth / 2;
258
+ setMenuDirection(rect.left < middlePoint ? "right" : "left");
259
+ };
260
+ updateMenuDirection();
261
+ window.addEventListener("resize", updateMenuDirection);
262
+ window.addEventListener("scroll", updateMenuDirection);
263
+ return () => {
264
+ window.removeEventListener("resize", updateMenuDirection);
265
+ window.removeEventListener("scroll", updateMenuDirection);
266
+ };
267
+ }, [mounted]);
268
+ const handleMouseDown = (e) => {
269
+ if (!containerRef.current) return;
270
+ e.stopPropagation();
271
+ mouseDownPosRef.current = { x: e.clientX, y: e.clientY };
272
+ const rect = containerRef.current.getBoundingClientRect();
273
+ setDragOffset({
274
+ x: e.clientX - rect.left,
275
+ y: e.clientY - rect.top
276
+ });
277
+ setHasDragged(false);
278
+ setIsDragging(true);
279
+ };
280
+ React4.useEffect(() => {
281
+ if (!isDragging) return;
282
+ const handleMouseMove = (e) => {
283
+ if (mouseDownPosRef.current) {
284
+ const dx = Math.abs(e.clientX - mouseDownPosRef.current.x);
285
+ const dy = Math.abs(e.clientY - mouseDownPosRef.current.y);
286
+ if (dx > 3 || dy > 3) {
287
+ setHasDragged(true);
288
+ }
289
+ }
290
+ const newX = e.clientX - dragOffset.x;
291
+ const newY = e.clientY - dragOffset.y;
292
+ const windowWidth = window.innerWidth;
293
+ const windowHeight = window.innerHeight;
294
+ setPosition({
295
+ x: Math.min(Math.max(newX, 0), windowWidth - 50),
296
+ y: Math.min(Math.max(newY, 0), windowHeight - 50)
297
+ });
298
+ };
299
+ const handleMouseUp = () => {
300
+ setIsDragging(false);
301
+ mouseDownPosRef.current = null;
302
+ if (dragTimerRef.current) {
303
+ window.clearTimeout(dragTimerRef.current);
304
+ }
305
+ dragTimerRef.current = window.setTimeout(() => {
306
+ setHasDragged(false);
307
+ }, 300);
308
+ };
309
+ document.addEventListener("mousemove", handleMouseMove);
310
+ document.addEventListener("mouseup", handleMouseUp);
311
+ return () => {
312
+ document.removeEventListener("mousemove", handleMouseMove);
313
+ document.removeEventListener("mouseup", handleMouseUp);
314
+ };
315
+ }, [isDragging, dragOffset]);
316
+ React4.useEffect(() => {
317
+ return () => {
318
+ if (dragTimerRef.current) {
319
+ window.clearTimeout(dragTimerRef.current);
320
+ }
321
+ };
322
+ }, []);
323
+ const toggleMenu = (e) => {
324
+ e.stopPropagation();
325
+ if (hasDragged) {
326
+ return;
327
+ }
328
+ setIsMenuOpen(!isMenuOpen);
329
+ };
330
+ React4.useEffect(() => {
331
+ if (!isMenuOpen) return;
332
+ const handleClickOutside = (e) => {
333
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
334
+ setIsMenuOpen(false);
335
+ }
336
+ };
337
+ document.addEventListener("mousedown", handleClickOutside);
338
+ return () => {
339
+ document.removeEventListener("mousedown", handleClickOutside);
340
+ };
341
+ }, [isMenuOpen]);
342
+ React4.useEffect(() => {
343
+ if (!mounted) return;
344
+ const checkBoundaries = () => {
345
+ const windowWidth = window.innerWidth;
346
+ const windowHeight = window.innerHeight;
347
+ setPosition((prev) => {
348
+ const newX = Math.min(Math.max(prev.x, 0), windowWidth - 50);
349
+ const newY = Math.min(Math.max(prev.y, 0), windowHeight - 50);
350
+ if (newX !== prev.x || newY !== prev.y) {
351
+ return { x: newX, y: newY };
352
+ }
353
+ return prev;
354
+ });
355
+ };
356
+ window.addEventListener("resize", checkBoundaries);
357
+ return () => {
358
+ window.removeEventListener("resize", checkBoundaries);
359
+ };
360
+ }, [mounted]);
361
+ if (!mounted) return null;
362
+ return reactDom.createPortal(
363
+ /* @__PURE__ */ React4__default.default.createElement(
364
+ "div",
365
+ {
366
+ ref: containerRef,
367
+ className: clsx.clsx("fixed select-none box-border", className),
368
+ style: {
369
+ left: position.x + "px",
370
+ top: position.y + "px",
371
+ zIndex
372
+ }
373
+ },
374
+ /* @__PURE__ */ React4__default.default.createElement(
375
+ "div",
376
+ {
377
+ className: clsx.clsx(
378
+ "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",
379
+ triggerClassName
380
+ ),
381
+ onMouseDown: handleMouseDown,
382
+ onClick: toggleMenu
383
+ },
384
+ trigger
385
+ ),
386
+ isMenuOpen && /* @__PURE__ */ React4__default.default.createElement(
387
+ "div",
388
+ {
389
+ className: clsx.clsx(
390
+ "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",
391
+ isMenuOpen ? "opacity-100 scale-100" : "opacity-0 scale-95",
392
+ menuDirection === "left" ? "right-[calc(100%+10px)]" : "left-[calc(100%+10px)]",
393
+ menuClassName
394
+ ),
395
+ onClick: (e) => e.stopPropagation(),
396
+ onMouseDown: (e) => e.stopPropagation(),
397
+ onMouseUp: (e) => e.stopPropagation(),
398
+ onTouchStart: (e) => e.stopPropagation(),
399
+ onTouchMove: (e) => e.stopPropagation(),
400
+ onTouchEnd: (e) => e.stopPropagation(),
401
+ onPointerDown: (e) => e.stopPropagation(),
402
+ onPointerUp: (e) => e.stopPropagation()
403
+ },
404
+ menu
405
+ )
406
+ ),
407
+ document.body
408
+ );
409
+ };
410
+ var FloatingMenu_default = FloatingMenu;
225
411
  var elementStyle = (element) => ({
226
412
  position: "absolute",
227
413
  zIndex: 2,
@@ -233,7 +419,7 @@ var elementStyle = (element) => ({
233
419
  });
234
420
  var renderElement = (element) => {
235
421
  if (element.type === "text") {
236
- return /* @__PURE__ */ React3__default.default.createElement(
422
+ return /* @__PURE__ */ React4__default.default.createElement(
237
423
  "div",
238
424
  {
239
425
  key: element.id,
@@ -251,7 +437,7 @@ var renderElement = (element) => {
251
437
  element.content
252
438
  );
253
439
  }
254
- return /* @__PURE__ */ React3__default.default.createElement(
440
+ return /* @__PURE__ */ React4__default.default.createElement(
255
441
  "img",
256
442
  {
257
443
  key: element.id,
@@ -267,15 +453,92 @@ var renderElement = (element) => {
267
453
  }
268
454
  );
269
455
  };
270
- var FestivalCardPageRenderer = ({ page }) => {
271
- const backgroundElement = page.elements.find(
272
- (element) => element.type === "image" && Boolean(element.isBackground)
456
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
457
+ var FestivalCardPageRenderer = ({
458
+ page,
459
+ editable = false,
460
+ selectedElementId = null,
461
+ onElementSelect,
462
+ onElementChange
463
+ }) => {
464
+ const [draggingElementId, setDraggingElementId] = React4.useState(null);
465
+ const [resizingElementId, setResizingElementId] = React4.useState(null);
466
+ const stageRef = React4.useRef(null);
467
+ const interactionRef = React4.useRef(null);
468
+ const backgroundElement = React4.useMemo(
469
+ () => page.elements.find(
470
+ (element) => element.type === "image" && Boolean(element.isBackground)
471
+ ),
472
+ [page]
273
473
  );
274
- const foregroundElements = page.elements.filter((element) => !(element.type === "image" && element.isBackground));
275
- return /* @__PURE__ */ React3__default.default.createElement(
474
+ const foregroundElements = React4.useMemo(
475
+ () => page.elements.filter((element) => !(element.type === "image" && element.isBackground)),
476
+ [page]
477
+ );
478
+ const updateElementByPointer = (element, interaction, clientX, clientY) => {
479
+ if (!onElementChange || interaction.rect.width <= 0 || interaction.rect.height <= 0) return;
480
+ const xPercent = clamp((clientX - interaction.rect.left) / interaction.rect.width * 100, 0, 100);
481
+ const yPercent = clamp((clientY - interaction.rect.top) / interaction.rect.height * 100, 0, 100);
482
+ if (interaction.mode === "move") {
483
+ onElementChange(element.id, { x: xPercent, y: yPercent });
484
+ return;
485
+ }
486
+ const nextWidth = clamp(Math.abs(xPercent - element.x) * 2, 4, 100);
487
+ if (element.type === "image") {
488
+ const nextHeight = clamp(Math.abs(yPercent - element.y) * 2, 4, 100);
489
+ onElementChange(element.id, { width: nextWidth, height: nextHeight });
490
+ return;
491
+ }
492
+ onElementChange(element.id, { width: nextWidth });
493
+ };
494
+ const beginInteraction = (event, elementId, mode) => {
495
+ if (!editable || !stageRef.current) return;
496
+ event.preventDefault();
497
+ event.stopPropagation();
498
+ const rect = stageRef.current.getBoundingClientRect();
499
+ interactionRef.current = {
500
+ pointerId: event.pointerId,
501
+ elementId,
502
+ mode,
503
+ rect
504
+ };
505
+ event.currentTarget.setPointerCapture(event.pointerId);
506
+ onElementSelect?.(elementId);
507
+ if (mode === "move") {
508
+ setDraggingElementId(elementId);
509
+ setResizingElementId(null);
510
+ } else {
511
+ setResizingElementId(elementId);
512
+ setDraggingElementId(null);
513
+ }
514
+ const element = foregroundElements.find((item) => item.id === elementId);
515
+ if (element) {
516
+ updateElementByPointer(element, interactionRef.current, event.clientX, event.clientY);
517
+ }
518
+ };
519
+ const handlePointerMove = (event) => {
520
+ const interaction = interactionRef.current;
521
+ if (!interaction || interaction.pointerId !== event.pointerId) return;
522
+ const element = foregroundElements.find((item) => item.id === interaction.elementId);
523
+ if (!element) return;
524
+ updateElementByPointer(element, interaction, event.clientX, event.clientY);
525
+ };
526
+ const endInteraction = (event) => {
527
+ const interaction = interactionRef.current;
528
+ if (!interaction || interaction.pointerId !== event.pointerId) return;
529
+ interactionRef.current = null;
530
+ setDraggingElementId(null);
531
+ setResizingElementId(null);
532
+ };
533
+ return /* @__PURE__ */ React4__default.default.createElement(
276
534
  "div",
277
535
  {
278
- className: "relative h-full w-full overflow-hidden rounded-2xl",
536
+ ref: stageRef,
537
+ onPointerMove: editable ? handlePointerMove : void 0,
538
+ onPointerUp: editable ? endInteraction : void 0,
539
+ onPointerCancel: editable ? endInteraction : void 0,
540
+ onClick: editable ? () => onElementSelect?.(null) : void 0,
541
+ className: `relative h-full w-full overflow-hidden rounded-2xl ${editable ? "touch-none" : ""}`,
279
542
  style: {
280
543
  backgroundColor: page.background?.color || "#0f172a",
281
544
  backgroundImage: backgroundElement ? `url(${backgroundElement.src})` : page.background?.image ? `url(${page.background.image})` : void 0,
@@ -283,19 +546,251 @@ var FestivalCardPageRenderer = ({ page }) => {
283
546
  backgroundPosition: "center"
284
547
  }
285
548
  },
286
- /* @__PURE__ */ React3__default.default.createElement("div", { className: "absolute inset-0 bg-slate-950/20" }),
287
- foregroundElements.map(renderElement)
549
+ /* @__PURE__ */ React4__default.default.createElement("div", { className: "absolute inset-0 bg-slate-950/20" }),
550
+ foregroundElements.map((element) => {
551
+ if (!editable) {
552
+ return renderElement(element);
553
+ }
554
+ const isSelected = selectedElementId === element.id;
555
+ const isDragging = draggingElementId === element.id;
556
+ const isResizing = resizingElementId === element.id;
557
+ return /* @__PURE__ */ React4__default.default.createElement(
558
+ "div",
559
+ {
560
+ key: element.id,
561
+ role: "button",
562
+ tabIndex: 0,
563
+ onClick: (event) => {
564
+ event.stopPropagation();
565
+ onElementSelect?.(element.id);
566
+ },
567
+ onPointerDown: (event) => beginInteraction(event, element.id, "move"),
568
+ 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"}`,
569
+ style: {
570
+ ...elementStyle(element),
571
+ zIndex: isSelected ? 4 : 2
572
+ }
573
+ },
574
+ element.type === "text" ? /* @__PURE__ */ React4__default.default.createElement(
575
+ "div",
576
+ {
577
+ className: "rounded-md bg-black/20 px-2 py-1",
578
+ style: {
579
+ color: element.color || "#f8fafc",
580
+ fontSize: element.fontSize || 18,
581
+ fontWeight: element.fontWeight || 500,
582
+ fontFamily: element.fontFamily || "inherit",
583
+ textAlign: element.align || "left",
584
+ lineHeight: 1.45,
585
+ whiteSpace: "pre-wrap"
586
+ }
587
+ },
588
+ element.content
589
+ ) : /* @__PURE__ */ React4__default.default.createElement(
590
+ "img",
591
+ {
592
+ src: element.src,
593
+ alt: element.alt || "festival-card-image",
594
+ draggable: false,
595
+ className: "pointer-events-none h-full w-full",
596
+ style: {
597
+ objectFit: element.fit || "cover",
598
+ borderRadius: element.borderRadius || 0,
599
+ overflow: "hidden",
600
+ boxShadow: "0 12px 30px rgba(2, 6, 23, 0.32)"
601
+ }
602
+ }
603
+ ),
604
+ /* @__PURE__ */ React4__default.default.createElement(
605
+ "button",
606
+ {
607
+ type: "button",
608
+ "aria-label": "resize",
609
+ onPointerDown: (event) => beginInteraction(event, element.id, "resize"),
610
+ className: "absolute -bottom-2 -right-2 h-4 w-4 rounded-full border border-white bg-sky-500 shadow"
611
+ }
612
+ )
613
+ );
614
+ })
288
615
  );
289
616
  };
290
617
 
291
618
  // src/festivalCard/components/FestivalCardBook3D.tsx
292
- var FestivalCardBook3D = ({ config, className }) => {
293
- const [currentPage, setCurrentPage] = React3.useState(0);
294
- const normalized = React3.useMemo(() => normalizeFestivalCardConfig(config), [config]);
619
+ var loadImage = (src) => new Promise((resolve, reject) => {
620
+ const image = new window.Image();
621
+ image.crossOrigin = "anonymous";
622
+ image.decoding = "async";
623
+ image.onload = () => resolve(image);
624
+ image.onerror = () => reject(new Error(`\u56FE\u7247\u52A0\u8F7D\u5931\u8D25: ${src}`));
625
+ image.src = src;
626
+ });
627
+ var drawImageWithFit = (ctx, image, left, top, width, height, fit) => {
628
+ const imageRatio = image.width / image.height;
629
+ const boxRatio = width / height;
630
+ let drawWidth = width;
631
+ let drawHeight = height;
632
+ let offsetX = left;
633
+ let offsetY = top;
634
+ if (fit === "cover") {
635
+ if (imageRatio > boxRatio) {
636
+ drawHeight = height;
637
+ drawWidth = height * imageRatio;
638
+ offsetX = left - (drawWidth - width) / 2;
639
+ } else {
640
+ drawWidth = width;
641
+ drawHeight = width / imageRatio;
642
+ offsetY = top - (drawHeight - height) / 2;
643
+ }
644
+ } else if (imageRatio > boxRatio) {
645
+ drawWidth = width;
646
+ drawHeight = width / imageRatio;
647
+ offsetY = top + (height - drawHeight) / 2;
648
+ } else {
649
+ drawHeight = height;
650
+ drawWidth = height * imageRatio;
651
+ offsetX = left + (width - drawWidth) / 2;
652
+ }
653
+ ctx.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
654
+ };
655
+ var withRoundedClip = (ctx, left, top, width, height, radius, draw) => {
656
+ const safeRadius = Math.max(0, Math.min(radius, Math.min(width, height) / 2));
657
+ if (safeRadius <= 0) {
658
+ draw();
659
+ return;
660
+ }
661
+ ctx.save();
662
+ ctx.beginPath();
663
+ ctx.moveTo(left + safeRadius, top);
664
+ ctx.lineTo(left + width - safeRadius, top);
665
+ ctx.quadraticCurveTo(left + width, top, left + width, top + safeRadius);
666
+ ctx.lineTo(left + width, top + height - safeRadius);
667
+ ctx.quadraticCurveTo(left + width, top + height, left + width - safeRadius, top + height);
668
+ ctx.lineTo(left + safeRadius, top + height);
669
+ ctx.quadraticCurveTo(left, top + height, left, top + height - safeRadius);
670
+ ctx.lineTo(left, top + safeRadius);
671
+ ctx.quadraticCurveTo(left, top, left + safeRadius, top);
672
+ ctx.closePath();
673
+ ctx.clip();
674
+ draw();
675
+ ctx.restore();
676
+ };
677
+ var drawMultilineText = (ctx, text, left, top, maxWidth, lineHeight) => {
678
+ const paragraphs = text.split("\n");
679
+ let currentY = top;
680
+ paragraphs.forEach((paragraph, index) => {
681
+ const words = paragraph.split("");
682
+ let line = "";
683
+ for (const word of words) {
684
+ const testLine = line + word;
685
+ if (ctx.measureText(testLine).width > maxWidth && line) {
686
+ ctx.fillText(line, left, currentY);
687
+ line = word;
688
+ currentY += lineHeight;
689
+ } else {
690
+ line = testLine;
691
+ }
692
+ }
693
+ ctx.fillText(line, left, currentY);
694
+ currentY += lineHeight;
695
+ if (index < paragraphs.length - 1) {
696
+ currentY += lineHeight * 0.2;
697
+ }
698
+ });
699
+ };
700
+ var exportPageToPng = async (page, fileName) => {
701
+ const width = 1080;
702
+ const height = 1440;
703
+ const canvas = document.createElement("canvas");
704
+ canvas.width = width;
705
+ canvas.height = height;
706
+ const ctx = canvas.getContext("2d");
707
+ if (!ctx) throw new Error("\u65E0\u6CD5\u521B\u5EFA Canvas \u4E0A\u4E0B\u6587");
708
+ ctx.fillStyle = page.background?.color || "#0f172a";
709
+ ctx.fillRect(0, 0, width, height);
710
+ const backgroundElement = page.elements.find(
711
+ (element) => element.type === "image" && Boolean(element.isBackground)
712
+ );
713
+ const backgroundImageSrc = backgroundElement?.src || page.background?.image;
714
+ if (backgroundImageSrc) {
715
+ const image = await loadImage(backgroundImageSrc);
716
+ drawImageWithFit(ctx, image, 0, 0, width, height, "cover");
717
+ }
718
+ const foregroundElements = page.elements.filter((element) => !(element.type === "image" && element.isBackground));
719
+ for (const element of foregroundElements) {
720
+ const elementWidth = width * (element.width ?? 70) / 100;
721
+ const elementHeight = element.height ? height * element.height / 100 : void 0;
722
+ const centerX = width * element.x / 100;
723
+ const centerY = height * element.y / 100;
724
+ const left = centerX - elementWidth / 2;
725
+ if (element.type === "image") {
726
+ const image = await loadImage(element.src);
727
+ const drawHeight = elementHeight ?? elementWidth;
728
+ const boxTop = centerY - drawHeight / 2;
729
+ withRoundedClip(ctx, left, boxTop, elementWidth, drawHeight, element.borderRadius ?? 0, () => {
730
+ drawImageWithFit(ctx, image, left, boxTop, elementWidth, drawHeight, element.fit || "cover");
731
+ });
732
+ continue;
733
+ }
734
+ const fontSize = (element.fontSize || 18) * 1.5;
735
+ ctx.fillStyle = element.color || "#f8fafc";
736
+ ctx.font = `${element.fontWeight || 500} ${fontSize}px ${element.fontFamily || "sans-serif"}`;
737
+ ctx.textBaseline = "top";
738
+ ctx.textAlign = element.align || "left";
739
+ const textX = element.align === "center" ? centerX : element.align === "right" ? left + elementWidth : left;
740
+ drawMultilineText(ctx, element.content || "", textX, centerY - fontSize * 0.72, elementWidth, fontSize * 1.45);
741
+ }
742
+ const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
743
+ if (!blob) throw new Error("\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
744
+ const url = URL.createObjectURL(blob);
745
+ const anchor = document.createElement("a");
746
+ anchor.href = url;
747
+ anchor.download = fileName;
748
+ anchor.click();
749
+ URL.revokeObjectURL(url);
750
+ };
751
+ var FestivalCardBook3D = ({
752
+ config,
753
+ className,
754
+ editable = false,
755
+ enableExportImage = !editable,
756
+ currentPage: currentPageProp,
757
+ onCurrentPageChange,
758
+ selectedElementId = null,
759
+ onSelectedElementChange,
760
+ onElementChange
761
+ }) => {
762
+ const [internalCurrentPage, setInternalCurrentPage] = React4.useState(0);
763
+ const [exporting, setExporting] = React4.useState(false);
764
+ const normalized = React4.useMemo(() => normalizeFestivalCardConfig(config), [config]);
295
765
  const pages = normalized.pages;
766
+ const currentPage = typeof currentPageProp === "number" ? currentPageProp : internalCurrentPage;
767
+ const setCurrentPage = (updater) => {
768
+ const prev = currentPage;
769
+ const nextValue = typeof updater === "function" ? updater(prev) : updater;
770
+ if (typeof currentPageProp === "number") {
771
+ onCurrentPageChange?.(nextValue);
772
+ return;
773
+ }
774
+ setInternalCurrentPage(nextValue);
775
+ onCurrentPageChange?.(nextValue);
776
+ };
296
777
  const canPrev = currentPage > 0;
297
778
  const canNext = currentPage < pages.length - 1;
298
- return /* @__PURE__ */ React3__default.default.createElement("div", { className }, /* @__PURE__ */ React3__default.default.createElement("div", { className: "w-full min-h-screen px-0 py-4" }, /* @__PURE__ */ React3__default.default.createElement("div", { className: "mx-auto w-full text-center text-slate-100" }, /* @__PURE__ */ React3__default.default.createElement("h3", { className: "mb-3 text-lg font-semibold" }, normalized.coverTitle || "Festival Card")), /* @__PURE__ */ React3__default.default.createElement("div", { className: "mx-auto w-full" }, /* @__PURE__ */ React3__default.default.createElement("div", { className: "relative h-[calc(100vh-170px)] min-h-[460px]" }, pages.map((page, index) => /* @__PURE__ */ React3__default.default.createElement(
779
+ const currentPageData = pages[currentPage];
780
+ const handleExportCurrentPage = async () => {
781
+ if (!currentPageData || exporting) return;
782
+ setExporting(true);
783
+ try {
784
+ const base = normalized.id || "festival-card";
785
+ const fileName = `${base}-page-${currentPage + 1}.png`;
786
+ await exportPageToPng(currentPageData, fileName);
787
+ } catch (error) {
788
+ window.alert(error.message || "\u5BFC\u51FA\u56FE\u7247\u5931\u8D25");
789
+ } finally {
790
+ setExporting(false);
791
+ }
792
+ };
793
+ return /* @__PURE__ */ React4__default.default.createElement("div", { className }, /* @__PURE__ */ React4__default.default.createElement("div", { className: "w-full min-h-screen px-0 py-4" }, /* @__PURE__ */ React4__default.default.createElement("div", { className: "mx-auto w-full text-center text-slate-100" }, /* @__PURE__ */ React4__default.default.createElement("h3", { className: "mb-3 text-lg font-semibold" }, normalized.coverTitle || "Festival Card")), /* @__PURE__ */ React4__default.default.createElement("div", { className: "mx-auto w-full" }, /* @__PURE__ */ React4__default.default.createElement("div", { className: "relative h-[calc(100vh-170px)] min-h-[460px]" }, pages.map((page, index) => /* @__PURE__ */ React4__default.default.createElement(
299
794
  "div",
300
795
  {
301
796
  key: page.id,
@@ -305,8 +800,17 @@ var FestivalCardBook3D = ({ config, className }) => {
305
800
  pointerEvents: index === currentPage ? "auto" : "none"
306
801
  }
307
802
  },
308
- /* @__PURE__ */ React3__default.default.createElement(FestivalCardPageRenderer, { page })
309
- )))), /* @__PURE__ */ React3__default.default.createElement("div", { className: "mt-4 flex justify-center gap-3" }, /* @__PURE__ */ React3__default.default.createElement(
803
+ /* @__PURE__ */ React4__default.default.createElement(
804
+ FestivalCardPageRenderer,
805
+ {
806
+ page,
807
+ editable: editable && index === currentPage,
808
+ selectedElementId,
809
+ onElementSelect: onSelectedElementChange,
810
+ onElementChange: (elementId, patch) => onElementChange?.(index, elementId, patch)
811
+ }
812
+ )
813
+ )))), /* @__PURE__ */ React4__default.default.createElement("div", { className: "mt-4 flex justify-center gap-3" }, /* @__PURE__ */ React4__default.default.createElement(
310
814
  "button",
311
815
  {
312
816
  type: "button",
@@ -315,7 +819,7 @@ var FestivalCardBook3D = ({ config, className }) => {
315
819
  className: "rounded-full bg-white px-5 py-2 text-sm font-medium text-slate-900 disabled:cursor-not-allowed disabled:opacity-45"
316
820
  },
317
821
  "\u4E0A\u4E00\u9875"
318
- ), /* @__PURE__ */ React3__default.default.createElement(
822
+ ), /* @__PURE__ */ React4__default.default.createElement(
319
823
  "button",
320
824
  {
321
825
  type: "button",
@@ -324,7 +828,7 @@ var FestivalCardBook3D = ({ config, className }) => {
324
828
  className: "rounded-full bg-sky-300 px-5 py-2 text-sm font-medium text-slate-900 disabled:cursor-not-allowed disabled:opacity-45"
325
829
  },
326
830
  "\u4E0B\u4E00\u9875"
327
- ))), normalized.backgroundMusic?.src ? /* @__PURE__ */ React3__default.default.createElement(
831
+ ))), normalized.backgroundMusic?.src ? /* @__PURE__ */ React4__default.default.createElement(
328
832
  "audio",
329
833
  {
330
834
  src: normalized.backgroundMusic.src,
@@ -333,6 +837,24 @@ var FestivalCardBook3D = ({ config, className }) => {
333
837
  controls: true,
334
838
  className: "mt-3 w-full"
335
839
  }
840
+ ) : null, enableExportImage ? /* @__PURE__ */ React4__default.default.createElement(
841
+ FloatingMenu_default,
842
+ {
843
+ initialPosition: { x: 24, y: 120 },
844
+ trigger: /* @__PURE__ */ React4__default.default.createElement("div", { className: "text-lg leading-none text-slate-700", "aria-hidden": true }, "\u2301"),
845
+ menu: /* @__PURE__ */ React4__default.default.createElement("div", { className: "grid gap-2" }, /* @__PURE__ */ React4__default.default.createElement("div", { className: "text-xs font-semibold tracking-wide text-slate-500" }, "\u8D3A\u5361\u5DE5\u5177"), /* @__PURE__ */ React4__default.default.createElement(
846
+ "button",
847
+ {
848
+ type: "button",
849
+ onClick: () => void handleExportCurrentPage(),
850
+ disabled: exporting,
851
+ className: "rounded-lg bg-sky-600 px-3 py-2 text-left text-sm font-medium text-white disabled:opacity-60"
852
+ },
853
+ exporting ? "\u5BFC\u51FA\u4E2D..." : `\u5BFC\u51FA\u7B2C ${currentPage + 1} \u9875 PNG`
854
+ )),
855
+ triggerClassName: "bg-white/95 backdrop-blur",
856
+ menuClassName: "bg-white/95 backdrop-blur"
857
+ }
336
858
  ) : null);
337
859
  };
338
860
  var createTextElement = (pageIndex) => ({
@@ -359,26 +881,25 @@ var createImageElement = (pageIndex) => ({
359
881
  fit: "cover",
360
882
  borderRadius: 12
361
883
  });
362
- var FestivalCardConfigEditor = ({ value, onChange }) => {
363
- const [activePageIndex, setActivePageIndex] = React3.useState(0);
364
- const [activeElementId, setActiveElementId] = React3.useState(null);
365
- const [draggingElementId, setDraggingElementId] = React3.useState(null);
366
- const previewRef = React3.useRef(null);
367
- const dragStateRef = React3.useRef(null);
884
+ var FestivalCardConfigEditor = ({
885
+ value,
886
+ onChange,
887
+ activePageIndex: activePageIndexProp,
888
+ onActivePageIndexChange,
889
+ selectedElementId
890
+ }) => {
891
+ const [internalActivePageIndex, setInternalActivePageIndex] = React4.useState(0);
892
+ const activePageIndex = activePageIndexProp ?? internalActivePageIndex;
893
+ const setActivePageIndex = (index) => {
894
+ if (typeof activePageIndexProp === "number") {
895
+ onActivePageIndexChange?.(index);
896
+ return;
897
+ }
898
+ setInternalActivePageIndex(index);
899
+ };
368
900
  const page = value.pages[activePageIndex];
369
901
  const canEditPage = Boolean(page);
370
- const pageOptions = React3.useMemo(() => value.pages.map((_, index) => index), [value.pages]);
371
- const backgroundElement = React3.useMemo(
372
- () => page?.elements.find(
373
- (element) => element.type === "image" && Boolean(element.isBackground)
374
- ),
375
- [page]
376
- );
377
- const foregroundElements = React3.useMemo(
378
- () => (page?.elements ?? []).filter((element) => !(element.type === "image" && element.isBackground)),
379
- [page]
380
- );
381
- const clampPercent = (valueToClamp) => Math.max(0, Math.min(100, Number.isFinite(valueToClamp) ? valueToClamp : 0));
902
+ const pageOptions = React4.useMemo(() => value.pages.map((_, index) => index), [value.pages]);
382
903
  const handlePageCountChange = (nextRaw) => {
383
904
  const next = Number.isFinite(nextRaw) ? Math.max(1, Math.min(12, Math.floor(nextRaw))) : value.pages.length;
384
905
  const resized = resizeFestivalCardPages(value, next);
@@ -415,40 +936,8 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
415
936
  pages: value.pages.map((p, index) => index === activePageIndex ? { ...p, ...patch } : p)
416
937
  });
417
938
  };
418
- const moveElementWithPointer = (elementId, clientX, clientY, rect) => {
419
- const bounds = rect || previewRef.current?.getBoundingClientRect();
420
- if (!bounds || bounds.width <= 0 || bounds.height <= 0) return;
421
- const x = clampPercent((clientX - bounds.left) / bounds.width * 100);
422
- const y = clampPercent((clientY - bounds.top) / bounds.height * 100);
423
- updateElement(elementId, { x, y });
424
- };
425
- const handleElementPointerDown = (event, elementId) => {
426
- if (!previewRef.current) return;
427
- event.preventDefault();
428
- const rect = previewRef.current.getBoundingClientRect();
429
- dragStateRef.current = {
430
- pointerId: event.pointerId,
431
- elementId,
432
- rect
433
- };
434
- event.currentTarget.setPointerCapture(event.pointerId);
435
- setActiveElementId(elementId);
436
- setDraggingElementId(elementId);
437
- moveElementWithPointer(elementId, event.clientX, event.clientY, rect);
438
- };
439
- const handlePreviewPointerMove = (event) => {
440
- const dragState = dragStateRef.current;
441
- if (!dragState || dragState.pointerId !== event.pointerId) return;
442
- moveElementWithPointer(dragState.elementId, event.clientX, event.clientY, dragState.rect);
443
- };
444
- const endPointerDrag = (event) => {
445
- const dragState = dragStateRef.current;
446
- if (!dragState || dragState.pointerId !== event.pointerId) return;
447
- dragStateRef.current = null;
448
- setDraggingElementId(null);
449
- };
450
939
  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";
451
- return /* @__PURE__ */ React3__default.default.createElement("div", { className: "rounded-2xl border border-slate-200 bg-white p-4 text-slate-900 shadow-sm" }, /* @__PURE__ */ React3__default.default.createElement("div", { className: "grid gap-3" }, /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React3__default.default.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u6570\u91CF"), /* @__PURE__ */ React3__default.default.createElement(
940
+ return /* @__PURE__ */ React4__default.default.createElement("div", { className: "rounded-2xl border border-slate-200 bg-white p-4 text-slate-900 shadow-sm" }, /* @__PURE__ */ React4__default.default.createElement("div", { className: "grid gap-3" }, /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React4__default.default.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u6570\u91CF"), /* @__PURE__ */ React4__default.default.createElement(
452
941
  "input",
453
942
  {
454
943
  type: "number",
@@ -458,7 +947,7 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
458
947
  onChange: (event) => handlePageCountChange(Number(event.target.value)),
459
948
  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"
460
949
  }
461
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React3__default.default.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u80CC\u666F\u97F3\u4E50 URL"), /* @__PURE__ */ React3__default.default.createElement(
950
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React4__default.default.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u80CC\u666F\u97F3\u4E50 URL"), /* @__PURE__ */ React4__default.default.createElement(
462
951
  "input",
463
952
  {
464
953
  type: "url",
@@ -472,7 +961,7 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
472
961
  }),
473
962
  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"
474
963
  }
475
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React3__default.default.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u6807\u9898"), /* @__PURE__ */ React3__default.default.createElement(
964
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React4__default.default.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u6807\u9898"), /* @__PURE__ */ React4__default.default.createElement(
476
965
  "input",
477
966
  {
478
967
  type: "text",
@@ -480,7 +969,7 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
480
969
  onChange: (event) => updatePage({ title: event.target.value }),
481
970
  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"
482
971
  }
483
- )), /* @__PURE__ */ React3__default.default.createElement("div", { className: "grid gap-2 sm:grid-cols-2" }, /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React3__default.default.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u80CC\u666F\u8272"), /* @__PURE__ */ React3__default.default.createElement(
972
+ )), /* @__PURE__ */ React4__default.default.createElement("div", { className: "grid gap-2 sm:grid-cols-2" }, /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React4__default.default.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u80CC\u666F\u8272"), /* @__PURE__ */ React4__default.default.createElement(
484
973
  "input",
485
974
  {
486
975
  type: "color",
@@ -493,7 +982,7 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
493
982
  }),
494
983
  className: "h-10 w-full rounded-lg border border-slate-300 bg-white p-1"
495
984
  }
496
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React3__default.default.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u80CC\u666F\u56FE URL"), /* @__PURE__ */ React3__default.default.createElement(
985
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React4__default.default.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u9875\u9762\u80CC\u666F\u56FE URL"), /* @__PURE__ */ React4__default.default.createElement(
497
986
  "input",
498
987
  {
499
988
  type: "url",
@@ -506,15 +995,15 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
506
995
  }),
507
996
  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"
508
997
  }
509
- ))), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React3__default.default.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u7F16\u8F91\u9875\u9762"), /* @__PURE__ */ React3__default.default.createElement(
998
+ ))), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1.5" }, /* @__PURE__ */ React4__default.default.createElement("span", { className: "text-sm font-medium text-slate-700" }, "\u7F16\u8F91\u9875\u9762"), /* @__PURE__ */ React4__default.default.createElement(
510
999
  "select",
511
1000
  {
512
1001
  value: activePageIndex,
513
1002
  onChange: (event) => setActivePageIndex(Number(event.target.value)),
514
1003
  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"
515
1004
  },
516
- pageOptions.map((index) => /* @__PURE__ */ React3__default.default.createElement("option", { key: index, value: index }, "\u7B2C ", index + 1, " \u9875"))
517
- ))), canEditPage ? /* @__PURE__ */ React3__default.default.createElement("div", { className: "mt-4" }, /* @__PURE__ */ React3__default.default.createElement("div", { className: "mb-3 flex gap-2" }, /* @__PURE__ */ React3__default.default.createElement(
1005
+ pageOptions.map((index) => /* @__PURE__ */ React4__default.default.createElement("option", { key: index, value: index }, "\u7B2C ", index + 1, " \u9875"))
1006
+ ))), canEditPage ? /* @__PURE__ */ React4__default.default.createElement("div", { className: "mt-4" }, /* @__PURE__ */ React4__default.default.createElement("div", { className: "mb-3 flex gap-2" }, /* @__PURE__ */ React4__default.default.createElement(
518
1007
  "button",
519
1008
  {
520
1009
  type: "button",
@@ -527,7 +1016,7 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
527
1016
  className: "rounded-lg bg-slate-900 px-3 py-2 text-sm font-medium text-white"
528
1017
  },
529
1018
  "+ \u6587\u5B57"
530
- ), /* @__PURE__ */ React3__default.default.createElement(
1019
+ ), /* @__PURE__ */ React4__default.default.createElement(
531
1020
  "button",
532
1021
  {
533
1022
  type: "button",
@@ -540,233 +1029,174 @@ var FestivalCardConfigEditor = ({ value, onChange }) => {
540
1029
  className: "rounded-lg bg-sky-600 px-3 py-2 text-sm font-medium text-white"
541
1030
  },
542
1031
  "+ \u56FE\u7247"
543
- )), /* @__PURE__ */ React3__default.default.createElement("div", { className: "mb-3" }, /* @__PURE__ */ React3__default.default.createElement("div", { className: "mb-2 flex items-center justify-between" }, /* @__PURE__ */ React3__default.default.createElement("div", { className: "text-xs font-semibold tracking-wide text-slate-500" }, "\u62D6\u62FD\u5B9A\u4F4D\uFF08\u76F4\u63A5\u62D6\u52A8\u5143\u7D20\u8C03\u6574 X / Y\uFF09"), /* @__PURE__ */ React3__default.default.createElement("div", { className: "text-xs text-slate-500" }, draggingElementId ? "\u62D6\u62FD\u4E2D..." : "\u53EF\u62D6\u62FD")), /* @__PURE__ */ React3__default.default.createElement(
1032
+ )), /* @__PURE__ */ React4__default.default.createElement("div", { className: "grid max-h-[340px] gap-2.5 overflow-auto pr-1" }, (page?.elements ?? []).map((element) => /* @__PURE__ */ React4__default.default.createElement(
544
1033
  "div",
545
1034
  {
546
- ref: previewRef,
547
- onPointerMove: handlePreviewPointerMove,
548
- onPointerUp: endPointerDrag,
549
- onPointerCancel: endPointerDrag,
550
- className: "relative aspect-[3/4] w-full touch-none overflow-hidden rounded-xl border border-slate-300 bg-slate-900",
551
- style: {
552
- backgroundColor: page?.background?.color || "#0f172a",
553
- backgroundImage: backgroundElement ? `url(${backgroundElement.src})` : page?.background?.image ? `url(${page.background.image})` : void 0,
554
- backgroundSize: "cover",
555
- backgroundPosition: "center"
556
- }
557
- },
558
- /* @__PURE__ */ React3__default.default.createElement("div", { className: "absolute inset-0 bg-slate-950/20" }),
559
- foregroundElements.map((element) => {
560
- const isActive = activeElementId === element.id;
561
- const isDragging = draggingElementId === element.id;
562
- return /* @__PURE__ */ React3__default.default.createElement(
563
- "div",
564
- {
565
- key: element.id,
566
- role: "button",
567
- tabIndex: 0,
568
- onPointerDown: (event) => handleElementPointerDown(event, element.id),
569
- onClick: () => setActiveElementId(element.id),
570
- className: `absolute select-none rounded-md ${isDragging ? "cursor-grabbing" : "cursor-grab"} ${isActive ? "ring-2 ring-sky-300" : "ring-1 ring-white/40"}`,
571
- style: {
572
- left: `${element.x}%`,
573
- top: `${element.y}%`,
574
- width: `${element.width ?? 70}%`,
575
- height: element.height ? `${element.height}%` : void 0,
576
- transform: "translate(-50%, -50%)",
577
- zIndex: isActive ? 4 : 2
578
- }
579
- },
580
- element.type === "text" ? /* @__PURE__ */ React3__default.default.createElement(
581
- "div",
582
- {
583
- className: "w-full rounded-md bg-black/20 px-2 py-1",
584
- style: {
585
- color: element.color || "#f8fafc",
586
- fontSize: element.fontSize || 18,
587
- fontWeight: element.fontWeight || 500,
588
- fontFamily: element.fontFamily || "inherit",
589
- textAlign: element.align || "left",
590
- lineHeight: 1.35,
591
- whiteSpace: "pre-wrap"
592
- }
593
- },
594
- element.content || "\u6587\u672C"
595
- ) : /* @__PURE__ */ React3__default.default.createElement(
596
- "img",
597
- {
598
- src: element.src,
599
- alt: element.alt || "festival-card-image",
600
- draggable: false,
601
- className: "h-full w-full pointer-events-none",
602
- style: {
603
- objectFit: element.fit || "cover",
604
- borderRadius: element.borderRadius || 0
605
- }
606
- }
607
- )
608
- );
609
- })
610
- )), /* @__PURE__ */ React3__default.default.createElement("div", { className: "grid max-h-[340px] gap-2.5 overflow-auto pr-1" }, (page?.elements ?? []).map((element) => /* @__PURE__ */ React3__default.default.createElement("div", { key: element.id, className: "rounded-xl border border-slate-200 bg-slate-50 p-3" }, /* @__PURE__ */ React3__default.default.createElement("div", { className: "mb-2 flex items-center justify-between" }, /* @__PURE__ */ React3__default.default.createElement("div", { className: "text-xs font-semibold tracking-wide text-slate-500" }, element.type.toUpperCase()), /* @__PURE__ */ React3__default.default.createElement(
611
- "button",
612
- {
613
- type: "button",
614
- onClick: () => removeElement(element.id),
615
- className: "rounded-md border border-rose-300 bg-rose-50 px-2 py-1 text-xs font-medium text-rose-700"
616
- },
617
- "\u5220\u9664"
618
- )), element.type === "text" ? /* @__PURE__ */ React3__default.default.createElement("div", { className: "grid gap-2" }, /* @__PURE__ */ React3__default.default.createElement(
619
- "textarea",
620
- {
621
- value: element.content,
622
- onChange: (event) => updateElement(element.id, { content: event.target.value }),
623
- rows: 3,
624
- 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"
625
- }
626
- ), /* @__PURE__ */ React3__default.default.createElement("div", { className: "grid gap-2 sm:grid-cols-2" }, /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "X(%)", /* @__PURE__ */ React3__default.default.createElement(
627
- "input",
628
- {
629
- type: "number",
630
- value: element.x,
631
- onChange: (event) => updateElement(element.id, { x: Number(event.target.value) }),
632
- className: numberFieldClassName
633
- }
634
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "Y(%)", /* @__PURE__ */ React3__default.default.createElement(
635
- "input",
636
- {
637
- type: "number",
638
- value: element.y,
639
- onChange: (event) => updateElement(element.id, { y: Number(event.target.value) }),
640
- className: numberFieldClassName
641
- }
642
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5BBD\u5EA6(%)", /* @__PURE__ */ React3__default.default.createElement(
643
- "input",
644
- {
645
- type: "number",
646
- value: element.width ?? 70,
647
- onChange: (event) => updateElement(element.id, { width: Number(event.target.value) }),
648
- className: numberFieldClassName
649
- }
650
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5B57\u53F7(px)", /* @__PURE__ */ React3__default.default.createElement(
651
- "input",
652
- {
653
- type: "number",
654
- value: element.fontSize ?? 18,
655
- onChange: (event) => updateElement(element.id, { fontSize: Number(event.target.value) }),
656
- className: numberFieldClassName
657
- }
658
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5B57\u91CD", /* @__PURE__ */ React3__default.default.createElement(
659
- "input",
660
- {
661
- type: "number",
662
- min: 100,
663
- max: 900,
664
- step: 100,
665
- value: element.fontWeight ?? 500,
666
- onChange: (event) => updateElement(element.id, { fontWeight: Number(event.target.value) }),
667
- className: numberFieldClassName
668
- }
669
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5BF9\u9F50", /* @__PURE__ */ React3__default.default.createElement(
670
- "select",
671
- {
672
- value: element.align || "left",
673
- onChange: (event) => updateElement(element.id, { align: event.target.value }),
674
- className: numberFieldClassName
675
- },
676
- /* @__PURE__ */ React3__default.default.createElement("option", { value: "left" }, "left"),
677
- /* @__PURE__ */ React3__default.default.createElement("option", { value: "center" }, "center"),
678
- /* @__PURE__ */ React3__default.default.createElement("option", { value: "right" }, "right")
679
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600 sm:col-span-2" }, "\u5B57\u4F53", /* @__PURE__ */ React3__default.default.createElement(
680
- "input",
681
- {
682
- type: "text",
683
- value: element.fontFamily || "",
684
- onChange: (event) => updateElement(element.id, { fontFamily: event.target.value }),
685
- placeholder: "inherit / serif / sans-serif / PingFang SC",
686
- className: numberFieldClassName
687
- }
688
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600 sm:col-span-2" }, "\u6587\u5B57\u989C\u8272", /* @__PURE__ */ React3__default.default.createElement("div", { className: "grid grid-cols-[64px_1fr] gap-2" }, /* @__PURE__ */ React3__default.default.createElement(
689
- "input",
690
- {
691
- type: "color",
692
- value: element.color || "#ffffff",
693
- onChange: (event) => updateElement(element.id, { color: event.target.value }),
694
- className: "h-10 rounded-lg border border-slate-300 bg-white p-1"
695
- }
696
- ), /* @__PURE__ */ React3__default.default.createElement(
697
- "input",
698
- {
699
- type: "text",
700
- value: element.color || "#ffffff",
701
- onChange: (event) => updateElement(element.id, { color: event.target.value }),
702
- className: numberFieldClassName
703
- }
704
- ))))) : /* @__PURE__ */ React3__default.default.createElement("div", { className: "grid gap-2" }, /* @__PURE__ */ React3__default.default.createElement(
705
- "input",
706
- {
707
- type: "url",
708
- value: element.src,
709
- onChange: (event) => updateElement(element.id, { src: event.target.value }),
710
- 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"
711
- }
712
- ), /* @__PURE__ */ React3__default.default.createElement("div", { className: "grid gap-2 sm:grid-cols-2" }, /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "X(%)", /* @__PURE__ */ React3__default.default.createElement(
713
- "input",
714
- {
715
- type: "number",
716
- value: element.x,
717
- onChange: (event) => updateElement(element.id, { x: Number(event.target.value) }),
718
- className: numberFieldClassName
719
- }
720
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "Y(%)", /* @__PURE__ */ React3__default.default.createElement(
721
- "input",
722
- {
723
- type: "number",
724
- value: element.y,
725
- onChange: (event) => updateElement(element.id, { y: Number(event.target.value) }),
726
- className: numberFieldClassName
727
- }
728
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5BBD\u5EA6(%)", /* @__PURE__ */ React3__default.default.createElement(
729
- "input",
730
- {
731
- type: "number",
732
- value: element.width ?? 60,
733
- onChange: (event) => updateElement(element.id, { width: Number(event.target.value) }),
734
- className: numberFieldClassName
735
- }
736
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u9AD8\u5EA6(%)", /* @__PURE__ */ React3__default.default.createElement(
737
- "input",
738
- {
739
- type: "number",
740
- value: element.height ?? 40,
741
- onChange: (event) => updateElement(element.id, { height: Number(event.target.value) }),
742
- className: numberFieldClassName
743
- }
744
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5706\u89D2(px)", /* @__PURE__ */ React3__default.default.createElement(
745
- "input",
746
- {
747
- type: "number",
748
- value: element.borderRadius ?? 0,
749
- onChange: (event) => updateElement(element.id, { borderRadius: Number(event.target.value) }),
750
- className: numberFieldClassName
751
- }
752
- )), /* @__PURE__ */ React3__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u586B\u5145", /* @__PURE__ */ React3__default.default.createElement(
753
- "select",
754
- {
755
- value: element.fit || "cover",
756
- onChange: (event) => updateElement(element.id, { fit: event.target.value }),
757
- className: numberFieldClassName
1035
+ key: element.id,
1036
+ className: `rounded-xl border bg-slate-50 p-3 ${selectedElementId === element.id ? "border-sky-400 ring-2 ring-sky-100" : "border-slate-200"}`
758
1037
  },
759
- /* @__PURE__ */ React3__default.default.createElement("option", { value: "cover" }, "cover"),
760
- /* @__PURE__ */ React3__default.default.createElement("option", { value: "contain" }, "contain")
761
- ))), /* @__PURE__ */ React3__default.default.createElement("label", { className: "inline-flex items-center gap-2 text-sm text-slate-700" }, /* @__PURE__ */ React3__default.default.createElement(
762
- "input",
763
- {
764
- type: "checkbox",
765
- checked: Boolean(element.isBackground),
766
- onChange: (event) => updateElement(element.id, { isBackground: event.target.checked }),
767
- className: "h-4 w-4 rounded border-slate-300 text-sky-600"
768
- }
769
- ), "\u4F5C\u4E3A\u672C\u9875\u80CC\u666F\u56FE")))))) : null);
1038
+ /* @__PURE__ */ React4__default.default.createElement("div", { className: "mb-2 flex items-center justify-between" }, /* @__PURE__ */ React4__default.default.createElement("div", { className: "text-xs font-semibold tracking-wide text-slate-500" }, element.type.toUpperCase()), /* @__PURE__ */ React4__default.default.createElement(
1039
+ "button",
1040
+ {
1041
+ type: "button",
1042
+ onClick: () => removeElement(element.id),
1043
+ className: "rounded-md border border-rose-300 bg-rose-50 px-2 py-1 text-xs font-medium text-rose-700"
1044
+ },
1045
+ "\u5220\u9664"
1046
+ )),
1047
+ element.type === "text" ? /* @__PURE__ */ React4__default.default.createElement("div", { className: "grid gap-2" }, /* @__PURE__ */ React4__default.default.createElement(
1048
+ "textarea",
1049
+ {
1050
+ value: element.content,
1051
+ onChange: (event) => updateElement(element.id, { content: event.target.value }),
1052
+ rows: 3,
1053
+ 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"
1054
+ }
1055
+ ), /* @__PURE__ */ React4__default.default.createElement("div", { className: "grid gap-2 sm:grid-cols-2" }, /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "X(%)", /* @__PURE__ */ React4__default.default.createElement(
1056
+ "input",
1057
+ {
1058
+ type: "number",
1059
+ value: element.x,
1060
+ onChange: (event) => updateElement(element.id, { x: Number(event.target.value) }),
1061
+ className: numberFieldClassName
1062
+ }
1063
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "Y(%)", /* @__PURE__ */ React4__default.default.createElement(
1064
+ "input",
1065
+ {
1066
+ type: "number",
1067
+ value: element.y,
1068
+ onChange: (event) => updateElement(element.id, { y: Number(event.target.value) }),
1069
+ className: numberFieldClassName
1070
+ }
1071
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5BBD\u5EA6(%)", /* @__PURE__ */ React4__default.default.createElement(
1072
+ "input",
1073
+ {
1074
+ type: "number",
1075
+ value: element.width ?? 70,
1076
+ onChange: (event) => updateElement(element.id, { width: Number(event.target.value) }),
1077
+ className: numberFieldClassName
1078
+ }
1079
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5B57\u53F7(px)", /* @__PURE__ */ React4__default.default.createElement(
1080
+ "input",
1081
+ {
1082
+ type: "number",
1083
+ value: element.fontSize ?? 18,
1084
+ onChange: (event) => updateElement(element.id, { fontSize: Number(event.target.value) }),
1085
+ className: numberFieldClassName
1086
+ }
1087
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5B57\u91CD", /* @__PURE__ */ React4__default.default.createElement(
1088
+ "input",
1089
+ {
1090
+ type: "number",
1091
+ min: 100,
1092
+ max: 900,
1093
+ step: 100,
1094
+ value: element.fontWeight ?? 500,
1095
+ onChange: (event) => updateElement(element.id, { fontWeight: Number(event.target.value) }),
1096
+ className: numberFieldClassName
1097
+ }
1098
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5BF9\u9F50", /* @__PURE__ */ React4__default.default.createElement(
1099
+ "select",
1100
+ {
1101
+ value: element.align || "left",
1102
+ onChange: (event) => updateElement(element.id, { align: event.target.value }),
1103
+ className: numberFieldClassName
1104
+ },
1105
+ /* @__PURE__ */ React4__default.default.createElement("option", { value: "left" }, "left"),
1106
+ /* @__PURE__ */ React4__default.default.createElement("option", { value: "center" }, "center"),
1107
+ /* @__PURE__ */ React4__default.default.createElement("option", { value: "right" }, "right")
1108
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600 sm:col-span-2" }, "\u5B57\u4F53", /* @__PURE__ */ React4__default.default.createElement(
1109
+ "input",
1110
+ {
1111
+ type: "text",
1112
+ value: element.fontFamily || "",
1113
+ onChange: (event) => updateElement(element.id, { fontFamily: event.target.value }),
1114
+ placeholder: "inherit / serif / sans-serif / PingFang SC",
1115
+ className: numberFieldClassName
1116
+ }
1117
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600 sm:col-span-2" }, "\u6587\u5B57\u989C\u8272", /* @__PURE__ */ React4__default.default.createElement("div", { className: "grid grid-cols-[64px_1fr] gap-2" }, /* @__PURE__ */ React4__default.default.createElement(
1118
+ "input",
1119
+ {
1120
+ type: "color",
1121
+ value: element.color || "#ffffff",
1122
+ onChange: (event) => updateElement(element.id, { color: event.target.value }),
1123
+ className: "h-10 rounded-lg border border-slate-300 bg-white p-1"
1124
+ }
1125
+ ), /* @__PURE__ */ React4__default.default.createElement(
1126
+ "input",
1127
+ {
1128
+ type: "text",
1129
+ value: element.color || "#ffffff",
1130
+ onChange: (event) => updateElement(element.id, { color: event.target.value }),
1131
+ className: numberFieldClassName
1132
+ }
1133
+ ))))) : /* @__PURE__ */ React4__default.default.createElement("div", { className: "grid gap-2" }, /* @__PURE__ */ React4__default.default.createElement(
1134
+ "input",
1135
+ {
1136
+ type: "url",
1137
+ value: element.src,
1138
+ onChange: (event) => updateElement(element.id, { src: event.target.value }),
1139
+ 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"
1140
+ }
1141
+ ), /* @__PURE__ */ React4__default.default.createElement("div", { className: "grid gap-2 sm:grid-cols-2" }, /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "X(%)", /* @__PURE__ */ React4__default.default.createElement(
1142
+ "input",
1143
+ {
1144
+ type: "number",
1145
+ value: element.x,
1146
+ onChange: (event) => updateElement(element.id, { x: Number(event.target.value) }),
1147
+ className: numberFieldClassName
1148
+ }
1149
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "Y(%)", /* @__PURE__ */ React4__default.default.createElement(
1150
+ "input",
1151
+ {
1152
+ type: "number",
1153
+ value: element.y,
1154
+ onChange: (event) => updateElement(element.id, { y: Number(event.target.value) }),
1155
+ className: numberFieldClassName
1156
+ }
1157
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5BBD\u5EA6(%)", /* @__PURE__ */ React4__default.default.createElement(
1158
+ "input",
1159
+ {
1160
+ type: "number",
1161
+ value: element.width ?? 60,
1162
+ onChange: (event) => updateElement(element.id, { width: Number(event.target.value) }),
1163
+ className: numberFieldClassName
1164
+ }
1165
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u9AD8\u5EA6(%)", /* @__PURE__ */ React4__default.default.createElement(
1166
+ "input",
1167
+ {
1168
+ type: "number",
1169
+ value: element.height ?? 40,
1170
+ onChange: (event) => updateElement(element.id, { height: Number(event.target.value) }),
1171
+ className: numberFieldClassName
1172
+ }
1173
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u5706\u89D2(px)", /* @__PURE__ */ React4__default.default.createElement(
1174
+ "input",
1175
+ {
1176
+ type: "number",
1177
+ value: element.borderRadius ?? 0,
1178
+ onChange: (event) => updateElement(element.id, { borderRadius: Number(event.target.value) }),
1179
+ className: numberFieldClassName
1180
+ }
1181
+ )), /* @__PURE__ */ React4__default.default.createElement("label", { className: "grid gap-1 text-xs text-slate-600" }, "\u586B\u5145", /* @__PURE__ */ React4__default.default.createElement(
1182
+ "select",
1183
+ {
1184
+ value: element.fit || "cover",
1185
+ onChange: (event) => updateElement(element.id, { fit: event.target.value }),
1186
+ className: numberFieldClassName
1187
+ },
1188
+ /* @__PURE__ */ React4__default.default.createElement("option", { value: "cover" }, "cover"),
1189
+ /* @__PURE__ */ React4__default.default.createElement("option", { value: "contain" }, "contain")
1190
+ ))), /* @__PURE__ */ React4__default.default.createElement("label", { className: "inline-flex items-center gap-2 text-sm text-slate-700" }, /* @__PURE__ */ React4__default.default.createElement(
1191
+ "input",
1192
+ {
1193
+ type: "checkbox",
1194
+ checked: Boolean(element.isBackground),
1195
+ onChange: (event) => updateElement(element.id, { isBackground: event.target.checked }),
1196
+ className: "h-4 w-4 rounded border-slate-300 text-sky-600"
1197
+ }
1198
+ ), "\u4F5C\u4E3A\u672C\u9875\u80CC\u666F\u56FE"))
1199
+ )))) : null);
770
1200
  };
771
1201
 
772
1202
  // src/festivalCard/components/FestivalCardStudio.tsx
@@ -776,8 +1206,55 @@ var FestivalCardStudio = ({ initialConfig, fetchConfig, onSave }) => {
776
1206
  fetchConfig,
777
1207
  onSave
778
1208
  });
779
- if (loading) return /* @__PURE__ */ React3__default.default.createElement("div", null, "\u52A0\u8F7D\u4E2D...");
780
- return /* @__PURE__ */ React3__default.default.createElement("div", { className: "grid items-start gap-4 lg:grid-cols-[1.45fr_1fr]" }, /* @__PURE__ */ React3__default.default.createElement(FestivalCardBook3D, { config, className: "h-full" }), /* @__PURE__ */ React3__default.default.createElement("div", { className: "lg:sticky lg:top-4" }, /* @__PURE__ */ React3__default.default.createElement(FestivalCardConfigEditor, { value: config, onChange: setConfig }), onSave ? /* @__PURE__ */ React3__default.default.createElement(
1209
+ const [activePageIndex, setActivePageIndex] = React4.useState(0);
1210
+ const [selectedElementId, setSelectedElementId] = React4.useState(null);
1211
+ React4.useEffect(() => {
1212
+ if (config.pages.length === 0) return;
1213
+ if (activePageIndex <= config.pages.length - 1) return;
1214
+ setActivePageIndex(config.pages.length - 1);
1215
+ }, [activePageIndex, config.pages.length]);
1216
+ const updateElementByPreview = (pageIndex, elementId, patch) => {
1217
+ setConfig((prev) => ({
1218
+ ...prev,
1219
+ pages: prev.pages.map(
1220
+ (page, index) => index === pageIndex ? {
1221
+ ...page,
1222
+ elements: page.elements.map(
1223
+ (element) => element.id === elementId ? { ...element, ...patch } : element
1224
+ )
1225
+ } : page
1226
+ )
1227
+ }));
1228
+ };
1229
+ if (loading) return /* @__PURE__ */ React4__default.default.createElement("div", null, "\u52A0\u8F7D\u4E2D...");
1230
+ return /* @__PURE__ */ React4__default.default.createElement("div", { className: "grid items-start gap-4 lg:grid-cols-[1.45fr_1fr]" }, /* @__PURE__ */ React4__default.default.createElement(
1231
+ FestivalCardBook3D,
1232
+ {
1233
+ config,
1234
+ className: "h-full",
1235
+ editable: true,
1236
+ currentPage: activePageIndex,
1237
+ onCurrentPageChange: (index) => {
1238
+ setActivePageIndex(index);
1239
+ setSelectedElementId(null);
1240
+ },
1241
+ selectedElementId,
1242
+ onSelectedElementChange: setSelectedElementId,
1243
+ onElementChange: updateElementByPreview
1244
+ }
1245
+ ), /* @__PURE__ */ React4__default.default.createElement("div", { className: "lg:sticky lg:top-4" }, /* @__PURE__ */ React4__default.default.createElement(
1246
+ FestivalCardConfigEditor,
1247
+ {
1248
+ value: config,
1249
+ onChange: setConfig,
1250
+ activePageIndex,
1251
+ onActivePageIndexChange: (index) => {
1252
+ setActivePageIndex(index);
1253
+ setSelectedElementId(null);
1254
+ },
1255
+ selectedElementId
1256
+ }
1257
+ ), onSave ? /* @__PURE__ */ React4__default.default.createElement(
781
1258
  "button",
782
1259
  {
783
1260
  type: "button",
@@ -795,8 +1272,8 @@ var FestivalCardConfigPage = ({
795
1272
  cardId,
796
1273
  mainPagePath = "/festivalCard"
797
1274
  }) => {
798
- const [list, setList] = React3.useState([]);
799
- const [selectedId, setSelectedId] = React3.useState(cardId || "default-festival-card");
1275
+ const [list, setList] = React4.useState([]);
1276
+ const [selectedId, setSelectedId] = React4.useState(cardId || "default-festival-card");
800
1277
  const parseListResponse2 = (data) => {
801
1278
  if (!data || typeof data !== "object") return [];
802
1279
  const payload = data.data;
@@ -809,7 +1286,7 @@ var FestivalCardConfigPage = ({
809
1286
  if (!payload || typeof payload !== "object") return normalizeFestivalCardConfig();
810
1287
  return normalizeFestivalCardConfig(payload);
811
1288
  };
812
- const reloadList = React3.useCallback(async () => {
1289
+ const reloadList = React4.useCallback(async () => {
813
1290
  try {
814
1291
  const response = await fetch(apiBase, { cache: "no-store" });
815
1292
  if (!response.ok) throw new Error(`\u52A0\u8F7D\u5361\u7247\u5217\u8868\u5931\u8D25: ${response.status}`);
@@ -832,7 +1309,7 @@ var FestivalCardConfigPage = ({
832
1309
  window.alert(error.message || "\u52A0\u8F7D\u5361\u7247\u5217\u8868\u5931\u8D25");
833
1310
  }
834
1311
  }, [apiBase]);
835
- React3.useEffect(() => {
1312
+ React4.useEffect(() => {
836
1313
  void reloadList();
837
1314
  }, [reloadList]);
838
1315
  const fetchConfig = async () => {
@@ -882,16 +1359,16 @@ var FestivalCardConfigPage = ({
882
1359
  window.alert(error.message || "\u521B\u5EFA\u5931\u8D25");
883
1360
  }
884
1361
  };
885
- const mainLink = React3.useMemo(() => `${mainPagePath}?cardId=${encodeURIComponent(selectedId)}`, [mainPagePath, selectedId]);
886
- return /* @__PURE__ */ React3__default.default.createElement("div", { className: "grid gap-4" }, /* @__PURE__ */ React3__default.default.createElement("div", { className: "flex flex-wrap items-center gap-2 rounded-xl border border-slate-800 bg-slate-900/50 p-3" }, /* @__PURE__ */ React3__default.default.createElement(
1362
+ const mainLink = React4.useMemo(() => `${mainPagePath}?cardId=${encodeURIComponent(selectedId)}`, [mainPagePath, selectedId]);
1363
+ return /* @__PURE__ */ React4__default.default.createElement("div", { className: "grid gap-4" }, /* @__PURE__ */ React4__default.default.createElement("div", { className: "flex flex-wrap items-center gap-2 rounded-xl border border-slate-800 bg-slate-900/50 p-3" }, /* @__PURE__ */ React4__default.default.createElement(
887
1364
  "select",
888
1365
  {
889
1366
  value: selectedId,
890
1367
  onChange: (event) => setSelectedId(event.target.value),
891
1368
  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"
892
1369
  },
893
- list.map((item) => /* @__PURE__ */ React3__default.default.createElement("option", { key: item.id, value: item.id }, item.name || item.id))
894
- ), /* @__PURE__ */ React3__default.default.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__default.default.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__default.default.createElement(FestivalCardStudio, { fetchConfig, onSave: saveConfig }));
1370
+ list.map((item) => /* @__PURE__ */ React4__default.default.createElement("option", { key: item.id, value: item.id }, item.name || item.id))
1371
+ ), /* @__PURE__ */ React4__default.default.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__default.default.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__default.default.createElement(FestivalCardStudio, { fetchConfig, onSave: saveConfig }));
895
1372
  };
896
1373
  var isSummary = (value) => {
897
1374
  if (!value || typeof value !== "object") return false;
@@ -914,10 +1391,10 @@ var FestivalCardManagedPage = ({
914
1391
  apiBase = "/api/festivalCard",
915
1392
  cardId
916
1393
  }) => {
917
- const [currentCardId, setCurrentCardId] = React3.useState(cardId || "");
918
- const [config, setConfig] = React3.useState(null);
919
- const [loading, setLoading] = React3.useState(true);
920
- React3.useEffect(() => {
1394
+ const [currentCardId, setCurrentCardId] = React4.useState(cardId || "");
1395
+ const [config, setConfig] = React4.useState(null);
1396
+ const [loading, setLoading] = React4.useState(true);
1397
+ React4.useEffect(() => {
921
1398
  const fetchList = async () => {
922
1399
  const response = await fetch(apiBase, { cache: "no-store" });
923
1400
  const data = await response.json();
@@ -929,12 +1406,12 @@ var FestivalCardManagedPage = ({
929
1406
  };
930
1407
  void fetchList();
931
1408
  }, [apiBase, currentCardId]);
932
- React3.useEffect(() => {
1409
+ React4.useEffect(() => {
933
1410
  if (!currentCardId) return;
934
1411
  setLoading(true);
935
1412
  void fetch(`${apiBase}/${encodeURIComponent(currentCardId)}`, { cache: "no-store" }).then((res) => res.json()).then((data) => setConfig(parseConfigResponse(data))).finally(() => setLoading(false));
936
1413
  }, [apiBase, currentCardId]);
937
- return /* @__PURE__ */ React3__default.default.createElement("div", null, loading || !config ? /* @__PURE__ */ React3__default.default.createElement("div", { className: "py-12 text-center text-slate-400" }, "\u52A0\u8F7D\u4E2D...") : /* @__PURE__ */ React3__default.default.createElement(FestivalCardBook3D, { config }));
1414
+ return /* @__PURE__ */ React4__default.default.createElement("div", null, loading || !config ? /* @__PURE__ */ React4__default.default.createElement("div", { className: "py-12 text-center text-slate-400" }, "\u52A0\u8F7D\u4E2D...") : /* @__PURE__ */ React4__default.default.createElement(FestivalCardBook3D, { config }));
938
1415
  };
939
1416
 
940
1417
  // src/festivalCard/server/db.ts