Lashtw commited on
Commit
8ebc1d1
·
verified ·
1 Parent(s): 8ee2116

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +216 -116
index.html CHANGED
@@ -19,6 +19,9 @@
19
  <link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css" rel="stylesheet">
20
  <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
21
 
 
 
 
22
  <!-- Google Fonts -->
23
  <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700&display=swap" rel="stylesheet">
24
 
@@ -97,6 +100,12 @@
97
  width: 100%;
98
  height: 100%;
99
  }
 
 
 
 
 
 
100
  </style>
101
  </head>
102
  <body class="h-screen overflow-hidden text-slate-800">
@@ -110,12 +119,12 @@
110
 
111
  <!-- Header -->
112
  <div class="p-6 border-b border-slate-100 bg-slate-50">
113
- <h1 class="text-2xl font-bold text-indigo-600 flex items-center gap-2">
114
- <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
115
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
116
- </svg>
117
- 魔法摺紙設計
118
- </h1>
119
 
120
  <!-- Page Navigator -->
121
  <div class="mt-4 flex bg-slate-200 p-1 rounded-lg">
@@ -160,9 +169,7 @@
160
  @click="clearAllContent"
161
  class="w-full py-2 text-sm text-red-600 border border-red-200 bg-red-50 hover:bg-red-100 rounded-lg transition-all flex items-center justify-center gap-1"
162
  >
163
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
164
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
165
- </svg>
166
  一鍵清空所有內容
167
  </button>
168
  </div>
@@ -174,9 +181,8 @@
174
  @click="applyTemplate('lucky')"
175
  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"
176
  >
177
- <!-- Updated Clover to Emoji -->
178
- <span class="text-xl leading-none">🍀</span>
179
- 套用:幸運遇見你(如皓老師分享)
180
  </button>
181
  <p class="text-xs text-slate-400 mt-2">提示:套用後會自動切換為 6x8 網格並填入內容。</p>
182
  </div>
@@ -189,18 +195,14 @@
189
  @click="exportProject"
190
  class="py-2 bg-slate-600 hover:bg-slate-700 text-white font-bold rounded-lg shadow-sm active:scale-95 transition-all flex items-center justify-center gap-1 text-sm"
191
  >
192
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
193
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
194
- </svg>
195
  匯出存檔
196
  </button>
197
  <button
198
  @click="triggerImport"
199
  class="py-2 bg-slate-600 hover:bg-slate-700 text-white font-bold rounded-lg shadow-sm active:scale-95 transition-all flex items-center justify-center gap-1 text-sm"
200
  >
201
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
202
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m-4 4v12" />
203
- </svg>
204
  匯入舊檔
205
  </button>
206
  <input
@@ -226,9 +228,26 @@
226
 
227
  <!-- Editing Controls -->
228
  <div v-if="viewMode === 'edit'" class="space-y-8 animate-fade-in pb-10">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  <!-- Text Input -->
230
  <div>
231
- <label class="block text-sm font-bold text-slate-700 mb-2">1. 文字輸入</label>
232
  <input
233
  ref="textInputRef"
234
  type="text"
@@ -239,15 +258,16 @@
239
  :disabled="selectedCellIndex === null"
240
  class="w-full text-center text-2xl p-3 border-2 rounded-lg focus:outline-none focus:ring-2 transition-all"
241
  :class="selectedCellIndex === null ? 'bg-slate-100 border-slate-200 cursor-not-allowed' : 'bg-white border-indigo-300 focus:border-indigo-500 focus:ring-indigo-200'"
 
242
  >
243
  </div>
244
 
245
  <!-- Icon Picker -->
246
  <div>
247
- <label class="block text-sm font-bold text-slate-700 mb-3">2. 選擇圖示</label>
248
  <div class="grid grid-cols-4 gap-2 max-h-60 overflow-y-auto p-1 custom-scrollbar">
249
  <button
250
- v-for="(iconVal, name) in icons"
251
  :key="name"
252
  @click="applyIconToCell(name)"
253
  :disabled="selectedCellIndex === null"
@@ -255,7 +275,7 @@
255
  :class="selectedCellIndex === null ? 'border-slate-200 text-slate-300 cursor-not-allowed' : 'border-slate-300 text-slate-600 hover:border-indigo-400 hover:text-indigo-600 cursor-pointer'"
256
  :title="name"
257
  >
258
- <span class="text-3xl leading-none select-none">{{ iconVal }}</span>
259
  </button>
260
  </div>
261
  </div>
@@ -263,14 +283,12 @@
263
  <!-- Image Upload -->
264
  <div>
265
  <div class="flex justify-between items-center mb-3">
266
- <label class="block text-sm font-bold text-slate-700">3. 自定義圖片</label>
267
  <button
268
  @click="triggerImageUpload"
269
  class="text-xs bg-indigo-100 text-indigo-700 px-2 py-1 rounded hover:bg-indigo-200 font-bold flex items-center gap-1"
270
  >
271
- <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
272
- <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
273
- </svg>
274
  上傳圖片
275
  </button>
276
  <input type="file" ref="imageUploadInput" class="hidden" accept="image/*" @change="handleImageUpload">
