sa2kit 3.2.0 → 3.2.1

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 (217) hide show
  1. package/dist/CollisionBalls-DgKtscU2.d.mts +41 -0
  2. package/dist/CollisionBalls-DgKtscU2.d.ts +41 -0
  3. package/dist/ConfigService-Oga_zFRS.d.mts +262 -0
  4. package/dist/ConfigService-Oga_zFRS.d.ts +262 -0
  5. package/dist/UniversalFileService-CC4d3wkc.d.ts +139 -0
  6. package/dist/UniversalFileService-CzAE_G4V.d.mts +139 -0
  7. package/dist/boothVaultService-lKcnyA-u.d.mts +83 -0
  8. package/dist/boothVaultService-lKcnyA-u.d.ts +83 -0
  9. package/dist/business/audioDetection/index.d.mts +2 -0
  10. package/dist/business/audioDetection/index.d.ts +2 -0
  11. package/dist/business/audioDetection/index.js +1244 -0
  12. package/dist/business/audioDetection/index.js.map +1 -0
  13. package/dist/business/audioDetection/index.mjs +1227 -0
  14. package/dist/business/audioDetection/index.mjs.map +1 -0
  15. package/dist/business/calendar/index.d.mts +6 -0
  16. package/dist/business/calendar/index.d.ts +6 -0
  17. package/dist/business/calendar/index.js +7433 -0
  18. package/dist/business/calendar/index.js.map +1 -0
  19. package/dist/business/calendar/index.mjs +7257 -0
  20. package/dist/business/calendar/index.mjs.map +1 -0
  21. package/dist/business/calendar/routes/index.d.mts +191 -0
  22. package/dist/business/calendar/routes/index.d.ts +191 -0
  23. package/dist/business/calendar/routes/index.js +844 -0
  24. package/dist/business/calendar/routes/index.js.map +1 -0
  25. package/dist/business/calendar/routes/index.mjs +826 -0
  26. package/dist/business/calendar/routes/index.mjs.map +1 -0
  27. package/dist/business/festivalCard/index.d.mts +4 -0
  28. package/dist/business/festivalCard/index.d.ts +4 -0
  29. package/dist/business/festivalCard/index.js +1492 -0
  30. package/dist/business/festivalCard/index.js.map +1 -0
  31. package/dist/business/festivalCard/index.mjs +1475 -0
  32. package/dist/business/festivalCard/index.mjs.map +1 -0
  33. package/dist/business/festivalCard/routes/index.d.mts +42 -0
  34. package/dist/business/festivalCard/routes/index.d.ts +42 -0
  35. package/dist/business/festivalCard/routes/index.js +361 -0
  36. package/dist/business/festivalCard/routes/index.js.map +1 -0
  37. package/dist/business/festivalCard/routes/index.mjs +356 -0
  38. package/dist/business/festivalCard/routes/index.mjs.map +1 -0
  39. package/dist/business/festivalCard/server/index.d.mts +120 -0
  40. package/dist/business/festivalCard/server/index.d.ts +120 -0
  41. package/dist/business/festivalCard/server/index.js +272 -0
  42. package/dist/business/festivalCard/server/index.js.map +1 -0
  43. package/dist/business/festivalCard/server/index.mjs +265 -0
  44. package/dist/business/festivalCard/server/index.mjs.map +1 -0
  45. package/dist/business/index.d.mts +34 -0
  46. package/dist/business/index.d.ts +34 -0
  47. package/dist/business/index.js +29282 -0
  48. package/dist/business/index.js.map +1 -0
  49. package/dist/business/index.mjs +29237 -0
  50. package/dist/business/index.mjs.map +1 -0
  51. package/dist/business/mikuContest/ui/web/index.d.mts +2 -0
  52. package/dist/business/mikuContest/ui/web/index.d.ts +2 -0
  53. package/dist/business/mikuContest/ui/web/index.js +353 -0
  54. package/dist/business/mikuContest/ui/web/index.js.map +1 -0
  55. package/dist/business/mikuContest/ui/web/index.mjs +343 -0
  56. package/dist/business/mikuContest/ui/web/index.mjs.map +1 -0
  57. package/dist/business/mikuFireworks3D/index.d.mts +2 -0
  58. package/dist/business/mikuFireworks3D/index.d.ts +2 -0
  59. package/dist/business/mikuFireworks3D/index.js +1267 -0
  60. package/dist/business/mikuFireworks3D/index.js.map +1 -0
  61. package/dist/business/mikuFireworks3D/index.mjs +1228 -0
  62. package/dist/business/mikuFireworks3D/index.mjs.map +1 -0
  63. package/dist/business/mikuFusionGame/index.d.mts +2 -0
  64. package/dist/business/mikuFusionGame/index.d.ts +2 -0
  65. package/dist/business/mikuFusionGame/index.js +1208 -0
  66. package/dist/business/mikuFusionGame/index.js.map +1 -0
  67. package/dist/business/mikuFusionGame/index.mjs +1195 -0
  68. package/dist/business/mikuFusionGame/index.mjs.map +1 -0
  69. package/dist/business/mmd/admin/index.d.mts +487 -0
  70. package/dist/business/mmd/admin/index.d.ts +487 -0
  71. package/dist/business/mmd/admin/index.js +1058 -0
  72. package/dist/business/mmd/admin/index.js.map +1 -0
  73. package/dist/business/mmd/admin/index.mjs +1027 -0
  74. package/dist/business/mmd/admin/index.mjs.map +1 -0
  75. package/dist/business/mmd/index.d.mts +5 -0
  76. package/dist/business/mmd/index.d.ts +5 -0
  77. package/dist/business/mmd/index.js +10119 -0
  78. package/dist/business/mmd/index.js.map +1 -0
  79. package/dist/business/mmd/index.mjs +10028 -0
  80. package/dist/business/mmd/index.mjs.map +1 -0
  81. package/dist/business/mmd/server/index.d.mts +139 -0
  82. package/dist/business/mmd/server/index.d.ts +139 -0
  83. package/dist/business/mmd/server/index.js +424 -0
  84. package/dist/business/mmd/server/index.js.map +1 -0
  85. package/dist/business/mmd/server/index.mjs +404 -0
  86. package/dist/business/mmd/server/index.mjs.map +1 -0
  87. package/dist/business/music/index.d.mts +3 -0
  88. package/dist/business/music/index.d.ts +3 -0
  89. package/dist/business/music/index.js +830 -0
  90. package/dist/business/music/index.js.map +1 -0
  91. package/dist/business/music/index.mjs +809 -0
  92. package/dist/business/music/index.mjs.map +1 -0
  93. package/dist/business/music/server/index.d.mts +1 -0
  94. package/dist/business/music/server/index.d.ts +1 -0
  95. package/dist/business/music/server/index.js +194 -0
  96. package/dist/business/music/server/index.js.map +1 -0
  97. package/dist/business/music/server/index.mjs +182 -0
  98. package/dist/business/music/server/index.mjs.map +1 -0
  99. package/dist/business/navigation/index.d.mts +2 -0
  100. package/dist/business/navigation/index.d.ts +2 -0
  101. package/dist/business/navigation/index.js +453 -0
  102. package/dist/business/navigation/index.js.map +1 -0
  103. package/dist/business/navigation/index.mjs +443 -0
  104. package/dist/business/navigation/index.mjs.map +1 -0
  105. package/dist/business/portfolio/index.d.mts +3 -0
  106. package/dist/business/portfolio/index.d.ts +3 -0
  107. package/dist/business/portfolio/index.js +736 -0
  108. package/dist/business/portfolio/index.js.map +1 -0
  109. package/dist/business/portfolio/index.mjs +724 -0
  110. package/dist/business/portfolio/index.mjs.map +1 -0
  111. package/dist/business/qqbot/server/index.d.mts +167 -0
  112. package/dist/business/qqbot/server/index.d.ts +167 -0
  113. package/dist/business/qqbot/server/index.js +394 -0
  114. package/dist/business/qqbot/server/index.js.map +1 -0
  115. package/dist/business/qqbot/server/index.mjs +385 -0
  116. package/dist/business/qqbot/server/index.mjs.map +1 -0
  117. package/dist/business/qqbot/ui/web/index.d.mts +10 -0
  118. package/dist/business/qqbot/ui/web/index.d.ts +10 -0
  119. package/dist/business/qqbot/ui/web/index.js +105 -0
  120. package/dist/business/qqbot/ui/web/index.js.map +1 -0
  121. package/dist/business/qqbot/ui/web/index.mjs +99 -0
  122. package/dist/business/qqbot/ui/web/index.mjs.map +1 -0
  123. package/dist/business/screenReceiver/index.d.mts +2 -0
  124. package/dist/business/screenReceiver/index.d.ts +2 -0
  125. package/dist/business/screenReceiver/index.js +281 -0
  126. package/dist/business/screenReceiver/index.js.map +1 -0
  127. package/dist/business/screenReceiver/index.mjs +273 -0
  128. package/dist/business/screenReceiver/index.mjs.map +1 -0
  129. package/dist/business/testYourself/admin/index.d.mts +58 -0
  130. package/dist/business/testYourself/admin/index.d.ts +58 -0
  131. package/dist/business/testYourself/admin/index.js +1009 -0
  132. package/dist/business/testYourself/admin/index.js.map +1 -0
  133. package/dist/business/testYourself/admin/index.mjs +1002 -0
  134. package/dist/business/testYourself/admin/index.mjs.map +1 -0
  135. package/dist/business/testYourself/index.d.mts +6 -0
  136. package/dist/business/testYourself/index.d.ts +6 -0
  137. package/dist/business/testYourself/index.js +2551 -0
  138. package/dist/business/testYourself/index.js.map +1 -0
  139. package/dist/business/testYourself/index.mjs +2531 -0
  140. package/dist/business/testYourself/index.mjs.map +1 -0
  141. package/dist/business/testYourself/server/index.d.mts +1029 -0
  142. package/dist/business/testYourself/server/index.d.ts +1029 -0
  143. package/dist/business/testYourself/server/index.js +825 -0
  144. package/dist/business/testYourself/server/index.js.map +1 -0
  145. package/dist/business/testYourself/server/index.mjs +816 -0
  146. package/dist/business/testYourself/server/index.mjs.map +1 -0
  147. package/dist/business/vocaloidBooth/index.d.mts +2 -0
  148. package/dist/business/vocaloidBooth/index.d.ts +2 -0
  149. package/dist/business/vocaloidBooth/index.js +172 -0
  150. package/dist/business/vocaloidBooth/index.js.map +1 -0
  151. package/dist/business/vocaloidBooth/index.mjs +166 -0
  152. package/dist/business/vocaloidBooth/index.mjs.map +1 -0
  153. package/dist/business/vocaloidBooth/server/index.d.mts +111 -0
  154. package/dist/business/vocaloidBooth/server/index.d.ts +111 -0
  155. package/dist/business/vocaloidBooth/server/index.js +247 -0
  156. package/dist/business/vocaloidBooth/server/index.js.map +1 -0
  157. package/dist/business/vocaloidBooth/server/index.mjs +237 -0
  158. package/dist/business/vocaloidBooth/server/index.mjs.map +1 -0
  159. package/dist/business/vocaloidBooth/web/index.d.mts +45 -0
  160. package/dist/business/vocaloidBooth/web/index.d.ts +45 -0
  161. package/dist/business/vocaloidBooth/web/index.js +376 -0
  162. package/dist/business/vocaloidBooth/web/index.js.map +1 -0
  163. package/dist/business/vocaloidBooth/web/index.mjs +362 -0
  164. package/dist/business/vocaloidBooth/web/index.mjs.map +1 -0
  165. package/dist/config-BQp3qLAL.d.mts +22 -0
  166. package/dist/config-BQp3qLAL.d.ts +22 -0
  167. package/dist/drizzle-schema-BNhqj2AZ.d.mts +1114 -0
  168. package/dist/drizzle-schema-BNhqj2AZ.d.ts +1114 -0
  169. package/dist/festivalCardService-D60G-sgr.d.mts +13 -0
  170. package/dist/festivalCardService-DnLyJpRh.d.ts +13 -0
  171. package/dist/index-BMgdH5dL.d.mts +1716 -0
  172. package/dist/index-BO9_Do5y.d.mts +93 -0
  173. package/dist/index-BO9_Do5y.d.ts +93 -0
  174. package/dist/index-BSmd4ikf.d.ts +76 -0
  175. package/dist/index-BSwvWYp2.d.mts +2632 -0
  176. package/dist/index-Bo_fW3Tl.d.mts +105 -0
  177. package/dist/index-Bo_fW3Tl.d.ts +105 -0
  178. package/dist/index-BrKazb8M.d.mts +148 -0
  179. package/dist/index-BrKazb8M.d.ts +148 -0
  180. package/dist/index-Bzh6QE4P.d.ts +25 -0
  181. package/dist/index-C5Ic6eSR.d.mts +25 -0
  182. package/dist/index-C8i9SIxk.d.ts +2632 -0
  183. package/dist/index-C_GhVhOT.d.mts +109 -0
  184. package/dist/index-C_GhVhOT.d.ts +109 -0
  185. package/dist/index-Cb3UEpG4.d.mts +101 -0
  186. package/dist/index-CjlkUj01.d.mts +103 -0
  187. package/dist/index-CucXCBNR.d.mts +302 -0
  188. package/dist/index-CucXCBNR.d.ts +302 -0
  189. package/dist/index-DLLPTprx.d.mts +1522 -0
  190. package/dist/index-DRiZy0dv.d.mts +525 -0
  191. package/dist/index-DRiZy0dv.d.ts +525 -0
  192. package/dist/index-Dc_I2t0P.d.mts +103 -0
  193. package/dist/index-DowAHRIP.d.mts +250 -0
  194. package/dist/index-DowAHRIP.d.ts +250 -0
  195. package/dist/index-Dpq_5H2n.d.ts +103 -0
  196. package/dist/index-Ds2M_9zb.d.ts +101 -0
  197. package/dist/index-IXMAeTtN.d.ts +1716 -0
  198. package/dist/index-VFDbZxVM.d.ts +1522 -0
  199. package/dist/index-jadkp96n.d.ts +103 -0
  200. package/dist/index-r2-zE3iC.d.mts +76 -0
  201. package/dist/index.d.mts +10682 -0
  202. package/dist/index.d.ts +10682 -0
  203. package/dist/index.js +38233 -0
  204. package/dist/index.js.map +1 -0
  205. package/dist/index.mjs +37959 -0
  206. package/dist/index.mjs.map +1 -0
  207. package/dist/types-B6B210gX.d.mts +270 -0
  208. package/dist/types-B6B210gX.d.ts +270 -0
  209. package/dist/types-B7voqjjA.d.mts +51 -0
  210. package/dist/types-B7voqjjA.d.ts +51 -0
  211. package/dist/types-Bdnte5EN.d.mts +292 -0
  212. package/dist/types-C2z_QQPI.d.mts +70 -0
  213. package/dist/types-C2z_QQPI.d.ts +70 -0
  214. package/dist/types-HorDyIRv.d.mts +303 -0
  215. package/dist/types-HorDyIRv.d.ts +303 -0
  216. package/dist/types-_rFX1atk.d.ts +292 -0
  217. package/package.json +3 -2
