Lashtw commited on
Commit
4919e24
·
verified ·
1 Parent(s): 6aec917

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +211 -99
index.html CHANGED
@@ -41,24 +41,12 @@
41
  transition: transform 0.3s ease-in-out;
42
  }
43
 
44
- /* 讓文字在編輯模式下也看起來很置中 */
45
  .cell-text {
46
  line-height: 1;
47
  display: block;
48
- padding-bottom: 0.1em; /* 視覺微調 */
49
  }
50
 
51
- /* 頁面切換特效 */
52
- .page-transition-enter-active,
53
- .page-transition-leave-active {
54
- transition: opacity 0.3s ease;
55
- }
56
- .page-transition-enter-from,
57
- .page-transition-leave-to {
58
- opacity: 0;
59
- }
60
-
61
- /* 預覽模式下的頁面縮放特效 */
62
  .preview-page {
63
  transform-origin: center center;
64
  cursor: pointer;
@@ -71,14 +59,12 @@
71
  z-index: 10;
72
  }
73
 
74
- /* PDF 生成時的隱藏容器 */
75
  #pdf-generator-container {
76
  position: absolute;
77
  top: -9999px;
78
  left: -9999px;
79
  }
80
 
81
- /* 隱藏滾動條但保留功能 (Chromium) */
82
  .no-scrollbar::-webkit-scrollbar {
83
  display: none;
84
  }
@@ -106,7 +92,7 @@
106
  魔法摺紙設計
107
  </h1>
108
 
109
- <!-- Page Navigator in Header -->
110
  <div class="mt-4 flex bg-slate-200 p-1 rounded-lg">
111
  <button
112
  @click="viewMode = 'overview'"
@@ -129,6 +115,35 @@
129
 
130
  <!-- Scrollable Content -->
131
  <div class="flex-1 overflow-y-auto p-6 space-y-8">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  <!-- Instruction -->
134
  <div class="bg-indigo-50 p-4 rounded-lg border border-indigo-100 text-sm text-indigo-800">
@@ -141,7 +156,7 @@
141
  </ul>
142
  </div>
143
 
144
- <!-- Editing Controls (Only visible in Edit Mode) -->
145
  <div v-if="viewMode === 'edit'" class="space-y-8 animate-fade-in">
146
  <!-- Text Input -->
147
  <div>
@@ -232,21 +247,26 @@
232
  ======================= -->
233
  <div class="flex-1 bg-slate-200 overflow-auto relative no-scrollbar">
234
 
235
- <!-- Workspace Centerer (Changed to flex-col to stack credits below) -->
236
  <div class="min-h-full flex flex-col items-center justify-center p-4 md:p-8 relative z-10">
237
 
238
- <!-- OVERVIEW MODE: Side by Side -->
239
  <div v-if="viewMode === 'overview'" class="flex flex-row flex-wrap gap-8 justify-center items-center">
240
  <div v-for="pageId in [1, 2]" :key="pageId" class="flex flex-col items-center">
241
  <h2 class="text-lg font-bold text-slate-600 mb-2">第 {{ pageId }} 頁</h2>
242
- <!-- Scaled Down Preview: Width ~120mm to fit side-by-side on laptop screens -->
243
  <div
244
  class="bg-white shadow-xl preview-page relative"
245
  style="width: 120mm; height: 170mm;"
246
  @click="switchToPage(pageId)"
247
  >
248
- <!-- Grid Component (Read Only in Overview) -->
249
- <div class="grid grid-cols-6 grid-rows-8 gap-0 border border-slate-200 w-full h-full pointer-events-none">
 
 
 
 
 
 
 
250
  <div
251
  v-for="(cell, index) in pages[pageId-1].cells"
252
  :key="cell.id"
@@ -262,25 +282,30 @@
262
  </div>
263
  </div>
264
 
265
- <!-- EDIT MODE: Show Single Active Page -->
266
  <div v-else class="flex flex-col gap-2 animate-fade-in">
267
- <!-- Back to Overview Button -->
268
  <button @click="viewMode = 'overview'" class="self-start text-slate-500 hover:text-indigo-600 font-bold text-sm flex items-center gap-1">
269
  ← 回到全覽
270
  </button>
271
 
272
- <!-- Active Page Canvas -->
273
  <div
274
  :id="`edit-canvas-${activePageId}`"
275
  class="bg-white shadow-2xl relative"
276
  style="width: 210mm; height: 297mm; padding: 0mm;"
277
  >
278
- <!-- A4 Watermark -->
279
  <div class="absolute bottom-1 right-2 text-slate-200 text-[10px] font-sans select-none z-0">
280
- Page {{ activePageId }} - Magic Origami
281
  </div>
282
 