@@ -331,12 +349,11 @@
331
  class="w-full py-4 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-xl shadow-lg hover:shadow-xl active:scale-95 transition-all flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-wait"
332
  >
333
  <span v-if="!isGenerating">
334
- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor">
335
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
336
- </svg>
337
  下載雙頁 PDF
338
  </span>
339
  <span v-else>
 
340
  處理中...
341
  </span>
342
  </button>
@@ -385,10 +402,10 @@
385
  class="grid-cell relative border border-slate-300"
386
  >
387
  <div class="rotation-wrapper" :style="{ transform: `rotate(${cell.rotation}deg)` }">
388
- <span v-if="cell.type === 'text'" class="text-2xl font-bold text-slate-800 cell-text">{{ cell.content }}</span>
389
 
390
- <!-- Render Icon: Emoji -->
391
- <span v-if="cell.type === 'icon'" class="text-3xl leading-none select-none">{{ icons[cell.content] }}</span>
392
 
393
  <!-- Image Renderer -->
394
  <img v-if="cell.type === 'image'" :src="cell.content" class="w-4/5 h-4/5 object-contain">
@@ -402,7 +419,7 @@
402
  <!-- EDIT MODE -->
403
  <div v-else class="flex flex-col gap-2 animate-fade-in">
404
  <button @click="viewMode = 'overview'" class="self-start text-slate-500 hover:text-indigo-600 font-bold text-sm flex items-center gap-1">
405
- 回到全覽
406
  </button>
407
 
408
  <div
@@ -435,12 +452,12 @@
435
  </span>
436
 
437
  <div class="rotation-wrapper pointer-events-none" :style="{ transform: `rotate(${cell.rotation}deg)` }">
438
- <span v-if="cell.type === 'text'" class="text-4xl md:text-5xl font-bold text-slate-800 cell-text block">
439
  {{ cell.content }}
440
  </span>
441
 
442
- <!-- Render Icon: Emoji -->
443
- <span v-if="cell.type === 'icon'" class="text-4xl md:text-5xl leading-none select-none">{{ icons[cell.content] }}</span>
444
 
445
  <!-- Image Renderer -->
446
  <img v-if="cell.type === 'image'" :src="cell.content" class="w-4/5 h-4/5 object-contain">
@@ -469,7 +486,7 @@
469
  </div>
470
 
471
  <script>
472
- const { createApp, ref, computed, nextTick, onMounted } = Vue;
473
  const { jsPDF } = window.jspdf;
474
 
475
  createApp({
@@ -482,26 +499,29 @@
482
  ];
483
  const currentGrid = ref(gridOptions[1]); // Default 6x8
484
 
485
- // --- Icons (Unified to Emoji) ---
486
- // Using standard unicode emojis for consistent PDF export logic
 
 
 
487
  const icons = {
488
- '幸運草': '🍀',
489
- '愛心': '❤️',
490
- '星星': '',
491
- '勝利': '✌️',
492
- '獎盃': '🏆',
493
- '笑臉': '😊',
494
- '皇冠': '👑',
495
- '鑽石': '💎',
496
- '燈泡': '💡',
497
- '太陽': '☀️',
498
- '月亮': '🌙',
499
- '雲朵': '☁️',
500
- '音符': '🎵',
501
- '飛機': '✈️',
502
- '花朵': '🌸',
503
- '樹木': '🌳',
504
- '禮物': '🎁'
505
  };
506
 
507
  // --- State Initialization ---
@@ -509,7 +529,8 @@
509
  id: pageOffset + i,
510
  type: 'text',
511
  content: '',
512
- rotation: 0
 
513
  }));
514
 
515
  const pages = ref([
@@ -521,6 +542,7 @@
521
  const activePageId = ref(1);
522
  const selectedCellIndex = ref(null);
523
  const inputBuffer = ref('');
 
524
  const isGenerating = ref(false);
525
 
526
  // Refs
@@ -535,6 +557,9 @@
535
  const customImages = ref([]); // Stores base64 strings
536
  let cropperInstance = null;
537
 
 
 
 
538
  const activePageCells = computed(() => pages.value[activePageId.value - 1].cells);
539
 
540
  // --- Actions ---
@@ -584,7 +609,7 @@
584
  }
585
  };
586
 
587
- const setCell = (pageIndex, row, col, type, content, rotation = 0) => {
588
  if (row > currentGrid.value.rows || col > currentGrid.value.cols) return;
589
  const cells = pages.value[pageIndex].cells;
590
  const index = (row - 1) * currentGrid.value.cols + (col - 1);
@@ -592,9 +617,17 @@
592
  cells[index].type = type;
593
  cells[index].content = content;
594
  cells[index].rotation = rotation;
 
595
  }
596
  };
597
 
 
 
 
 
 
 
 
598
  // --- Image Upload Logic ---
599
 