@@ -0,0 +1,1492 @@
1
+ 'use strict';
2
+
3
+ var React4 = require('react');
4
+ var reactDom = require('react-dom');
5
+ var clsx = require('clsx');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var React4__default = /*#__PURE__*/_interopDefault(React4);
10
+
11
+ // src/business/festivalCard/core/defaults.ts
12
+ var DEFAULT_FESTIVAL_CARD_CONFIG = {
13
+ id: "default-festival-card",
14
+ name: "Holiday Card",
15
+ theme: "winter",
16
+ coverTitle: "Happy Holidays",
17
+ coverSubtitle: "Warm wishes for you",
18
+ background: {
19
+ colorA: "#0c1a34",
20
+ colorB: "#1f4f8a"
21
+ },
22
+ backgroundMusic: {
23
+ src: "",
24
+ loop: true,
25
+ autoPlay: false,
26
+ volume: 0.5
27
+ },
28
+ pages: [
29
+ {
30
+ id: "page-1",
31
+ title: "\u5C01\u9762",
32
+ background: { color: "#11284d" },
33
+ elements: [
34
+ {
35
+ id: "p1-text-1",
36
+ type: "text",
37
+ x: 50,
38
+ y: 20,
39
+ content: "\u65B0\u5E74\u5FEB\u4E50",
40
+ fontSize: 34,
41
+ fontWeight: 700,
42
+ align: "center",
43
+ color: "#f8fafc"
44
+ },
45
+ {
46
+ id: "p1-text-2",
47
+ type: "text",
48
+ x: 50,
49
+ y: 36,
50
+ content: "\u613F\u8FD9\u4E00\u5E74\u5E73\u5B89\u559C\u4E50",
51
+ fontSize: 18,
52
+ fontWeight: 500,
53
+ align: "center",
54
+ color: "#dbeafe"
55
+ },
56
+ {
57
+ id: "p1-image-1",
58
+ type: "image",
59
+ x: 50,
60
+ y: 68,
61
+ width: 72,
62
+ height: 42,
63
+ src: "https://images.unsplash.com/photo-1512389142860-9c449e58a543?auto=format&fit=crop&w=1200&q=80",
64
+ fit: "cover",
65
+ borderRadius: 16,
66
+ alt: "holiday",
67
+ isBackground: false
68
+ }
69
+ ]
70
+ },
71
+ {
72
+ id: "page-2",
73
+ title: "\u795D\u798F",
74
+ background: { color: "#1a3766" },
75
+ elements: [
76
+ {
77
+ id: "p2-text-1",
78
+ type: "text",
79
+ x: 50,
80
+ y: 28,
81
+ content: "\u613F\u4F60\u65B0\u5C81\uFF1A",
82
+ fontSize: 28,
83
+ fontWeight: 700,
84
+ align: "center",
85
+ color: "#fef3c7"
86
+ },
87
+ {
88
+ id: "p2-text-2",
89
+ type: "text",
90
+ x: 50,
91
+ y: 42,
92
+ content: "\u6240\u5F97\u7686\u6240\u671F\uFF0C\u6240\u5931\u4EA6\u65E0\u788D",
93
+ fontSize: 18,
94
+ fontWeight: 500,
95
+ align: "center",
96
+ color: "#f8fafc"
97
+ },
98
+ {
99
+ id: "p2-text-3",
100
+ type: "text",
101
+ x: 50,
102
+ y: 55,
103
+ content: "\u613F\u4F60\u7684\u6BCF\u4E00\u6B65\u90FD\u8D70\u5411\u5149\u4EAE",
104
+ fontSize: 16,
105
+ fontWeight: 400,
106
+ align: "center",
107
+ color: "#dbeafe"
108
+ }
109
+ ]
110
+ },
111
+ {
112
+ id: "page-3",
113
+ title: "\u843D\u6B3E",
114
+ background: { color: "#11284d" },
115
+ elements: [
116
+ {
117
+ id: "p3-image-1",
118
+ type: "image",
119
+ x: 50,
120
+ y: 34,
121
+ width: 60,
122
+ height: 42,
123
+ src: "https://images.unsplash.com/photo-1456324504439-367cee3b3c32?auto=format&fit=crop&w=1200&q=80",
124
+ fit: "cover",
125
+ borderRadius: 14,
126
+ alt: "gift",
127
+ isBackground: false
128
+ },
129
+ {
130
+ id: "p3-text-1",
131
+ type: "text",
132
+ x: 50,
133
+ y: 72,
134
+ content: "Best wishes, from SA2Kit",
135
+ fontSize: 18,
136
+ fontWeight: 600,
137
+ align: "center",
138
+ color: "#f8fafc"
139
+ }
140
+ ]
141
+ }
142
+ ]
143
+ };
144
+
145
+ // src/business/festivalCard/core/normalize.ts
146
+ var ensurePage = (page, index) => ({
147
+ id: page.id || `page-${index + 1}`,
148
+ title: page.title || `\u7B2C ${index + 1} \u9875`,
149
+ elements: Array.isArray(page.elements) ? page.elements : [],
150
+ background: page.background || {}
151
+ });
152
+ var normalizeFestivalCardConfig = (config) => {
153
+ const pages = config?.pages && config.pages.length > 0 ? config.pages : DEFAULT_FESTIVAL_CARD_CONFIG.pages;
154
+ return {
155
+ ...DEFAULT_FESTIVAL_CARD_CONFIG,
156
+ ...config,
157
+ background: {
158
+ ...DEFAULT_FESTIVAL_CARD_CONFIG.background,
159
+ ...config?.background || {}
160
+ },
161
+ backgroundMusic: {
162
+ ...DEFAULT_FESTIVAL_CARD_CONFIG.backgroundMusic,
163
+ ...config?.backgroundMusic || {},
164
+ src: config?.backgroundMusic?.src ?? DEFAULT_FESTIVAL_CARD_CONFIG.backgroundMusic?.src ?? ""
165
+ },
166
+ pages: pages.map(ensurePage)
167
+ };
168
+ };
169
+ var resizeFestivalCardPages = (config, pageCount) => {
170
+ const safeCount = Math.max(1, Math.min(12, Math.floor(pageCount || 1)));
171
+ const nextPages = [...config.pages];
172
+ while (nextPages.length < safeCount) {
173
+ const idx = nextPages.length;
174
+ nextPages.push({
175
+ id: `page-${idx + 1}`,
176
+ title: `\u7B2C ${idx + 1} \u9875`,
177
+ elements: [],
178
+ background: {}
179
+ });
180
+ }
181
+ return {
182
+ ...config,
183
+ pages: nextPages.slice(0, safeCount)
184
+ };
185
+ };
186
+ var useFestivalCardConfig = (options) => {
187
+ const [config, setConfig] = React4.useState(
188
+ () => normalizeFestivalCardConfig(options?.initialConfig || DEFAULT_FESTIVAL_CARD_CONFIG)
189
+ );
190
+ const [loading, setLoading] = React4.useState(Boolean(options?.fetchConfig));
191
+ const [saving, setSaving] = React4.useState(false);
192
+ React4.useEffect(() => {
193
+ const fetchConfig = options?.fetchConfig;
194
+ if (!fetchConfig) return;
195
+ let active = true;
196
+ void fetchConfig().then((value) => {
197
+ if (!active) return;
198
+ setConfig(normalizeFestivalCardConfig(value));
199
+ }).finally(() => {
200
+ if (active) setLoading(false);
201
+ });
202
+ return () => {
203
+ active = false;
204
+ };
205
+ }, [options?.fetchConfig]);
206
+ const save = React4.useCallback(async () => {
207
+ const onSave = options?.onSave;
208
+ if (!onSave) return;
209
+ setSaving(true);
210
+ try {
211
+ await onSave(config);
212
+ } finally {
213
+ setSaving(false);
214
+ }
215
+ }, [config, options?.onSave]);
216
+ return React4.useMemo(
217
+ () => ({
218
+ config,
219
+ setConfig,
220
+ loading,
221
+ saving,
222
+ save
223
+ }),
224
+ [config, loading, save, saving]
225
+ );
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;
411
+ var elementStyle = (element) => ({
412
+ position: "absolute",
413
+ zIndex: 2,
414
+ left: `${element.x}%`,
415
+ top: `${element.y}%`,
416
+ width: `${element.width ?? 70}%`,
417
+ height: element.height ? `${element.height}%` : void 0,
418
+ transform: "translate(-50%, -50%)"
419
+ });
420
+ var renderElement = (element) => {
421
+ if (element.type === "text") {
422
+ return /* @__PURE__ */ React4__default.default.createElement(
423
+ "div",
424
+ {
425
+ key: element.id,
426
+ style: {
427
+ ...elementStyle(element),
428
+ color: element.color || "#f8fafc",
429
+ fontSize: element.fontSize || 18,
430
+ fontWeight: element.fontWeight || 500,
431
+ fontFamily: element.fontFamily || "inherit",
432
+ textAlign: element.align || "left",
433
+ lineHeight: 1.45,
434
+ whiteSpace: "pre-wrap"
435
+ }
436
+ },
437
+ element.content
438
+ );
439
+ }
440
+ return /* @__PURE__ */ React4__default.default.createElement(
441
+ "img",
442
+ {
443
+ key: element.id,
444
+ src: element.src,
445
+ alt: element.alt || "festival-card-image",
446
+ style: {
447
+ ...elementStyle(element),
448
+ objectFit: element.fit || "cover",
449
+ borderRadius: element.borderRadius || 0,
450
+ overflow: "hidden",
451
+ boxShadow: "0 12px 30px rgba(2, 6, 23, 0.32)"
452
+ }
453
+ }
454
+ );
455
+ };
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]
473
+ );
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(
534
+ "div",
535
+ {
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" : ""}`,
542
+ style: {
543
+ backgroundColor: page.background?.color || "#0f172a",
544
+ backgroundImage: backgroundElement ? `url(${backgroundElement.src})` : page.background?.image ? `url(${page.background.image})` : void 0,
545
+ backgroundSize: "cover",
546
+ backgroundPosition: "center"
547
+ }
548
+ },
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
+ })
615
+ );
616
+ };
617
+
618
+ // src/business/festivalCard/components/FestivalCardBook3D.tsx
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]);
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
+ };
777
+ const canPrev = currentPage > 0;
778
+ const canNext = currentPage < pages.length - 1;
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(
794
+ "div",
795
+ {
796
+ key: page.id,
797
+ className: "absolute inset-0 transition-opacity duration-500 ease-out",
798
+ style: {
799
+ opacity: index === currentPage ? 1 : 0,
800
+ pointerEvents: index === currentPage ? "auto" : "none"
801
+ }
802
+ },
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(
814
+ "button",
815
+ {
816
+ type: "button",
817
+ disabled: !canPrev,
818
+ onClick: () => setCurrentPage((p) => Math.max(0, p - 1)),
819
+ className: "rounded-full bg-white px-5 py-2 text-sm font-medium text-slate-900 disabled:cursor-not-allowed disabled:opacity-45"
820
+ },
821
+ "\u4E0A\u4E00\u9875"
822
+ ), /* @__PURE__ */ React4__default.default.createElement(
823
+ "button",
824
+ {
825
+ type: "button",
826
+ disabled: !canNext,
827
+ onClick: () => setCurrentPage((p) => Math.min(pages.length - 1, p + 1)),
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"
829
+ },
830
+ "\u4E0B\u4E00\u9875"
831
+ ))), normalized.backgroundMusic?.src ? /* @__PURE__ */ React4__default.default.createElement(
832
+ "audio",
833
+ {
834
+ src: normalized.backgroundMusic.src,
835
+ autoPlay: normalized.backgroundMusic.autoPlay,
836
+ loop: normalized.backgroundMusic.loop,
837
+ controls: true,
838
+ className: "mt-3 w-full"
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
+ }
858
+ ) : null);
859
+ };
860
+ var createTextElement = (pageIndex) => ({
861
+ id: `text-${Date.now()}-${pageIndex}`,
862
+ type: "text",
863
+ x: 50,
864
+ y: 50,
865
+ width: 70,
866
+ content: "\u8BF7\u8F93\u5165\u6587\u5B57",
867
+ fontSize: 18,
868
+ fontWeight: 500,
869
+ align: "center",
870
+ color: "#ffffff",
871
+ fontFamily: "inherit"
872
+ });
873
+ var createImageElement = (pageIndex) => ({
874
+ id: `image-${Date.now()}-${pageIndex}`,
875
+ type: "image",
876
+ x: 50,
877
+ y: 50,
878
+ width: 60,
879
+ height: 40,
880
+ src: "https://images.unsplash.com/photo-1482517967863-00e15c9b44be?auto=format&fit=crop&w=1200&q=80",
881
+ fit: "cover",
882
+ borderRadius: 12
883
+ });
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
+ };
900
+ const page = value.pages[activePageIndex];
901
+ const canEditPage = Boolean(page);
902
+ const pageOptions = React4.useMemo(() => value.pages.map((_, index) => index), [value.pages]);
903
+ const handlePageCountChange = (nextRaw) => {
904
+ const next = Number.isFinite(nextRaw) ? Math.max(1, Math.min(12, Math.floor(nextRaw))) : value.pages.length;
905
+ const resized = resizeFestivalCardPages(value, next);
906
+ onChange(resized);
907
+ if (activePageIndex > resized.pages.length - 1) {
908
+ setActivePageIndex(resized.pages.length - 1);
909
+ }
910
+ };
911
+ const updateElement = (elementId, patch) => {
912
+ onChange({
913
+ ...value,
914
+ pages: value.pages.map(
915
+ (p, pIndex) => pIndex === activePageIndex ? {
916
+ ...p,
917
+ elements: p.elements.map((el) => el.id === elementId ? { ...el, ...patch } : el)
918
+ } : p
919
+ )
920
+ });
921
+ };
922
+ const removeElement = (elementId) => {
923
+ onChange({
924
+ ...value,
925
+ pages: value.pages.map(
926
+ (p, pIndex) => pIndex === activePageIndex ? {
927
+ ...p,
928
+ elements: p.elements.filter((el) => el.id !== elementId)
929
+ } : p
930
+ )
931
+ });
932
+ };
933
+ const updatePage = (patch) => {
934
+ onChange({
935
+ ...value,
936
+ pages: value.pages.map((p, index) => index === activePageIndex ? { ...p, ...patch } : p)
937
+ });
938
+ };
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";
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(
941
+ "input",
942
+ {
943
+ type: "number",
944
+ min: 1,
945
+ max: 12,
946
+ value: value.pages.length,
947
+ onChange: (event) => handlePageCountChange(Number(event.target.value)),
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"
949
+ }
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(
951
+ "input",
952
+ {
953
+ type: "url",
954
+ value: value.backgroundMusic?.src || "",
955
+ onChange: (event) => onChange({
956
+ ...value,
957
+ backgroundMusic: {
958
+ ...value.backgroundMusic,
959
+ src: event.target.value
960
+ }
961
+ }),
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"
963
+ }
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(
965
+ "input",
966
+ {
967
+ type: "text",
968
+ value: page?.title || "",
969
+ onChange: (event) => updatePage({ title: event.target.value }),
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"
971
+ }
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(
973
+ "input",
974
+ {
975
+ type: "color",
976
+ value: page?.background?.color || "#0f172a",
977
+ onChange: (event) => updatePage({
978
+ background: {
979
+ ...page?.background,
980
+ color: event.target.value
981
+ }
982
+ }),
983
+ className: "h-10 w-full rounded-lg border border-slate-300 bg-white p-1"
984
+ }
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(
986
+ "input",
987
+ {
988
+ type: "url",
989
+ value: page?.background?.image || "",
990
+ onChange: (event) => updatePage({
991
+ background: {
992
+ ...page?.background,
993
+ image: event.target.value
994
+ }
995
+ }),
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"
997
+ }
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(
999
+ "select",
1000
+ {
1001
+ value: activePageIndex,
1002
+ onChange: (event) => setActivePageIndex(Number(event.target.value)),
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"
1004
+ },
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(
1007
+ "button",
1008
+ {
1009
+ type: "button",
1010
+ onClick: () => onChange({
1011
+ ...value,
1012
+ pages: value.pages.map(
1013
+ (p, index) => index === activePageIndex ? { ...p, elements: [...p.elements, createTextElement(index)] } : p
1014
+ )
1015
+ }),
1016
+ className: "rounded-lg bg-slate-900 px-3 py-2 text-sm font-medium text-white"
1017
+ },
1018
+ "+ \u6587\u5B57"
1019
+ ), /* @__PURE__ */ React4__default.default.createElement(
1020
+ "button",
1021
+ {
1022
+ type: "button",
1023
+ onClick: () => onChange({
1024
+ ...value,
1025
+ pages: value.pages.map(
1026
+ (p, index) => index === activePageIndex ? { ...p, elements: [...p.elements, createImageElement(index)] } : p
1027
+ )
1028
+ }),
1029
+ className: "rounded-lg bg-sky-600 px-3 py-2 text-sm font-medium text-white"
1030
+ },
1031
+ "+ \u56FE\u7247"
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(
1033
+ "div",
1034
+ {
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"}`
1037
+ },
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);
1200
+ };
1201
+
1202
+ // src/business/festivalCard/components/FestivalCardStudio.tsx
1203
+ var FestivalCardStudio = ({ initialConfig, fetchConfig, onSave }) => {
1204
+ const { config, setConfig, loading, save, saving } = useFestivalCardConfig({
1205
+ initialConfig: normalizeFestivalCardConfig(initialConfig),
1206
+ fetchConfig,
1207
+ onSave
1208
+ });
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(
1258
+ "button",
1259
+ {
1260
+ type: "button",
1261
+ onClick: () => void save(),
1262
+ disabled: saving,
1263
+ className: "mt-3 w-full rounded-lg bg-slate-900 px-4 py-2.5 text-sm font-medium text-white disabled:opacity-60"
1264
+ },
1265
+ saving ? "\u4FDD\u5B58\u4E2D..." : "\u4FDD\u5B58\u914D\u7F6E"
1266
+ ) : null));
1267
+ };
1268
+
1269
+ // src/business/festivalCard/components/FestivalCardConfigPage.tsx
1270
+ var FestivalCardConfigPage = ({
1271
+ apiBase = "/api/festivalCard",
1272
+ cardId,
1273
+ mainPagePath = "/festivalCard"
1274
+ }) => {
1275
+ const [list, setList] = React4.useState([]);
1276
+ const [selectedId, setSelectedId] = React4.useState(cardId || "default-festival-card");
1277
+ const parseListResponse2 = (data) => {
1278
+ if (!data || typeof data !== "object") return [];
1279
+ const payload = data.data;
1280
+ if (!Array.isArray(payload)) return [];
1281
+ return payload.filter((item) => Boolean(item && typeof item === "object" && typeof item.id === "string")).map((item) => ({ id: item.id, name: item.name }));
1282
+ };
1283
+ const parseConfigResponse2 = (data) => {
1284
+ if (!data || typeof data !== "object") return normalizeFestivalCardConfig();
1285
+ const payload = data.data;
1286
+ if (!payload || typeof payload !== "object") return normalizeFestivalCardConfig();
1287
+ return normalizeFestivalCardConfig(payload);
1288
+ };
1289
+ const reloadList = React4.useCallback(async () => {
1290
+ try {
1291
+ const response = await fetch(apiBase, { cache: "no-store" });
1292
+ if (!response.ok) throw new Error(`\u52A0\u8F7D\u5361\u7247\u5217\u8868\u5931\u8D25: ${response.status}`);
1293
+ const data = await response.json();
1294
+ const items = parseListResponse2(data);
1295
+ if (items.length === 0) {
1296
+ const fallbackId = "default-festival-card";
1297
+ const fallbackConfig = normalizeFestivalCardConfig({ id: fallbackId, name: "Holiday Card" });
1298
+ await fetch(`${apiBase}/${encodeURIComponent(fallbackId)}`, {
1299
+ method: "PUT",
1300
+ headers: { "Content-Type": "application/json" },
1301
+ body: JSON.stringify({ config: fallbackConfig })
1302
+ });
1303
+ setList([{ id: fallbackId, name: fallbackConfig.name }]);
1304
+ setSelectedId(fallbackId);
1305
+ return;
1306
+ }
1307
+ setList(items);
1308
+ } catch (error) {
1309
+ window.alert(error.message || "\u52A0\u8F7D\u5361\u7247\u5217\u8868\u5931\u8D25");
1310
+ }
1311
+ }, [apiBase]);
1312
+ React4.useEffect(() => {
1313
+ void reloadList();
1314
+ }, [reloadList]);
1315
+ const fetchConfig = async () => {
1316
+ const response = await fetch(`${apiBase}/${encodeURIComponent(selectedId)}`, { cache: "no-store" });
1317
+ if (!response.ok) {
1318
+ const message = `\u52A0\u8F7D\u914D\u7F6E\u5931\u8D25: ${response.status}`;
1319
+ window.alert(message);
1320
+ throw new Error(message);
1321
+ }
1322
+ const data = await response.json();
1323
+ return parseConfigResponse2(data);
1324
+ };
1325
+ const saveConfig = async (config) => {
1326
+ try {
1327
+ const response = await fetch(`${apiBase}/${encodeURIComponent(selectedId)}`, {
1328
+ method: "PUT",
1329
+ headers: { "Content-Type": "application/json" },
1330
+ body: JSON.stringify({ config })
1331
+ });
1332
+ if (!response.ok) throw new Error(`\u4FDD\u5B58\u5931\u8D25: ${response.status}`);
1333
+ await reloadList();
1334
+ window.alert("\u4FDD\u5B58\u6210\u529F");
1335
+ } catch (error) {
1336
+ window.alert(error.message || "\u4FDD\u5B58\u5931\u8D25");
1337
+ throw error;
1338
+ }
1339
+ };
1340
+ const createNew = async () => {
1341
+ const name = window.prompt("\u8BF7\u8F93\u5165\u65B0\u5361\u7247\u540D\u79F0");
1342
+ if (!name) return;
1343
+ const id = `festival-${Date.now()}`;
1344
+ const config = normalizeFestivalCardConfig({
1345
+ id,
1346
+ name
1347
+ });
1348
+ try {
1349
+ const response = await fetch(`${apiBase}/${encodeURIComponent(id)}`, {
1350
+ method: "PUT",
1351
+ headers: { "Content-Type": "application/json" },
1352
+ body: JSON.stringify({ config })
1353
+ });
1354
+ if (!response.ok) throw new Error(`\u521B\u5EFA\u5931\u8D25: ${response.status}`);
1355
+ setSelectedId(id);
1356
+ await reloadList();
1357
+ window.alert("\u65B0\u5361\u7247\u521B\u5EFA\u6210\u529F");
1358
+ } catch (error) {
1359
+ window.alert(error.message || "\u521B\u5EFA\u5931\u8D25");
1360
+ }
1361
+ };
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(
1364
+ "select",
1365
+ {
1366
+ value: selectedId,
1367
+ onChange: (event) => setSelectedId(event.target.value),
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"
1369
+ },
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 }));
1372
+ };
1373
+ var isSummary = (value) => {
1374
+ if (!value || typeof value !== "object") return false;
1375
+ return typeof value.id === "string";
1376
+ };
1377
+ var parseListResponse = (data) => {
1378
+ if (!data || typeof data !== "object") return [];
1379
+ const payload = data.data;
1380
+ if (!Array.isArray(payload)) return [];
1381
+ return payload.filter(isSummary).map((item) => ({ id: item.id, name: item.name }));
1382
+ };
1383
+ var parseConfigResponse = (data) => {
1384
+ if (!data || typeof data !== "object") return null;
1385
+ const payload = data.data;
1386
+ if (!payload || typeof payload !== "object") return null;
1387
+ if (!Array.isArray(payload.pages)) return null;
1388
+ return payload;
1389
+ };
1390
+ var FestivalCardManagedPage = ({
1391
+ apiBase = "/api/festivalCard",
1392
+ cardId
1393
+ }) => {
1394
+ const [currentCardId, setCurrentCardId] = React4.useState(cardId || "");
1395
+ const [config, setConfig] = React4.useState(null);
1396
+ const [loading, setLoading] = React4.useState(true);
1397
+ React4.useEffect(() => {
1398
+ const fetchList = async () => {
1399
+ const response = await fetch(apiBase, { cache: "no-store" });
1400
+ const data = await response.json();
1401
+ const items = parseListResponse(data);
1402
+ const first = items[0];
1403
+ if (!currentCardId && first) {
1404
+ setCurrentCardId(first.id);
1405
+ }
1406
+ };
1407
+ void fetchList();
1408
+ }, [apiBase, currentCardId]);
1409
+ React4.useEffect(() => {
1410
+ if (!currentCardId) return;
1411
+ setLoading(true);
1412
+ void fetch(`${apiBase}/${encodeURIComponent(currentCardId)}`, { cache: "no-store" }).then((res) => res.json()).then((data) => setConfig(parseConfigResponse(data))).finally(() => setLoading(false));
1413
+ }, [apiBase, currentCardId]);
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 }));
1415
+ };
1416
+
1417
+ // src/business/festivalCard/server/db.ts
1418
+ var dbAdapter = null;
1419
+ var getFestivalCardDb = () => dbAdapter;
1420
+
1421
+ // src/business/festivalCard/services/festivalCardService.ts
1422
+ var FestivalCardService = class {
1423
+ constructor(options) {
1424
+ this.db = options?.db || getFestivalCardDb();
1425
+ }
1426
+ async listConfigs() {
1427
+ if (!this.db) {
1428
+ return [{ id: DEFAULT_FESTIVAL_CARD_CONFIG.id || "default-festival-card", name: DEFAULT_FESTIVAL_CARD_CONFIG.name }];
1429
+ }
1430
+ if (this.db.listConfigs) {
1431
+ const list = await this.db.listConfigs();
1432
+ return list.length > 0 ? list : [{ id: DEFAULT_FESTIVAL_CARD_CONFIG.id || "default-festival-card", name: DEFAULT_FESTIVAL_CARD_CONFIG.name }];
1433
+ }
1434
+ return [{ id: DEFAULT_FESTIVAL_CARD_CONFIG.id || "default-festival-card", name: DEFAULT_FESTIVAL_CARD_CONFIG.name }];
1435
+ }
1436
+ async getConfig(cardId = "default-festival-card") {
1437
+ if (!this.db) return DEFAULT_FESTIVAL_CARD_CONFIG;
1438
+ const config = await this.db.getConfig(cardId);
1439
+ return normalizeFestivalCardConfig(config);
1440
+ }
1441
+ async saveConfig(cardId, config) {
1442
+ const normalized = normalizeFestivalCardConfig(config);
1443
+ if (!this.db) return normalized;
1444
+ await this.db.saveConfig(cardId, normalized);
1445
+ return normalized;
1446
+ }
1447
+ async deleteConfig(cardId) {
1448
+ if (!this.db?.deleteConfig) return;
1449
+ await this.db.deleteConfig(cardId);
1450
+ }
1451
+ };
1452
+ var memoryStore = /* @__PURE__ */ new Map();
1453
+ var defaultId = DEFAULT_FESTIVAL_CARD_CONFIG.id || "default-festival-card";
1454
+ if (!memoryStore.has(defaultId)) {
1455
+ memoryStore.set(defaultId, DEFAULT_FESTIVAL_CARD_CONFIG);
1456
+ }
1457
+ var createInMemoryFestivalCardDb = () => ({
1458
+ listConfigs() {
1459
+ return Promise.resolve(
1460
+ Array.from(memoryStore.entries()).map(([id, config]) => ({
1461
+ id,
1462
+ name: config.name || id
1463
+ }))
1464
+ );
1465
+ },
1466
+ getConfig(id) {
1467
+ return Promise.resolve(memoryStore.get(id) || null);
1468
+ },
1469
+ saveConfig(id, config) {
1470
+ memoryStore.set(id, config);
1471
+ return Promise.resolve();
1472
+ },
1473
+ deleteConfig(id) {
1474
+ memoryStore.delete(id);
1475
+ return Promise.resolve();
1476
+ }
1477
+ });
1478
+
1479
+ exports.DEFAULT_FESTIVAL_CARD_CONFIG = DEFAULT_FESTIVAL_CARD_CONFIG;
1480
+ exports.FestivalCardBook3D = FestivalCardBook3D;
1481
+ exports.FestivalCardConfigEditor = FestivalCardConfigEditor;
1482
+ exports.FestivalCardConfigPage = FestivalCardConfigPage;
1483
+ exports.FestivalCardManagedPage = FestivalCardManagedPage;
1484
+ exports.FestivalCardPageRenderer = FestivalCardPageRenderer;
1485
+ exports.FestivalCardService = FestivalCardService;
1486
+ exports.FestivalCardStudio = FestivalCardStudio;
1487
+ exports.createInMemoryFestivalCardDb = createInMemoryFestivalCardDb;
1488
+ exports.normalizeFestivalCardConfig = normalizeFestivalCardConfig;
1489
+ exports.resizeFestivalCardPages = resizeFestivalCardPages;
1490
+ exports.useFestivalCardConfig = useFestivalCardConfig;
1491
+ //# sourceMappingURL=index.js.map
1492
+ //# sourceMappingURL=index.js.map