283
- <div class="grid grid-cols-6 grid-rows-8 gap-0 border border-slate-200 w-full h-full">
 
 
 
 
 
 
 
 
284
  <div
285
  v-for="(cell, index) in activePageCells"
286
  :key="cell.id"
@@ -288,9 +313,11 @@
288
  class="grid-cell relative border border-slate-300 cursor-pointer hover:bg-slate-50 select-none"
289
  :class="{ 'ring-4 ring-indigo-500 ring-inset z-20': selectedCellIndex === index }"
290
  >
 
291
  <span class="absolute top-1 left-1 text-[8px] text-slate-200 pointer-events-none z-10">
292
- {{ Math.floor(index / 6) + 1 }}-{{ (index % 6) + 1 }}
293
  </span>
 
294
  <div class="rotation-wrapper pointer-events-none" :style="{ transform: `rotate(${cell.rotation}deg)` }">
295
  <span v-if="cell.type === 'text'" class="text-4xl md:text-5xl font-bold text-slate-800 cell-text block">
296
  {{ cell.content }}
@@ -302,8 +329,7 @@
302
  </div>
303
  </div>
304
 
305
- <!-- Credits / Watermark (Moved Here) -->
306
- <!-- Use 'mt-8' to add margin top, preventing overlap with the canvas -->
307
  <div class="mt-8 text-center w-full max-w-2xl">
308
  <div class="text-xs text-slate-400 font-sans space-y-1 bg-slate-100/50 p-4 rounded-lg inline-block text-left border border-slate-200">
309
  <p>靈感來源:台北市興雅國中 吳如皓老師 《摺紙中的數學魔術》</p>
@@ -318,46 +344,33 @@
318
  </div>
319
  </div>
320
 
321
- <!-- Hidden Container for PDF Rendering -->
322
- <div id="pdf-generator-container">
323
- <!-- This will be populated dynamically -->
324
- </div>
325
-
326
  </div>
327
 
328
  <script>