600
  const triggerImageUpload = () => {
@@ -609,10 +642,8 @@
609
  reader.onload = (e) => {
610
  tempImageSrc.value = e.target.result;
611
  showCropper.value = true;
612
- // Reset input so same file can be selected again if needed
613
  event.target.value = '';
614
 
615
- // Initialize cropper after DOM update
616
  nextTick(() => {
617
  if (cropperInstance) cropperInstance.destroy();
618
  cropperInstance = new Cropper(cropperImgRef.value, {
@@ -640,14 +671,14 @@
640
  if (!cropperInstance) return;
641
 
642
  const canvas = cropperInstance.getCroppedCanvas({
643
- width: 300, // Resize to 300x300 for optimization
644
  height: 300
645
  });
646
 
647
- const base64 = canvas.toDataURL('image/jpeg', 0.85); // Compress slightly
648
  customImages.value.push(base64);
649
 
650
- cancelCrop(); // Cleanup and close
651
  };
652
 
653
  const applyCustomImageToCell = (imgSrc) => {
@@ -668,11 +699,11 @@
668
 
669
  const exportProject = () => {
670
  const projectData = {
671
- version: '1.3',
672
  timestamp: new Date().toISOString(),
673
  grid: currentGrid.value,
674
  pages: pages.value,
675
- customImages: customImages.value // Also save custom images
676
  };
677
 
678
  const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(projectData));
@@ -704,7 +735,6 @@
704
  currentGrid.value = importedData.grid;
705
  pages.value = importedData.pages;
706
 
707
- // Import custom images if they exist
708
  if (importedData.customImages && Array.isArray(importedData.customImages)) {
709
  customImages.value = importedData.customImages;
710
  }
@@ -725,10 +755,14 @@
725
  };
726
 
727
  const applyTemplate = (templateId) => {
 
 
 
 
 
728
  try {
729
  const targetConf = gridOptions[1]; // 6x8
730
  currentGrid.value = targetConf;
731
- const totalCells = 48;
732
 
733
  pages.value = [
734
  { id: 1, cells: createPageCells(0, 8, 6) },
@@ -739,29 +773,29 @@
739
  // Page 1
740
  setCell(0, 1, 3, 'text', '最', 180);
741
  setCell(0, 1, 4, 'text', '是', 180);
742
- setCell(0, 2, 3, 'icon', '幸運草', 0);
743
- setCell(0, 2, 4, 'icon', '幸運草', 0);
744
- setCell(0, 3, 3, 'icon', '幸運草', 0);
745
- setCell(0, 3, 4, 'icon', '幸運草', 0);
746
  setCell(0, 3, 5, 'text', '遇', 0);
747
  setCell(0, 3, 6, 'text', '幸', 0);
748
- setCell(0, 6, 3, 'icon', '幸運草', 0);
749
- setCell(0, 6, 4, 'icon', '幸運草', 0);
750
  setCell(0, 6, 5, 'text', '見', 0);
751
  setCell(0, 6, 6, 'text', '運', 0);
752
- setCell(0, 7, 3, 'icon', '幸運草', 0);
753
- setCell(0, 7, 4, 'icon', '幸運草', 0);
754
  setCell(0, 8, 3, 'text', '就', 180);
755
  setCell(0, 8, 4, 'text', '你', 180);
756
 
757
  // Page 2
758
  setCell(1, 1, 6, 'text', '茫', 180);
759
- setCell(1, 2, 6, 'icon', '幸運草', 0);
760
  setCell(1, 5, 5, 'text', '茫', 0);
761
- setCell(1, 5, 6, 'icon', '幸運草', 0);
762
  setCell(1, 6, 5, 'text', '人', 0);
763
- setCell(1, 6, 6, 'icon', '幸運草', 0);
764
- setCell(1, 7, 6, 'icon', '幸運草', 0);
765
  setCell(1, 8, 6, 'text', '海', 180);
766
  }
767
 
@@ -789,6 +823,7 @@
789
  selectedCellIndex.value = index;
790
  const cell = activePageCells.value[index];
791
  inputBuffer.value = (cell.type === 'text') ? cell.content : '';
 
792
  nextTick(() => { if (textInputRef.value) textInputRef.value.focus(); });
793
  } else {
794
  rotateCurrentCell();
@@ -806,6 +841,7 @@
806
  const cell = activePageCells.value[selectedCellIndex.value];
807
  cell.type = 'text';
808
  cell.content = inputBuffer.value;
 
809
  };
810
 
811
  const applyIconToCell = (iconName) => {
@@ -813,6 +849,7 @@
813
  const cell = activePageCells.value[selectedCellIndex.value];
814
  cell.type = 'icon';
815
  cell.content = iconName;
 
816
  inputBuffer.value = '';
817
  };
818
 
@@ -822,9 +859,56 @@
822
  cell.content = '';
823
  cell.type = 'text';
824
  cell.rotation = 0;
 
825
  inputBuffer.value = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
  };
827
 
 
828
  const renderPageToCanvas = async (pageId) => {
829
  const pageData = pages.value[pageId - 1];
830
  const rows = currentGrid.value.rows;
@@ -844,13 +928,15 @@
844
  pageData.cells.forEach((cell, idx) => {
845
  let contentHtml = '';
846
  if (cell.type === 'text') {
847
- contentHtml = `<span class="export-text" style="font-size: 40px; font-weight: bold; color: #1e293b; font-family: 'Noto Sans TC', sans-serif;">${cell.content}</span>`;
 
848
  } else if (cell.type === 'icon') {
849
- // FIX: Treat icons (Emoji) as text for export!
850
- // Using same class 'export-text' so they get SVG centered replacement.
851
- // Slightly larger font size for emojis usually looks better.
852
- const iconVal = icons[cell.content];
853
- contentHtml = `<span class="export-text" style="font-size: 50px; line-height: 1; user-select: none;">${iconVal}</span>`;
 
854
  } else if (cell.type === 'image') {
855
  contentHtml = `<img src="${cell.content}" style="width: 80%; height: 80%; object-fit: contain;">`;
856
  }
@@ -878,42 +964,23 @@
878
  useCORS: true,
879
  backgroundColor: '#ffffff',
880
  onclone: (clonedDoc) => {
881
- // Both text and icons (emojis) now have class 'export-text'
882
  const textElements = clonedDoc.querySelectorAll('.export-text');
883
-
884
  textElements.forEach(el => {
885
  const textContent = el.innerText;
 
886
  if (!textContent) return;
 
 
887
 
888
- const ns = "http://www.w3.org/2000/svg";
889
- const svg = document.createElementNS(ns, "svg");
890
- svg.setAttribute("width", "100%");
891
- svg.setAttribute("height", "100%");
892
- svg.setAttribute("viewBox", "0 0 100 100");
893
- svg.style.position = "absolute";
894
- svg.style.top = "0";
895
- svg.style.left = "0";
896
-
897
- const textNode = document.createElementNS(ns, "text");
898
- textNode.setAttribute("x", "50%");
899
- textNode.setAttribute("y", "50%");
900
- textNode.setAttribute("dominant-baseline", "central");
901
- textNode.setAttribute("text-anchor", "middle");
902
- textNode.setAttribute("fill", "#1e293b");
903
- textNode.setAttribute("font-family", "'Noto Sans TC', sans-serif"); // Noto Sans supports many emojis
904
- textNode.setAttribute("font-weight", "bold");
905
-
906
- // Adjust size slightly for Emojis vs Text if needed,
907
- // but 45 is a safe middle ground for the SVG viewbox 0-100
908
- textNode.setAttribute("font-size", "45");
909
- textNode.textContent = textContent;
910
-
911
- svg.appendChild(textNode);
912
-
913
- const parent = el.parentNode;
914
- parent.style.position = "relative";
915
- parent.innerHTML = '';
916
- parent.appendChild(svg);
917
  });
918
  }
919
  });
@@ -921,6 +988,36 @@
921
  return canvas;
922
  };
923
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
924
  const exportPDF = async () => {
925
  if (selectedCellIndex.value !== null) selectedCellIndex.value = null;
926
  isGenerating.value = true;
@@ -962,6 +1059,9 @@
962
  selectedCellIndex,
963
  inputBuffer,
964
  icons,
 
 
 
965
  textInputRef,
966
  isGenerating,
967
  switchToPage,
 
19
  <link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css" rel="stylesheet">
20
  <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
21
 
22
+ <!-- FontAwesome 6 (For Consistent Icons) -->
23
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
24
+
25
  <!-- Google Fonts -->
26
  <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700&display=swap" rel="stylesheet">
27
 
 
100
  width: 100%;
101
  height: 100%;
102
  }
103
+
104
+ /* FontAwesome Adjustments */
105
+ .fa-icon {
106
+ display: inline-block;
107
+ line-height: 1;
108
+ }
109
  </style>
110
  </head>
111
  <body class="h-screen overflow-hidden text-slate-800">
 
119
 
120
  <!-- Header -->
121
  <div class="p-6 border-b border-slate-100 bg-slate-50">
122
+ <div class="flex justify-between items-center mb-2">
123
+ <h1 class="text-2xl font-bold text-indigo-600 flex items-center gap-2">
124
+ <i class="fa-solid fa-wand-magic-sparkles"></i>
125
+ 魔法摺紙
126
+ </h1>
127
+ </div>
128
 
129
  <!-- Page Navigator -->
130
  <div class="mt-4 flex bg-slate-200 p-1 rounded-lg">
 
169
  @click="clearAllContent"
170
  class="w-full py-2 text-sm text-red-600 border border-red-200 bg-red-50 hover:bg-red-100 rounded-lg transition-all flex items-center justify-center gap-1"
171
  >
172
+ <i class="fa-solid fa-trash-can mr-1"></i>
 
 
173
  一鍵清空所有內容
174
  </button>
175
  </div>
 
181
  @click="applyTemplate('lucky')"
182
  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"
183
  >
184
+ <i class="fa-solid fa-clover text-xl"></i>
185
+ <span class="text-sm">套用:幸運遇見你<br><span class="text-xs opacity-90">(如皓老師分享)</span></span>
 
186
  </button>
187
  <p class="text-xs text-slate-400 mt-2">提示:套用後會自動切換為 6x8 網格並填入內容。</p>
188
  </div>
 
195
  @click="exportProject"
196
  class="py-2 bg-slate-600 hover:bg-slate-700 text-white font-bold rounded-lg shadow-sm active:scale-95 transition-all flex items-center justify-center gap-1 text-sm"
197
  >
198
+ <i class="fa-solid fa-download mr-1"></i>
 
 
199
  匯出存檔
200
  </button>
201
  <button
202
  @click="triggerImport"
203
  class="py-2 bg-slate-600 hover:bg-slate-700 text-white font-bold rounded-lg shadow-sm active:scale-95 transition-all flex items-center justify-center gap-1 text-sm"
204
  >
205
+ <i class="fa-solid fa-upload mr-1"></i>
 
 
206
  匯入舊檔
207
  </button>
208
  <input
 
228
 
229
  <!-- Editing Controls -->
230
  <div v-if="viewMode === 'edit'" class="space-y-8 animate-fade-in pb-10">
231
+
232
+ <!-- Color Picker (New) -->
233
+ <div>
234
+ <label class="block text-sm font-bold text-slate-700 mb-2">1. 選擇顏色</label>
235
+ <div class="flex flex-wrap gap-2">
236
+ <button
237
+ v-for="color in colors"
238
+ :key="color"
239
+ @click="applyColor(color)"
240
+ :disabled="selectedCellIndex === null"
241
+ class="w-8 h-8 rounded-full border-2 transition-all shadow-sm active:scale-95 hover:scale-110"
242
+ :class="{'ring-2 ring-indigo-400 ring-offset-2': selectedColor === color && selectedCellIndex !== null, 'opacity-50 cursor-not-allowed': selectedCellIndex === null}"
243
+ :style="{ backgroundColor: color, borderColor: color === '#ffffff' ? '#e2e8f0' : 'transparent' }"
244
+ ></button>
245
+ </div>
246
+ </div>
247
+
248
  <!-- Text Input -->
249
  <div>
250
+ <label class="block text-sm font-bold text-slate-700 mb-2">2. 文字輸入</label>
251
  <input
252
  ref="textInputRef"
253
  type="text"
 
258
  :disabled="selectedCellIndex === null"
259
  class="w-full text-center text-2xl p-3 border-2 rounded-lg focus:outline-none focus:ring-2 transition-all"
260
  :class="selectedCellIndex === null ? 'bg-slate-100 border-slate-200 cursor-not-allowed' : 'bg-white border-indigo-300 focus:border-indigo-500 focus:ring-indigo-200'"
261
+ :style="{ color: selectedCellIndex !== null ? selectedColor : '' }"
262
  >
263
  </div>
264
 
265
  <!-- Icon Picker -->
266
  <div>
267
+ <label class="block text-sm font-bold text-slate-700 mb-3">3. 選擇圖示</label>
268
  <div class="grid grid-cols-4 gap-2 max-h-60 overflow-y-auto p-1 custom-scrollbar">
269
  <button
270
+ v-for="(iconClass, name) in icons"
271
  :key="name"
272
  @click="applyIconToCell(name)"
273
  :disabled="selectedCellIndex === null"
 
275
  :class="selectedCellIndex === null ? 'border-slate-200 text-slate-300 cursor-not-allowed' : 'border-slate-300 text-slate-600 hover:border-indigo-400 hover:text-indigo-600 cursor-pointer'"
276
  :title="name"
277
  >
278
+ <i :class="[iconClass, 'text-3xl']" :style="{ color: selectedCellIndex !== null ? selectedColor : '' }"></i>
279
  </button>
280
  </div>
281
  </div>
 
283
  <!-- Image Upload -->
284
  <div>
285
  <div class="flex justify-between items-center mb-3">
286
+ <label class="block text-sm font-bold text-slate-700">4. 自定義圖片</label>
287
  <button
288
  @click="triggerImageUpload"
289
  class="text-xs bg-indigo-100 text-indigo-700 px-2 py-1 rounded hover:bg-indigo-200 font-bold flex items-center gap-1"
290
  >
291
+ <i class="fa-solid fa-image"></i>
 
 
292
  上傳圖片
293
  </button>
294
  <input type="file" ref="imageUploadInput" class="hidden" accept="image/*" @change="handleImageUpload">
 
349
  class="w-full py-4 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-xl shadow-lg hover:shadow-xl active:scale-95 transition-all flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-wait"
350
  >
351
  <span v-if="!isGenerating">
352
+ <i class="fa-solid fa-file-pdf text-xl"></i>
 
 
353
  下載雙頁 PDF
354
  </span>
355
  <span v-else>
356
+ <i class="fa-solid fa-circle-notch fa-spin"></i>
357
  處理中...
358
  </span>
359
  </button>
 
402
  class="grid-cell relative border border-slate-300"
403
  >
404
  <div class="rotation-wrapper" :style="{ transform: `rotate(${cell.rotation}deg)` }">
405
+ <span v-if="cell.type === 'text'" class="text-2xl font-bold cell-text" :style="{ color: cell.color }">{{ cell.content }}</span>
406
 
407
+ <!-- Render Icon: FontAwesome -->
408
+ <i v-if="cell.type === 'icon'" :class="[icons[cell.content], 'text-3xl fa-icon']" :style="{ color: cell.color }"></i>
409
 
410
  <!-- Image Renderer -->
411
  <img v-if="cell.type === 'image'" :src="cell.content" class="w-4/5 h-4/5 object-contain">
 
419
  <!-- EDIT MODE -->
420
  <div v-else class="flex flex-col gap-2 animate-fade-in">
421
  <button @click="viewMode = 'overview'" class="self-start text-slate-500 hover:text-indigo-600 font-bold text-sm flex items-center gap-1">
422
+ <i class="fa-solid fa-arrow-left"></i> 回到全覽
423
  </button>
424
 
425
  <div
 
452
  </span>
453
 
454
  <div class="rotation-wrapper pointer-events-none" :style="{ transform: `rotate(${cell.rotation}deg)` }">
455
+ <span v-if="cell.type === 'text'" class="text-4xl md:text-5xl font-bold cell-text block" :style="{ color: cell.color }">
456
  {{ cell.content }}
457
  </span>
458
 
459
+ <!-- Render Icon: FontAwesome -->
460
+ <i v-if="cell.type === 'icon'" :class="[icons[cell.content], 'text-4xl md:text-5xl fa-icon']" :style="{ color: cell.color }"></i>
461
 
462
  <!-- Image Renderer -->
463
  <img v-if="cell.type === 'image'" :src="cell.content" class="w-4/5 h-4/5 object-contain">
 
486
  </div>
487
 
488
  <script>
489
+ const { createApp, ref, computed, nextTick, onMounted, watch } = Vue;
490
  const { jsPDF } = window.jspdf;
491
 
492
  createApp({
 
499
  ];
500
  const currentGrid = ref(gridOptions[1]); // Default 6x8
501
 
502
+ // --- Colors ---
503
+ // Slate-800 is default
504
+ const colors = ['#1e293b', '#ef4444', '#f97316', '#f59e0b', '#22c55e', '#14b8a6', '#3b82f6', '#6366f1', '#a855f7', '#ec4899'];
505
+
506
+ // --- Icons (Unified to FontAwesome) ---
507
  const icons = {
508
+ '幸運草': 'fa-solid fa-clover',
509
+ '愛心': 'fa-solid fa-heart',
510
+ '星星': 'fa-solid fa-star',
511
+ '勝利': 'fa-solid fa-hand-peace',
512
+ '獎盃': 'fa-solid fa-trophy',
513
+ '笑臉': 'fa-solid fa-face-smile',
514
+ '皇冠': 'fa-solid fa-crown',
515
+ '鑽石': 'fa-solid fa-gem',
516
+ '燈泡': 'fa-solid fa-lightbulb',
517
+ '太陽': 'fa-solid fa-sun',
518
+ '月亮': 'fa-solid fa-moon',
519
+ '雲朵': 'fa-solid fa-cloud',
520
+ '音符': 'fa-solid fa-music',
521
+ '飛機': 'fa-solid fa-plane',
522
+ '花朵': 'fa-solid fa-fan',
523
+ '樹木': 'fa-solid fa-tree',
524
+ '禮物': 'fa-solid fa-gift'
525
  };
526
 
527
  // --- State Initialization ---
 
529
  id: pageOffset + i,
530
  type: 'text',
531
  content: '',
532
+ rotation: 0,
533
+ color: '#1e293b' // Default color
534
  }));
535
 
536
  const pages = ref([
 
542
  const activePageId = ref(1);
543
  const selectedCellIndex = ref(null);
544
  const inputBuffer = ref('');
545
+ const selectedColor = ref('#1e293b');
546
  const isGenerating = ref(false);
547
 
548
  // Refs
 
557
  const customImages = ref([]); // Stores base64 strings
558
  let cropperInstance = null;
559
 
560
+ // --- Auto-Save Key ---
561
+ const AUTOSAVE_KEY = 'magic_origami_autosave_v1';
562
+
563
  const activePageCells = computed(() => pages.value[activePageId.value - 1].cells);
564
 
565
  // --- Actions ---
 
609
  }
610
  };
611
 
612
+ const setCell = (pageIndex, row, col, type, content, rotation = 0, color = '#1e293b') => {
613
  if (row > currentGrid.value.rows || col > currentGrid.value.cols) return;
614
  const cells = pages.value[pageIndex].cells;
615
  const index = (row - 1) * currentGrid.value.cols + (col - 1);
 
617
  cells[index].type = type;
618
  cells[index].content = content;
619
  cells[index].rotation = rotation;
620
+ cells[index].color = color;
621
  }
622
  };
623
 
624
+ const applyColor = (color) => {
625
+ if (selectedCellIndex.value === null) return;
626
+ const cell = activePageCells.value[selectedCellIndex.value];
627
+ cell.color = color;
628
+ selectedColor.value = color;
629
+ };
630
+
631
  // --- Image Upload Logic ---
632
 
633
  const triggerImageUpload = () => {
 
642
  reader.onload = (e) => {
643
  tempImageSrc.value = e.target.result;
644
  showCropper.value = true;
 
645
  event.target.value = '';
646
 
 
647
  nextTick(() => {
648
  if (cropperInstance) cropperInstance.destroy();
649
  cropperInstance = new Cropper(cropperImgRef.value, {
 
671
  if (!cropperInstance) return;
672
 
673
  const canvas = cropperInstance.getCroppedCanvas({
674
+ width: 300,
675
  height: 300
676
  });
677
 
678
+ const base64 = canvas.toDataURL('image/jpeg', 0.85);
679
  customImages.value.push(base64);
680
 
681
+ cancelCrop();
682
  };
683
 
684
  const applyCustomImageToCell = (imgSrc) => {
 
699
 
700
  const exportProject = () => {
701
  const projectData = {
702
+ version: '1.5',
703
  timestamp: new Date().toISOString(),
704
  grid: currentGrid.value,
705
  pages: pages.value,
706
+ customImages: customImages.value
707
  };
708
 
709
  const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(projectData));
 
735
  currentGrid.value = importedData.grid;
736
  pages.value = importedData.pages;
737
 
 
738
  if (importedData.customImages && Array.isArray(importedData.customImages)) {
739
  customImages.value = importedData.customImages;
740
  }
 
755
  };
756
 
757
  const applyTemplate = (templateId) => {
758
+ // Add Confirmation
759
+ if (!confirm("套用模板將會清空您目前的設計內容,確定要套用嗎?")) {
760
+ return;
761
+ }
762
+
763
  try {
764
  const targetConf = gridOptions[1]; // 6x8
765
  currentGrid.value = targetConf;
 
766
 
767
  pages.value = [
768
  { id: 1, cells: createPageCells(0, 8, 6) },
 
773
  // Page 1
774
  setCell(0, 1, 3, 'text', '最', 180);
775
  setCell(0, 1, 4, 'text', '是', 180);
776
+ setCell(0, 2, 3, 'icon', '幸運草', 0, '#22c55e');
777
+ setCell(0, 2, 4, 'icon', '幸運草', 0, '#22c55e');
778
+ setCell(0, 3, 3, 'icon', '幸運草', 0, '#22c55e');
779
+ setCell(0, 3, 4, 'icon', '幸運草', 0, '#22c55e');
780
  setCell(0, 3, 5, 'text', '遇', 0);
781
  setCell(0, 3, 6, 'text', '幸', 0);
782
+ setCell(0, 6, 3, 'icon', '幸運草', 0, '#22c55e');
783
+ setCell(0, 6, 4, 'icon', '幸運草', 0, '#22c55e');
784
  setCell(0, 6, 5, 'text', '見', 0);
785
  setCell(0, 6, 6, 'text', '運', 0);
786
+ setCell(0, 7, 3, 'icon', '幸運草', 0, '#22c55e');
787
+ setCell(0, 7, 4, 'icon', '幸運草', 0, '#22c55e');
788
  setCell(0, 8, 3, 'text', '就', 180);
789
  setCell(0, 8, 4, 'text', '你', 180);
790
 
791
  // Page 2
792
  setCell(1, 1, 6, 'text', '茫', 180);
793
+ setCell(1, 2, 6, 'icon', '幸運草', 0, '#22c55e');
794
  setCell(1, 5, 5, 'text', '茫', 0);
795
+ setCell(1, 5, 6, 'icon', '幸運草', 0, '#22c55e');
796
  setCell(1, 6, 5, 'text', '人', 0);
797
+ setCell(1, 6, 6, 'icon', '幸運草', 0, '#22c55e');
798
+ setCell(1, 7, 6, 'icon', '幸運草', 0, '#22c55e');
799
  setCell(1, 8, 6, 'text', '海', 180);
800
  }
801
 
 
823
  selectedCellIndex.value = index;
824
  const cell = activePageCells.value[index];
825
  inputBuffer.value = (cell.type === 'text') ? cell.content : '';
826
+ selectedColor.value = cell.color; // Sync color picker
827
  nextTick(() => { if (textInputRef.value) textInputRef.value.focus(); });
828
  } else {
829
  rotateCurrentCell();
 
841
  const cell = activePageCells.value[selectedCellIndex.value];
842
  cell.type = 'text';
843
  cell.content = inputBuffer.value;
844
+ cell.color = selectedColor.value; // Apply current color
845
  };
846
 
847
  const applyIconToCell = (iconName) => {
 
849
  const cell = activePageCells.value[selectedCellIndex.value];
850
  cell.type = 'icon';
851
  cell.content = iconName;
852
+ cell.color = selectedColor.value; // Apply current color
853
  inputBuffer.value = '';
854
  };
855
 
 
859
  cell.content = '';
860
  cell.type = 'text';
861
  cell.rotation = 0;
862
+ cell.color = '#1e293b'; // Reset color
863
  inputBuffer.value = '';
864
+ selectedColor.value = '#1e293b';
865
+ };
866
+
867
+ // --- Auto Save & Init ---
868
+
869
+ onMounted(() => {
870
+ // 1. Auto-save restoration check
871
+ const savedData = localStorage.getItem(AUTOSAVE_KEY);
872
+ if (savedData) {
873
+ try {
874
+ const parsed = JSON.parse(savedData);
875
+ const hasContent = parsed.pages.some(p => p.cells.some(c => c.content !== ''));
876
+
877
+ if (hasContent) {
878
+ const restore = confirm("發現上次未完成的自動存檔,是否要還原進度?");
879
+ if (restore) {
880
+ currentGrid.value = parsed.grid;
881
+ pages.value = parsed.pages;
882
+ customImages.value = parsed.customImages || [];
883
+ }
884
+ }
885
+ } catch (e) {
886
+ console.error("Auto-save parse error", e);
887
+ }
888
+ }
889
+
890
+ // 2. Auto-save Watcher
891
+ watch([currentGrid, pages, customImages], () => {
892
+ const dataToSave = {
893
+ grid: currentGrid.value,
894
+ pages: pages.value,
895
+ customImages: customImages.value
896
+ };
897
+ localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(dataToSave));
898
+ }, { deep: true });
899
+ });
900
+
901
+ // Helper to extract unicode from FontAwesome Class
902
+ const getIconUnicode = (className) => {
903
+ const el = document.createElement('i');
904
+ el.className = className;
905
+ document.body.appendChild(el);
906
+ const content = window.getComputedStyle(el, ':before').getPropertyValue('content');
907
+ document.body.removeChild(el);
908
+ return content.replace(/['"]/g, '');
909
  };
910
 
911
+ // ... PDF Export Logic ...
912
  const renderPageToCanvas = async (pageId) => {
913
  const pageData = pages.value[pageId - 1];
914
  const rows = currentGrid.value.rows;
 
928
  pageData.cells.forEach((cell, idx) => {
929
  let contentHtml = '';
930
  if (cell.type === 'text') {
931
+ // Text uses the SVG text replacement trick for perfect centering
932
+ contentHtml = `<span class="export-text" data-color="${cell.color}" style="font-size: 40px; font-weight: bold; color: ${cell.color}; font-family: 'Noto Sans TC', sans-serif;">${cell.content}</span>`;
933
  } else if (cell.type === 'icon') {
934
+ // FontAwesome Icons -> Convert to Unicode Char + SVG Text Trick
935
+ // This ensures the icon behaves exactly like text (perfectly centered, rotates well)
936
+ const iconClass = icons[cell.content];
937
+ const unicode = getIconUnicode(iconClass);
938
+ // Using 'Font Awesome 6 Free' with font-weight 900 for solid icons
939
+ contentHtml = `<span class="export-icon" data-color="${cell.color}" style="font-size: 50px; line-height: 1; color: ${cell.color};">${unicode}</span>`;
940
  } else if (cell.type === 'image') {
941
  contentHtml = `<img src="${cell.content}" style="width: 80%; height: 80%; object-fit: contain;">`;
942
  }
 
964
  useCORS: true,
965
  backgroundColor: '#ffffff',
966
  onclone: (clonedDoc) => {
967
+ // Handle Text Centering
968
  const textElements = clonedDoc.querySelectorAll('.export-text');
 
969
  textElements.forEach(el => {
970
  const textContent = el.innerText;
971
+ const color = el.getAttribute('data-color');
972
  if (!textContent) return;
973
+ replaceWithSvgText(el, textContent, color, "'Noto Sans TC', sans-serif", "bold");
974
+ });
975
 
976
+ // Handle Icon Centering (Same trick!)
977
+ const iconElements = clonedDoc.querySelectorAll('.export-icon');
978
+ iconElements.forEach(el => {
979
+ const textContent = el.innerText;
980
+ const color = el.getAttribute('data-color');
981
+ if (!textContent) return;
982
+ // FontAwesome solid needs font-weight 900
983
+ replaceWithSvgText(el, textContent, color, "'Font Awesome 6 Free'", "900");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984
  });
985
  }
986
  });
 
988
  return canvas;
989
  };
990
 
991
+ // Helper to replace text element with SVG for perfect centering
992
+ const replaceWithSvgText = (el, content, color, fontFamily, fontWeight) => {
993
+ const ns = "http://www.w3.org/2000/svg";
994
+ const svg = document.createElementNS(ns, "svg");
995
+ svg.setAttribute("width", "100%");
996
+ svg.setAttribute("height", "100%");
997
+ svg.setAttribute("viewBox", "0 0 100 100");
998
+ svg.style.position = "absolute";
999
+ svg.style.top = "0";
1000
+ svg.style.left = "0";
1001
+
1002
+ const textNode = document.createElementNS(ns, "text");
1003
+ textNode.setAttribute("x", "50%");
1004
+ textNode.setAttribute("y", "50%");
1005
+ textNode.setAttribute("dominant-baseline", "central");
1006
+ textNode.setAttribute("text-anchor", "middle");
1007
+ textNode.setAttribute("fill", color);
1008
+ textNode.setAttribute("font-family", fontFamily);
1009
+ textNode.setAttribute("font-weight", fontWeight);
1010
+ textNode.setAttribute("font-size", "45");
1011
+ textNode.textContent = content;
1012
+
1013
+ svg.appendChild(textNode);
1014
+
1015
+ const parent = el.parentNode;
1016
+ parent.style.position = "relative";
1017
+ parent.innerHTML = '';
1018
+ parent.appendChild(svg);
1019
+ };
1020
+
1021
  const exportPDF = async () => {
1022
  if (selectedCellIndex.value !== null) selectedCellIndex.value = null;
1023
  isGenerating.value = true;
 
1059
  selectedCellIndex,
1060
  inputBuffer,
1061
  icons,
1062
+ colors,
1063
+ selectedColor,
1064
+ applyColor,
1065
  textInputRef,
1066
  isGenerating,
1067
  switchToPage,