329
- const { createApp, ref, computed, nextTick } = Vue;
330
- const { jsPDF } = window.jspdf;
331
-
332
- createApp({
333
- setup() {
334
- // --- State ---
335
- // Two pages, each with 48 cells
336
- const createPageCells = (pageOffset) => Array.from({ length: 48 }, (_, i) => ({
337
- id: pageOffset + i,
338
- type: 'text',
339
- content: '',
340
- rotation: 0
341
- }));
342
-
343
- const pages = ref([
344
- { id: 1, cells: createPageCells(0) },
345
- { id: 2, cells: createPageCells(48) }
346
- ]);
347
-
348
- const viewMode = ref('overview'); // 'overview' | 'edit'
349
- const activePageId = ref(1);
350
- const selectedCellIndex = ref(null);
351
- const inputBuffer = ref('');
352
- const isGenerating = ref(false);
353
- const textInputRef = ref(null);
354
-
355
- // --- Computed ---
356
- const activePageCells = computed(() => pages.value[activePageId.value - 1].cells);
357
-
358
- // --- Icons ---
359
- const icons = {
360
- '愛心': 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z',
361
  '星星': 'M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z',
362
  '勝利': 'M16.5 13c-.55 0-1 .45-1 1v5h-1v-5c0-.55-.45-1-1-1s-1 .45-1 1v5h-1v-5c0-.55-.45-1-1-1s-1 .45-1 1v2.5c0 .55-.45 1-1 1s-1-.45-1-1V5.63c0-1-.7-1.63-1.6-1.63-.88 0-1.6.82-1.6 1.73V13h-1V5.73C5.3 3.66 7.03 2 9.1 2c1.77 0 3.32 1.22 3.8 2.87.66-1.07 1.8-1.87 3.1-1.87 2.21 0 4 1.79 4 4v6c0 .55-.45 1-1 1s-1-.45-1-1v-1h-1.5v1z M21 16c0 2.97-2.16 5.43-5 5.91V22h-8v-2.09c-2.84-.48-5-2.94-5-5.91h2c0 2.76 2.24 5 5 5s5-2.24 5-5h2z',
363
  '獎盃': 'M20.2 6.5C19.7 3.9 17.5 2 15 2H9C6.5 2 4.3 3.9 3.8 6.5L3 11c-.5 2.5 1.2 5 3.8 5.8V17c0 2.2 1.8 4 4 4h2.4c2.2 0 4-1.8 4-4v-.2c2.6-.8 4.3-3.3 3.8-5.8l-.8-4.5zM6.5 10.6l.6-3.8C7.4 4.8 9.1 4 9 4h6c-.1 0 1.6.8 1.9 2.8l.6 3.8c.2 1.3-.8 2.4-2.1 2.4h-9c-1.3 0-2.3-1.1-2.1-2.4z M17 17c0 1.1-.9 2-2 2H9c-1.1 0-2-.9-2-2v-1h10v1z M15 22H9v1h6v-1z',
@@ -375,12 +388,125 @@
375
  '禮物': 'M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.54-2.5 1.35l-.5.67-.5-.68C10.96 2.54 10.05 2 9 2 7.34 2 6 3.34 6 5c0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM9 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm11 15H4v-2h16v2zm0-5H4V8h5.08L7 10.83 8.62 12 11 8.76 13.38 12 15 10.83 12.92 8H20v6z'
376
  };
377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  // --- Actions ---
379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  const switchToPage = (pageId) => {
381
  activePageId.value = pageId;
382
  viewMode.value = 'edit';
383
- selectedCellIndex.value = null; // Reset selection on page switch
384
  inputBuffer.value = '';
385
  };
386
 
@@ -425,37 +551,31 @@
425
  inputBuffer.value = '';
426
  };
427
 
428
- // --- Helper: Render a single page to canvas ---
429
  const renderPageToCanvas = async (pageId) => {
430
  const pageData = pages.value[pageId - 1];
431
-
432
- // Create a temporary DOM element for rendering this specific page off-screen
433
- // We construct the HTML manually to match the structure, but ensuring it's "clean" for export
434
  const container = document.getElementById('pdf-generator-container');
435
- container.innerHTML = ''; // Clear previous
436
 
437
  const wrapper = document.createElement('div');
438
  wrapper.style.width = '210mm';
439
  wrapper.style.height = '297mm';
440
  wrapper.style.backgroundColor = 'white';
441
  wrapper.style.position = 'relative';
442
- // We don't use grid gap to avoid border doubling issues
443
 
444
- // Build the grid HTML string
445
- let gridHtml = `<div style="display: grid; grid-template-columns: repeat(6, 1fr); grid-template-rows: repeat(8, 1fr); width: 100%; height: 100%; border: 1px solid #e2e8f0;">`;
446
 
447
  pageData.cells.forEach((cell, idx) => {
448
- // Icon Logic
449
  let contentHtml = '';
450
  if (cell.type === 'text') {
451
- // Important: Use a span for text
452
  contentHtml = `<span class="export-text" style="font-size: 40px; font-weight: bold; color: #1e293b; font-family: 'Noto Sans TC', sans-serif;">${cell.content}</span>`;
453
  } else if (cell.type === 'icon') {
454
  contentHtml = `<svg viewBox="0 0 24 24" style="width: 60%; height: 60%; fill: #1e293b;"><path d="${icons[cell.content]}"></path></svg>`;
455
  }
456
 
457
- // Grid Coordinate
458
- const coord = `${Math.floor(idx / 6) + 1}-${(idx % 6) + 1}`;
459
 
460
  gridHtml += `
461
  <div class="grid-cell" style="position: relative; border: 1px solid #cbd5e1; display: flex; align-items: center; justify-content: center; overflow: hidden;">
@@ -468,27 +588,22 @@
468
  });
469
  gridHtml += `</div>`;
470
 
471
- // Add watermark
472
  gridHtml += `<div style="position: absolute; bottom: 5px; right: 10px; color: #e2e8f0; font-size: 10px; font-family: sans-serif;">Page ${pageId} - Magic Origami</div>`;
473
 
474
  wrapper.innerHTML = gridHtml;
475
  container.appendChild(wrapper);
476
 
477
- // Generate Canvas
478
  const canvas = await html2canvas(wrapper, {
479
- scale: 3, // Lower scale slightly for performance with multiple pages, 3 is still high quality
480
  useCORS: true,
481
  backgroundColor: '#ffffff',
482
  onclone: (clonedDoc) => {
483
- // === The SVG Text Replacement Strategy ===
484
- // Find all text elements in the CLONED document
485
  const textElements = clonedDoc.querySelectorAll('.export-text');
486
 
487
  textElements.forEach(el => {
488
  const textContent = el.innerText;
489
  if (!textContent) return;
490
 
491
- // 1. Create an SVG element to replace the text span
492
  const ns = "http://www.w3.org/2000/svg";
493
  const svg = document.createElementNS(ns, "svg");
494
  svg.setAttribute("width", "100%");
@@ -498,27 +613,23 @@
498
  svg.style.top = "0";
499
  svg.style.left = "0";
500
 
501
- // 2. Create the Text node inside SVG
502
  const textNode = document.createElementNS(ns, "text");
503
  textNode.setAttribute("x", "50%");
504
  textNode.setAttribute("y", "50%");
505
- textNode.setAttribute("dominant-baseline", "central"); // The magic attribute for vertical centering
506
- textNode.setAttribute("text-anchor", "middle"); // Horizontal centering
507
  textNode.setAttribute("fill", "#1e293b");
508
  textNode.setAttribute("font-family", "'Noto Sans TC', sans-serif");
509
  textNode.setAttribute("font-weight", "bold");
510
- // Adjust font size relative to the viewBox (100x100).
511
- // Since the cell is roughly square, 40-50 is a good percentage.
512
  textNode.setAttribute("font-size", "45");
513
  textNode.textContent = textContent;
514
 
515
  svg.appendChild(textNode);
516
 
517
- // 3. Replace the original HTML span with this SVG
518
  const parent = el.parentNode;
519
- parent.style.position = "relative"; // Ensure parent can hold absolute SVG
520
- parent.innerHTML = ''; // Clear the HTML text
521
- parent.appendChild(svg); // Inject the SVG
522
  });
523
  }
524
  });
@@ -535,12 +646,10 @@
535
  const pdfWidth = 210;
536
  const pdfHeight = 297;
537
 
538
- // Render Page 1
539
  const canvas1 = await renderPageToCanvas(1);
540
  const imgData1 = canvas1.toDataURL('image/jpeg', 0.95);
541
  pdf.addImage(imgData1, 'JPEG', 0, 0, pdfWidth, pdfHeight);
542
 
543
- // Render Page 2
544
  pdf.addPage();
545
  const canvas2 = await renderPageToCanvas(2);
546
  const imgData2 = canvas2.toDataURL('image/jpeg', 0.95);
@@ -553,12 +662,15 @@
553
  alert("PDF 生成發生錯誤");
554
  } finally {
555
  isGenerating.value = false;
556
- // Clean up temp container
557
  document.getElementById('pdf-generator-container').innerHTML = '';
558
  }
559
  };
560
 
561
  return {
 
 
 
 
562
  pages,
563
  viewMode,
564
  activePageId,
 
41
  transition: transform 0.3s ease-in-out;
42
  }
43
 
 
44
  .cell-text {
45
  line-height: 1;
46
  display: block;
47
+ padding-bottom: 0.1em;
48
  }
49
 
 
 
 
 
 
 
 
 
 
 
 
50
  .preview-page {
51
  transform-origin: center center;
52
  cursor: pointer;
 
59
  z-index: 10;
60
  }
61
 
 
62
  #pdf-generator-container {
63
  position: absolute;
64
  top: -9999px;
65
  left: -9999px;
66
  }
67
 
 
68
  .no-scrollbar::-webkit-scrollbar {
69
  display: none;
70
  }
 
92
  魔法摺紙設計
93
  </h1>
94
 
95
+ <!-- Page Navigator -->
96
  <div class="mt-4 flex bg-slate-200 p-1 rounded-lg">
97
  <button
98
  @click="viewMode = 'overview'"
 
115
 
116
  <!-- Scrollable Content -->
117
  <div class="flex-1 overflow-y-auto p-6 space-y-8">
118
+
119
+ <!-- Grid Settings -->
120
+ <div>
121
+ <label class="block text-sm font-bold text-slate-700 mb-2">📐 網格設定 (點擊切換)</label>
122
+ <div class="grid grid-cols-3 gap-2">
123
+ <button
124
+ v-for="conf in gridOptions"
125
+ :key="conf.label"
126
+ @click="changeGridSize(conf)"
127
+ class="py-2 text-sm border rounded-lg transition-all active:scale-95"
128
+ :class="currentGrid.rows === conf.rows && currentGrid.cols === conf.cols ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-slate-600 border-slate-300 hover:bg-slate-50'"
129
+ >
130
+ {{ conf.label }}
131
+ </button>
132
+ </div>
133
+ </div>
134
+
135
+ <!-- Template -->
136
+ <div>
137
+ <label class="block text-sm font-bold text-slate-700 mb-2">🎁 快速模板</label>
138
+ <button
139
+ @click="applyTemplate('lucky')"
140
+ class="w-full py-3 bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 text-white font-bold rounded-lg shadow-md hover:shadow-lg active:scale-95 transition-all flex items-center justify-center gap-2"
141
+ >
142
+ <svg viewBox="0 0 24 24" class="w-5 h-5 fill-current"><path d="M12,2C9,2,6,4,6,7c0-2,3-5,3-5S6,0,9,0c4,0,5,5,2,8c3-3,8-2,8,2s-5,5-8,2c2,2,2,7,0,7c0-3-3-5-3-5s-1,4-4,4c0-4,5-5,2-8C6,13,1,12,1,8C1,8,6,7,9,10C10,11,11,11,12,10C13,9,12,2,12,2z"></path></svg>
143
+ 套用:幸運遇見你 (如皓老師版)
144
+ </button>
145
+ <p class="text-xs text-slate-400 mt-2">提示:套用後會自動切換為 6x8 網格並填入內容。</p>
146
+ </div>
147
 
148
  <!-- Instruction -->
149
  <div class="bg-indigo-50 p-4 rounded-lg border border-indigo-100 text-sm text-indigo-800">
 
156
  </ul>
157
  </div>
158
 
159
+ <!-- Editing Controls -->
160
  <div v-if="viewMode === 'edit'" class="space-y-8 animate-fade-in">
161
  <!-- Text Input -->
162
  <div>
 
247
  ======================= -->
248
  <div class="flex-1 bg-slate-200 overflow-auto relative no-scrollbar">
249
 
 
250
  <div class="min-h-full flex flex-col items-center justify-center p-4 md:p-8 relative z-10">
251
 
252
+ <!-- OVERVIEW MODE -->
253
  <div v-if="viewMode === 'overview'" class="flex flex-row flex-wrap gap-8 justify-center items-center">
254
  <div v-for="pageId in [1, 2]" :key="pageId" class="flex flex-col items-center">
255
  <h2 class="text-lg font-bold text-slate-600 mb-2">第 {{ pageId }} 頁</h2>
 
256
  <div
257
  class="bg-white shadow-xl preview-page relative"
258
  style="width: 120mm; height: 170mm;"
259
  @click="switchToPage(pageId)"
260
  >
261
+ <!-- Added :key to force re-render when grid changes -->
262
+ <div
263
+ class="grid gap-0 border border-slate-200 w-full h-full pointer-events-none"
264
+ :key="currentGrid.label"
265
+ :style="{
266
+ gridTemplateColumns: `repeat(${currentGrid.cols}, 1fr)`,
267
+ gridTemplateRows: `repeat(${currentGrid.rows}, 1fr)`
268
+ }"
269
+ >
270
  <div
271
  v-for="(cell, index) in pages[pageId-1].cells"
272
  :key="cell.id"
 
282
  </div>
283
  </div>
284
 
285
+ <!-- EDIT MODE -->
286
  <div v-else class="flex flex-col gap-2 animate-fade-in">
 
287
  <button @click="viewMode = 'overview'" class="self-start text-slate-500 hover:text-indigo-600 font-bold text-sm flex items-center gap-1">
288
  ← 回到全覽
289
  </button>
290
 
 
291
  <div
292
  :id="`edit-canvas-${activePageId}`"
293
  class="bg-white shadow-2xl relative"
294
  style="width: 210mm; height: 297mm; padding: 0mm;"
295
  >
 
296
  <div class="absolute bottom-1 right-2 text-slate-200 text-[10px] font-sans select-none z-0">
297
+ Page {{ activePageId }} - {{ currentGrid.rows }}x{{ currentGrid.cols }}
298
  </div>
299
 
300
+ <!-- Added :key here too -->
301
+ <div
302
+ class="grid gap-0 border border-slate-200 w-full h-full"
303
+ :key="currentGrid.label + activePageId"
304
+ :style="{
305
+ gridTemplateColumns: `repeat(${currentGrid.cols}, 1fr)`,
306
+ gridTemplateRows: `repeat(${currentGrid.rows}, 1fr)`
307
+ }"
308
+ >
309
  <div
310
  v-for="(cell, index) in activePageCells"
311
  :key="cell.id"
 
313
  class="grid-cell relative border border-slate-300 cursor-pointer hover:bg-slate-50 select-none"
314
  :class="{ 'ring-4 ring-indigo-500 ring-inset z-20': selectedCellIndex === index }"
315
  >
316
+ <!-- Grid Coordinate -->
317
  <span class="absolute top-1 left-1 text-[8px] text-slate-200 pointer-events-none z-10">
318
+ {{ Math.floor(index / currentGrid.cols) + 1 }}-{{ (index % currentGrid.cols) + 1 }}
319
  </span>
320
+
321
  <div class="rotation-wrapper pointer-events-none" :style="{ transform: `rotate(${cell.rotation}deg)` }">
322
  <span v-if="cell.type === 'text'" class="text-4xl md:text-5xl font-bold text-slate-800 cell-text block">
323
  {{ cell.content }}
 
329
  </div>
330
  </div>
331
 
332
+ <!-- Credits -->
 
333
  <div class="mt-8 text-center w-full max-w-2xl">
334
  <div class="text-xs text-slate-400 font-sans space-y-1 bg-slate-100/50 p-4 rounded-lg inline-block text-left border border-slate-200">
335
  <p>靈感來源:台北市興雅國中 吳如皓老師 《摺紙中的數學魔術》</p>
 
344
  </div>
345
  </div>
346
 
347
+ <div id="pdf-generator-container"></div>
 
 
 
 
348
  </div>
349
 
350
  <script>
351
+ const { createApp, ref, computed, nextTick } = Vue;
352
+ const { jsPDF } = window.jspdf;
353
+
354
+ createApp({
355
+ setup() {
356
+ // --- Configuration ---
357
+ const gridOptions = [
358
+ { label: '4x4', rows: 4, cols: 4 },
359
+ { label: '6x8', rows: 8, cols: 6 },
360
+ { label: '8x8', rows: 8, cols: 8 }
361
+ ];
362
+ const currentGrid = ref(gridOptions[1]); // Default 6x8
363
+
364
+ // --- Icons (Updated Clover) ---
365
+ const icons = {
366
+ // 修正後的幸運草:四個愛心尖端朝內 (12,12)
367
+ '幸運草': `
368
+ M12,12 C9,8 5,5 5,3 C5,1 7,0 9,0 C10.5,0 11.5,1 12,2 C12.5,1 13.5,0 15,0 C17,0 19,1 19,3 C19,5 15,8 12,12 z
369
+ M12,12 C16,9 19,5 21,5 C23,5 24,7 24,9 C24,10.5 23,11.5 22,12 C23,12.5 24,13.5 24,15 C24,17 23,19 21,19 C19,19 16,15 12,12 z
370
+ M12,12 C15,16 19,19 19,21 C19,23 17,24 15,24 C13.5,24 12.5,23 12,22 C11.5,23 10.5,24 9,24 C7,24 5,23 5,21 C5,19 9,16 12,12 z
371
+ M12,12 C8,15 5,19 3,19 C1,19 0,17 0,15 C0,13.5 1,12.5 2,12 C1,11.5 0,10.5 0,9 C0,7 1,5 3,5 C5,5 8,9 12,12 z
372
+ `,
373
+ '愛心': 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z',
 
 
 
 
 
 
 
 
 
374
  '星星': 'M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z',
375
  '勝利': 'M16.5 13c-.55 0-1 .45-1 1v5h-1v-5c0-.55-.45-1-1-1s-1 .45-1 1v5h-1v-5c0-.55-.45-1-1-1s-1 .45-1 1v2.5c0 .55-.45 1-1 1s-1-.45-1-1V5.63c0-1-.7-1.63-1.6-1.63-.88 0-1.6.82-1.6 1.73V13h-1V5.73C5.3 3.66 7.03 2 9.1 2c1.77 0 3.32 1.22 3.8 2.87.66-1.07 1.8-1.87 3.1-1.87 2.21 0 4 1.79 4 4v6c0 .55-.45 1-1 1s-1-.45-1-1v-1h-1.5v1z M21 16c0 2.97-2.16 5.43-5 5.91V22h-8v-2.09c-2.84-.48-5-2.94-5-5.91h2c0 2.76 2.24 5 5 5s5-2.24 5-5h2z',
376
  '獎盃': 'M20.2 6.5C19.7 3.9 17.5 2 15 2H9C6.5 2 4.3 3.9 3.8 6.5L3 11c-.5 2.5 1.2 5 3.8 5.8V17c0 2.2 1.8 4 4 4h2.4c2.2 0 4-1.8 4-4v-.2c2.6-.8 4.3-3.3 3.8-5.8l-.8-4.5zM6.5 10.6l.6-3.8C7.4 4.8 9.1 4 9 4h6c-.1 0 1.6.8 1.9 2.8l.6 3.8c.2 1.3-.8 2.4-2.1 2.4h-9c-1.3 0-2.3-1.1-2.1-2.4z M17 17c0 1.1-.9 2-2 2H9c-1.1 0-2-.9-2-2v-1h10v1z M15 22H9v1h6v-1z',
 
388
  '禮物': 'M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.54-2.5 1.35l-.5.67-.5-.68C10.96 2.54 10.05 2 9 2 7.34 2 6 3.34 6 5c0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM9 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm11 15H4v-2h16v2zm0-5H4V8h5.08L7 10.83 8.62 12 11 8.76 13.38 12 15 10.83 12.92 8H20v6z'
389
  };
390
 
391
+ // --- State Initialization ---
392
+ // Helper to create empty cells
393
+ const createPageCells = (pageOffset, rows, cols) => Array.from({ length: rows * cols }, (_, i) => ({
394
+ id: pageOffset + i,
395
+ type: 'text',
396
+ content: '',
397
+ rotation: 0
398
+ }));
399
+
400
+ const pages = ref([
401
+ { id: 1, cells: createPageCells(0, 8, 6) },
402
+ { id: 2, cells: createPageCells(48, 8, 6) }
403
+ ]);
404
+
405
+ const viewMode = ref('overview');
406
+ const activePageId = ref(1);
407
+ const selectedCellIndex = ref(null);
408
+ const inputBuffer = ref('');
409
+ const isGenerating = ref(false);
410
+ const textInputRef = ref(null);
411
+
412
+ const activePageCells = computed(() => pages.value[activePageId.value - 1].cells);
413
+
414
  // --- Actions ---
415
 
416
+ const changeGridSize = (conf) => {
417
+ // 移除了 confirm() 對話框,直接執行切換
418
+ currentGrid.value = conf;
419
+ const totalCells = conf.rows * conf.cols;
420
+
421
+ // Recreate pages with new dimensions
422
+ pages.value = [
423
+ { id: 1, cells: createPageCells(0, conf.rows, conf.cols) },
424
+ { id: 2, cells: createPageCells(totalCells, conf.rows, conf.cols) }
425
+ ];
426
+
427
+ // Reset view state
428
+ selectedCellIndex.value = null;
429
+ activePageId.value = 1;
430
+ viewMode.value = 'overview';
431
+ };
432
+
433
+ // Helper to set cell content easily using 1-based Row/Col
434
+ // Row: 1-8, Col: 1-6
435
+ const setCell = (pageIndex, row, col, type, content, rotation = 0) => {
436
+ // Bounds check
437
+ if (row > currentGrid.value.rows || col > currentGrid.value.cols) return;
438
+
439
+ const cells = pages.value[pageIndex].cells;
440
+ const index = (row - 1) * currentGrid.value.cols + (col - 1);
441
+
442
+ if (cells[index]) {
443
+ cells[index].type = type;
444
+ cells[index].content = content;
445
+ cells[index].rotation = rotation;
446
+ }
447
+ };
448
+
449
+ const applyTemplate = (templateId) => {
450
+ // 移除了 confirm() 對話框,直接套用模板
451
+ try {
452
+ // 1. Force switch to 6x8 grid
453
+ const targetConf = gridOptions[1]; // 6x8
454
+ currentGrid.value = targetConf;
455
+ const totalCells = 48;
456
+
457
+ // Reset Pages
458
+ pages.value = [
459
+ { id: 1, cells: createPageCells(0, 8, 6) },
460
+ { id: 2, cells: createPageCells(48, 8, 6) }
461
+ ];
462
+
463
+ if (templateId === 'lucky') {
464
+ // Page 1
465
+ setCell(0, 1, 3, 'text', '最', 180);
466
+ setCell(0, 1, 4, 'text', '是', 180);
467
+ setCell(0, 2, 3, 'icon', '幸運草', 0);
468
+ setCell(0, 2, 4, 'icon', '幸運草', 0);
469
+ setCell(0, 3, 3, 'icon', '幸運草', 0);
470
+ setCell(0, 3, 4, 'icon', '幸運草', 0);
471
+ setCell(0, 3, 5, 'text', '遇', 0);
472
+ setCell(0, 3, 6, 'text', '幸', 0);
473
+ setCell(0, 6, 3, 'icon', '幸運草', 0);
474
+ setCell(0, 6, 4, 'icon', '幸運草', 0);
475
+ setCell(0, 6, 5, 'text', '見', 0);
476
+ setCell(0, 6, 6, 'text', '運', 0);
477
+ setCell(0, 7, 3, 'icon', '幸運草', 0);
478
+ setCell(0, 7, 4, 'icon', '幸運草', 0);
479
+ setCell(0, 8, 3, 'text', '就', 180);
480
+ setCell(0, 8, 4, 'text', '你', 180);
481
+
482
+ // Page 2
483
+ setCell(1, 1, 6, 'text', '茫', 180);
484
+ setCell(1, 2, 6, 'icon', '幸運草', 0);
485
+ setCell(1, 5, 5, 'text', '茫', 0);
486
+ setCell(1, 5, 6, 'icon', '幸運草', 0);
487
+ setCell(1, 6, 5, 'text', '人', 0);
488
+ setCell(1, 6, 6, 'icon', '幸運草', 0);
489
+ setCell(1, 7, 6, 'icon', '幸運草', 0);
490
+ setCell(1, 8, 6, 'text', '海', 180);
491
+ }
492
+
493
+ // Reset View
494
+ selectedCellIndex.value = null;
495
+ activePageId.value = 1;
496
+ viewMode.value = 'overview';
497
+
498
+ alert("模板套用成功!");
499
+
500
+ } catch (e) {
501
+ console.error(e);
502
+ alert("套用模板時發生錯誤,請重試。");
503
+ }
504
+ };
505
+
506
  const switchToPage = (pageId) => {
507
  activePageId.value = pageId;
508
  viewMode.value = 'edit';
509
+ selectedCellIndex.value = null;
510
  inputBuffer.value = '';
511
  };
512
 
 
551
  inputBuffer.value = '';
552
  };
553
 
 
554
  const renderPageToCanvas = async (pageId) => {
555
  const pageData = pages.value[pageId - 1];
556
+ const rows = currentGrid.value.rows;
557
+ const cols = currentGrid.value.cols;
558
+
559
  const container = document.getElementById('pdf-generator-container');
560
+ container.innerHTML = '';
561
 
562
  const wrapper = document.createElement('div');
563
  wrapper.style.width = '210mm';
564
  wrapper.style.height = '297mm';
565
  wrapper.style.backgroundColor = 'white';
566
  wrapper.style.position = 'relative';
 
567
 
568
+ let gridHtml = `<div style="display: grid; grid-template-columns: repeat(${cols}, 1fr); grid-template-rows: repeat(${rows}, 1fr); width: 100%; height: 100%; border: 1px solid #e2e8f0;">`;
 
569
 
570
  pageData.cells.forEach((cell, idx) => {
 
571
  let contentHtml = '';
572
  if (cell.type === 'text') {
 
573
  contentHtml = `<span class="export-text" style="font-size: 40px; font-weight: bold; color: #1e293b; font-family: 'Noto Sans TC', sans-serif;">${cell.content}</span>`;
574
  } else if (cell.type === 'icon') {
575
  contentHtml = `<svg viewBox="0 0 24 24" style="width: 60%; height: 60%; fill: #1e293b;"><path d="${icons[cell.content]}"></path></svg>`;
576
  }
577
 
578
+ const coord = `${Math.floor(idx / cols) + 1}-${(idx % cols) + 1}`;
 
579
 
580
  gridHtml += `
581
  <div class="grid-cell" style="position: relative; border: 1px solid #cbd5e1; display: flex; align-items: center; justify-content: center; overflow: hidden;">
 
588
  });
589
  gridHtml += `</div>`;
590
 
 
591
  gridHtml += `<div style="position: absolute; bottom: 5px; right: 10px; color: #e2e8f0; font-size: 10px; font-family: sans-serif;">Page ${pageId} - Magic Origami</div>`;
592
 
593
  wrapper.innerHTML = gridHtml;
594
  container.appendChild(wrapper);
595
 
 
596
  const canvas = await html2canvas(wrapper, {
597
+ scale: 3,
598
  useCORS: true,
599
  backgroundColor: '#ffffff',
600
  onclone: (clonedDoc) => {
 
 
601
  const textElements = clonedDoc.querySelectorAll('.export-text');
602
 
603
  textElements.forEach(el => {
604
  const textContent = el.innerText;
605
  if (!textContent) return;
606
 
 
607
  const ns = "http://www.w3.org/2000/svg";
608
  const svg = document.createElementNS(ns, "svg");
609
  svg.setAttribute("width", "100%");
 
613
  svg.style.top = "0";
614
  svg.style.left = "0";
615
 
 
616
  const textNode = document.createElementNS(ns, "text");
617
  textNode.setAttribute("x", "50%");
618
  textNode.setAttribute("y", "50%");
619
+ textNode.setAttribute("dominant-baseline", "central");
620
+ textNode.setAttribute("text-anchor", "middle");
621
  textNode.setAttribute("fill", "#1e293b");
622
  textNode.setAttribute("font-family", "'Noto Sans TC', sans-serif");
623
  textNode.setAttribute("font-weight", "bold");
 
 
624
  textNode.setAttribute("font-size", "45");
625
  textNode.textContent = textContent;
626
 
627
  svg.appendChild(textNode);
628
 
 
629
  const parent = el.parentNode;
630
+ parent.style.position = "relative";
631
+ parent.innerHTML = '';
632
+ parent.appendChild(svg);
633
  });
634
  }
635
  });
 
646
  const pdfWidth = 210;
647
  const pdfHeight = 297;
648
 
 
649
  const canvas1 = await renderPageToCanvas(1);
650
  const imgData1 = canvas1.toDataURL('image/jpeg', 0.95);
651
  pdf.addImage(imgData1, 'JPEG', 0, 0, pdfWidth, pdfHeight);
652
 
 
653
  pdf.addPage();
654
  const canvas2 = await renderPageToCanvas(2);
655
  const imgData2 = canvas2.toDataURL('image/jpeg', 0.95);
 
662
  alert("PDF 生成發生錯誤");
663
  } finally {
664
  isGenerating.value = false;
 
665
  document.getElementById('pdf-generator-container').innerHTML = '';
666
  }
667
  };
668
 
669
  return {
670
+ gridOptions,
671
+ currentGrid,
672
+ changeGridSize,
673
+ applyTemplate,
674
  pages,
675
  viewMode,
676
  activePageId,