Lashtw commited on
Commit
cb40a55
·
verified ·
1 Parent(s): 254929a

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +216 -116
index.html CHANGED
@@ -115,7 +115,6 @@
115
  <div class="p-6 border-b border-slate-100 bg-slate-50">
116
  <div class="flex justify-between items-center mb-2">
117
  <h1 class="text-2xl font-bold text-indigo-600 flex items-center gap-2">
118
- <!-- Top Hat Icon U+1F3A9 -->
119
  <span class="text-3xl">🎩</span>
120
  摺紙魔術設計師
121
  </h1>
@@ -172,10 +171,7 @@
172
  @click="clearAllContent"
173
  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"
174
  >
175
- <!-- Trash Icon -->
176
- <svg viewBox="0 0 448 512" class="w-4 h-4 fill-current mr-1">
177
- <path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/>
178
- </svg>
179
  一鍵清空所有內容
180
  </button>
181
  </div>
@@ -201,16 +197,14 @@
201
  @click="exportProject"
202
  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"
203
  >
204
- <!-- Download Icon -->
205
- <svg viewBox="0 0 512 512" class="w-4 h-4 fill-current mr-1"><path d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32V274.7l-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7V32zM64 352c-35.3 0-64 28.7-64 64v32c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V416c0-35.3-28.7-64-64-64H346.5l-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352H64zM432 456c-13.3 0-24-10.7-24-24s10.7-24 24-24s24 10.7 24 24s-10.7 24-24 24z"/></svg>
206
  匯出存檔
207
  </button>
208
  <button
209
  @click="triggerImport"
210
  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"
211
  >
212
- <!-- Upload Icon -->
213
- <svg viewBox="0 0 512 512" class="w-4 h-4 fill-current mr-1"><path d="M288 109.3V352c0 17.7-14.3 32-32 32s-32-14.3-32-32V109.3l-73.4 73.4c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l128-128c12.5-12.5 32.8-12.5 45.3 0l128 128c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L288 109.3zM64 352H192c0 35.3 28.7 64 64 64s64-28.7 64-64H448c35.3 0 64 28.7 64 64v32c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V416c0-35.3 28.7-64 64-64zM432 456c13.3 0 24-10.7 24-24s-10.7-24-24-24s-24 10.7 24 24s10.7 24 24 24z"/></svg>
214
  匯入舊檔
215
  </button>
216
  <input
@@ -230,57 +224,87 @@
230
  <li v-if="viewMode === 'overview'"><strong>點擊頁面:</strong>進入該頁編輯模式</li>
231
  <li v-else><strong>目前編輯:</strong>第 {{ activePageId }} 頁</li>
232
  <li><strong>點擊格子:</strong>選取編輯</li>
233
- <li><strong>再次點擊:</strong>旋轉 90°</li>
 
234
  </ul>
235
  </div>
236
 
237
  <!-- Editing Controls -->
238
  <div v-if="viewMode === 'edit'" class="space-y-8 animate-fade-in pb-10">
239
 
240
- <!-- Color Picker (New) -->
241
- <div>
242
- <label class="block text-sm font-bold text-slate-700 mb-2">1. 選擇顏色</label>
243
- <div class="flex flex-wrap gap-2">
244
- <button
245
- v-for="color in colors"
246
- :key="color"
247
- @click="applyColor(color)"
248
- :disabled="selectedCellIndex === null"
249
- class="w-8 h-8 rounded-full border-2 transition-all shadow-sm active:scale-95 hover:scale-110"
250
- :class="{'ring-2 ring-indigo-400 ring-offset-2': selectedColor === color && selectedCellIndex !== null, 'opacity-50 cursor-not-allowed': selectedCellIndex === null}"
251
- :style="{ backgroundColor: color, borderColor: color === '#ffffff' ? '#e2e8f0' : 'transparent' }"
252
- ></button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  </div>
254
  </div>
255
 
256
  <!-- Text Input -->
257
  <div>
258
- <label class="block text-sm font-bold text-slate-700 mb-2">2. 文字輸入</label>
259
  <input
260
  ref="textInputRef"
261
  type="text"
262
  v-model="inputBuffer"
263
  @input="updateSelectedCellText"
264
  placeholder="先點選右側格子..."
265
- maxlength="1"
266
- :disabled="selectedCellIndex === null"
267
  class="w-full text-center text-2xl p-3 border-2 rounded-lg focus:outline-none focus:ring-2 transition-all"
268
- :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'"
269
- :style="{ color: selectedCellIndex !== null ? selectedColor : '' }"
270
  >
 
271
  </div>
272
 
273
  <!-- Icon Picker -->
274
  <div>
275
- <label class="block text-sm font-bold text-slate-700 mb-3">3. 選擇圖示</label>
276
  <div class="grid grid-cols-4 gap-2 max-h-60 overflow-y-auto p-1 custom-scrollbar">
277
  <button
278
  v-for="(val, name) in icons"
279
  :key="name"
280
  @click="applyIconToCell(name)"
281
- :disabled="selectedCellIndex === null"
282
  class="aspect-square flex items-center justify-center rounded-lg border hover:bg-indigo-50 active:scale-95 transition-all"
283
- :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'"
284
  :title="name"
285
  >
286
  <span class="text-3xl leading-none select-none">{{ val }}</span>
@@ -291,7 +315,7 @@
291
  <!-- Image Upload -->
292
  <div>
293
  <div class="flex justify-between items-center mb-3">
294
- <label class="block text-sm font-bold text-slate-700">4. 自定義圖片</label>
295
  <button
296
  @click="triggerImageUpload"
297
  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"
@@ -310,9 +334,9 @@
310
  v-for="(imgSrc, idx) in customImages"
311
  :key="idx"
312
  @click="applyCustomImageToCell(imgSrc)"
313
- :disabled="selectedCellIndex === null"
314
  class="aspect-square flex items-center justify-center rounded-lg border overflow-hidden relative hover:opacity-90 active:scale-95 transition-all bg-white"
315
- :class="selectedCellIndex === null ? 'border-slate-200 cursor-not-allowed opacity-50' : 'border-slate-300 cursor-pointer hover:border-indigo-400 ring-offset-1'"
316
  >
317
  <img :src="imgSrc" class="w-full h-full object-cover">
318
  <!-- Delete Button -->
@@ -322,20 +346,19 @@
322
  </div>
323
 
324
  <!-- Cell Info -->
325
- <div v-if="selectedCellIndex !== null" class="bg-slate-100 p-4 rounded-lg flex justify-between items-center">
326
- <div>
327
- <span class="text-xs font-bold text-slate-500 uppercase">角度</span>
328
- <span class="text-lg font-mono font-bold text-slate-700 ml-2">{{ activePageCells[selectedCellIndex].rotation }}°</span>
329
  </div>
330
  <button @click="rotateCurrentCell" class="px-3 py-1 bg-white border border-slate-300 rounded shadow-sm text-sm">
331
- 手動旋轉
332
  </button>
333
  </div>
334
 
335
  <div>
336
  <button
337
  @click="clearCurrentCell"
338
- :disabled="selectedCellIndex === null"
339
  class="w-full py-2 text-sm text-red-500 border border-red-200 rounded hover:bg-red-50 disabled:opacity-50 disabled:cursor-not-allowed"
340
  >
341
  清除內容
@@ -442,9 +465,10 @@
442
  v-for="(cell, index) in pages[pageId-1].cells"
443
  :key="cell.id"
444
  class="grid-cell relative border border-slate-300"
 
445
  >
446
  <div class="rotation-wrapper" :style="{ transform: `rotate(${cell.rotation}deg)` }">
447
- <span v-if="cell.type === 'text'" class="text-4xl font-bold cell-text" :style="{ color: cell.color }">{{ cell.content }}</span>
448
 
449
  <!-- Render Icon: Emoji -->
450
  <span v-if="cell.type === 'icon'" class="text-3xl leading-none select-none">{{ icons[cell.content] }}</span>
@@ -486,12 +510,13 @@
486
  <div
487
  v-for="(cell, index) in activePageCells"
488
  :key="cell.id"
489
- @click="handleCellClick(index)"
490
  class="grid-cell relative border border-slate-300 cursor-pointer hover:bg-slate-50 select-none"
491
- :class="{ 'ring-4 ring-indigo-500 ring-inset z-20': selectedCellIndex === index }"
 
492
  >
493
  <div class="rotation-wrapper pointer-events-none" :style="{ transform: `rotate(${cell.rotation}deg)` }">
494
- <span v-if="cell.type === 'text'" class="text-5xl md:text-7xl font-bold cell-text block" :style="{ color: cell.color }">
495
  {{ cell.content }}
496
  </span>
497
 
@@ -540,6 +565,9 @@
540
 
541
  // --- Colors ---
542
  const colors = ['#1e293b', '#ef4444', '#f97316', '#f59e0b', '#22c55e', '#14b8a6', '#3b82f6', '#6366f1', '#a855f7', '#ec4899'];
 
 
 
543
 
544
  // --- Icons (Unified to Unicode Emoji) ---
545
  const icons = {
@@ -568,7 +596,8 @@
568
  type: 'text',
569
  content: '',
570
  rotation: 0,
571
- color: '#1e293b' // Default color
 
572
  }));
573
 
574
  const pages = ref([
@@ -578,9 +607,13 @@
578
 
579
  const viewMode = ref('overview');
580
  const activePageId = ref(1);
581
- const selectedCellIndex = ref(null);
 
 
 
582
  const inputBuffer = ref('');
583
  const selectedColor = ref('#1e293b');
 
584
  const isGenerating = ref(false);
585
 
586
  // Refs
@@ -604,12 +637,25 @@
604
 
605
  const activePageCells = computed(() => pages.value[activePageId.value - 1].cells);
606
 
607
- // Computed for UI style of custom button
608
  const isCustomGridActive = computed(() => {
609
- // If current config matches none of the preset labels, it's custom
610
  return !gridOptions.some(opt => opt.label === currentGrid.value.label);
611
  });
612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
  // --- Actions ---
614
 
615
  const changeGridSize = (conf) => {
@@ -628,7 +674,7 @@
628
  { id: 2, cells: createPageCells(totalCells, conf.rows, conf.cols) }
629
  ];
630
 
631
- selectedCellIndex.value = null;
632
  activePageId.value = 1;
633
  viewMode.value = 'overview';
634
  };
@@ -640,7 +686,6 @@
640
  if(!confirm("設定自訂網格將會清空您目前的設計,確定要��續嗎?")) return;
641
  }
642
 
643
- // Set initial values from current grid
644
  customGridConfig.value.rows = currentGrid.value.rows;
645
  customGridConfig.value.cols = currentGrid.value.cols;
646
  showGridModal.value = true;
@@ -664,7 +709,7 @@
664
  { id: 2, cells: createPageCells(totalCells, r, c) }
665
  ];
666
 
667
- selectedCellIndex.value = null;
668
  activePageId.value = 1;
669
  viewMode.value = 'overview';
670
  showGridModal.value = false;
@@ -687,13 +732,13 @@
687
  { id: 2, cells: createPageCells(totalCells, rows, cols) }
688
  ];
689
 
690
- selectedCellIndex.value = null;
691
  inputBuffer.value = '';
692
  alert("內容已清空!");
693
  }
694
  };
695
 
696
- const setCell = (pageIndex, row, col, type, content, rotation = 0, color = '#1e293b') => {
697
  if (row > currentGrid.value.rows || col > currentGrid.value.cols) return;
698
  const cells = pages.value[pageIndex].cells;
699
  const index = (row - 1) * currentGrid.value.cols + (col - 1);
@@ -702,14 +747,22 @@
702
  cells[index].content = content;
703
  cells[index].rotation = rotation;
704
  cells[index].color = color;
 
705
  }
706
  };
707
 
708
  const applyColor = (color) => {
709
- if (selectedCellIndex.value === null) return;
710
- const cell = activePageCells.value[selectedCellIndex.value];
711
- cell.color = color;
712
  selectedColor.value = color;
 
 
 
 
 
 
 
 
 
 
713
  };
714
 
715
  // --- Image Upload Logic ---
@@ -766,10 +819,11 @@
766
  };
767
 
768
  const applyCustomImageToCell = (imgSrc) => {
769
- if (selectedCellIndex.value === null) return;
770
- const cell = activePageCells.value[selectedCellIndex.value];
771
- cell.type = 'image';
772
- cell.content = imgSrc;
 
773
  inputBuffer.value = '';
774
  };
775
 
@@ -783,7 +837,7 @@
783
 
784
  const exportProject = () => {
785
  const projectData = {
786
- version: '1.5',
787
  timestamp: new Date().toISOString(),
788
  grid: currentGrid.value,
789
  pages: pages.value,
@@ -823,7 +877,7 @@
823
  customImages.value = importedData.customImages;
824
  }
825
 
826
- selectedCellIndex.value = null;
827
  activePageId.value = 1;
828
  viewMode.value = 'overview';
829
 
@@ -857,33 +911,33 @@
857
  // Page 1
858
  setCell(0, 1, 3, 'text', '最', 180);
859
  setCell(0, 1, 4, 'text', '是', 180);
860
- setCell(0, 2, 3, 'icon', '幸運草', 0);
861
- setCell(0, 2, 4, 'icon', '幸運草', 0);
862
- setCell(0, 3, 3, 'icon', '幸運草', 0);
863
- setCell(0, 3, 4, 'icon', '幸運草', 0);
864
  setCell(0, 3, 5, 'text', '遇', 0);
865
  setCell(0, 3, 6, 'text', '幸', 0);
866
- setCell(0, 6, 3, 'icon', '幸運草', 0);
867
- setCell(0, 6, 4, 'icon', '幸運草', 0);
868
  setCell(0, 6, 5, 'text', '見', 0);
869
  setCell(0, 6, 6, 'text', '運', 0);
870
- setCell(0, 7, 3, 'icon', '幸運草', 0);
871
- setCell(0, 7, 4, 'icon', '幸運草', 0);
872
  setCell(0, 8, 3, 'text', '就', 180);
873
  setCell(0, 8, 4, 'text', '你', 180);
874
 
875
  // Page 2
876
  setCell(1, 1, 6, 'text', '茫', 180);
877
- setCell(1, 2, 6, 'icon', '幸運草', 0);
878
  setCell(1, 5, 5, 'text', '茫', 0);
879
- setCell(1, 5, 6, 'icon', '幸運草', 0);
880
  setCell(1, 6, 5, 'text', '人', 0);
881
- setCell(1, 6, 6, 'icon', '幸運草', 0);
882
- setCell(1, 7, 6, 'icon', '幸運草', 0);
883
  setCell(1, 8, 6, 'text', '海', 180);
884
  }
885
 
886
- selectedCellIndex.value = null;
887
  activePageId.value = 1;
888
  viewMode.value = 'overview';
889
 
@@ -898,54 +952,95 @@
898
  const switchToPage = (pageId) => {
899
  activePageId.value = pageId;
900
  viewMode.value = 'edit';
901
- selectedCellIndex.value = null;
902
  inputBuffer.value = '';
903
  };
904
 
905
- const handleCellClick = (index) => {
906
- if (selectedCellIndex.value !== index) {
907
- selectedCellIndex.value = index;
908
- const cell = activePageCells.value[index];
909
- inputBuffer.value = (cell.type === 'text') ? cell.content : '';
910
- selectedColor.value = cell.color; // Sync color picker
911
- nextTick(() => { if (textInputRef.value) textInputRef.value.focus(); });
912
- } else {
913
- rotateCurrentCell();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
914
  }
915
  };
916
 
917
  const rotateCurrentCell = () => {
918
- if (selectedCellIndex.value === null) return;
919
- const cell = activePageCells.value[selectedCellIndex.value];
920
- cell.rotation = (cell.rotation + 90) % 360;
 
 
921
  };
922
 
923
  const updateSelectedCellText = () => {
924
- if (selectedCellIndex.value === null) return;
925
- const cell = activePageCells.value[selectedCellIndex.value];
926
- cell.type = 'text';
927
- cell.content = inputBuffer.value;
928
- cell.color = selectedColor.value; // Apply current color
 
 
929
  };
930
 
931
  const applyIconToCell = (iconName) => {
932
- if (selectedCellIndex.value === null) return;
933
- const cell = activePageCells.value[selectedCellIndex.value];
934
- cell.type = 'icon';
935
- cell.content = iconName;
936
- cell.color = selectedColor.value; // Apply current color (though emoji mostly ignores it)
 
 
937
  inputBuffer.value = '';
938
  };
939
 
940
  const clearCurrentCell = () => {
941
- if (selectedCellIndex.value === null) return;
942
- const cell = activePageCells.value[selectedCellIndex.value];
943
- cell.content = '';
944
- cell.type = 'text';
945
- cell.rotation = 0;
946
- cell.color = '#1e293b'; // Reset color
 
 
 
947
  inputBuffer.value = '';
948
  selectedColor.value = '#1e293b';
 
949
  };
950
 
951
  // --- Auto Save & Init ---
@@ -983,7 +1078,7 @@
983
  });
984
 
985
  // Helper to replace text element with SVG for perfect centering
986
- const replaceWithSvgText = (el, content, color, fontFamily, fontWeight) => {
987
  const ns = "http://www.w3.org/2000/svg";
988
  const svg = document.createElementNS(ns, "svg");
989
  svg.setAttribute("width", "100%");
@@ -1001,7 +1096,7 @@
1001
  textNode.setAttribute("fill", color);
1002
  textNode.setAttribute("font-family", fontFamily);
1003
  textNode.setAttribute("font-weight", fontWeight);
1004
- textNode.setAttribute("font-size", "60");
1005
  textNode.textContent = content;
1006
 
1007
  svg.appendChild(textNode);
@@ -1031,22 +1126,22 @@
1031
 
1032
  pageData.cells.forEach((cell, idx) => {
1033
  let contentHtml = '';
 
 
 
1034
  if (cell.type === 'text') {
1035
  // Text uses the SVG text replacement trick for perfect centering
1036
- 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>`;
1037
  } else if (cell.type === 'icon') {
1038
  // Icon (Emoji) -> Treat as Text for Export to fix offset
1039
- // We use the same 'export-text' class so it gets replaced by SVG text
1040
  const iconChar = icons[cell.content];
1041
- contentHtml = `<span class="export-text" data-color="${cell.color}" style="font-size: 50px; line-height: 1; user-select: none;">${iconChar}</span>`;
1042
  } else if (cell.type === 'image') {
1043
  contentHtml = `<img src="${cell.content}" style="width: 80%; height: 80%; object-fit: contain;">`;
1044
  }
1045
 
1046
- // Coordinate span REMOVED here
1047
-
1048
  gridHtml += `
1049
- <div class="grid-cell" style="position: relative; border: 1px solid #cbd5e1; display: flex; align-items: center; justify-content: center; overflow: hidden;">
1050
  <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; transform: rotate(${cell.rotation}deg);">
1051
  ${contentHtml}
1052
  </div>
@@ -1070,8 +1165,9 @@
1070
  textElements.forEach(el => {
1071
  const textContent = el.innerText;
1072
  const color = el.getAttribute('data-color');
 
1073
  if (!textContent) return;
1074
- replaceWithSvgText(el, textContent, color, "'Noto Sans TC', sans-serif", "bold");
1075
  });
1076
  }
1077
  });
@@ -1080,7 +1176,7 @@
1080
  };
1081
 
1082
  const exportPDF = async () => {
1083
- if (selectedCellIndex.value !== null) selectedCellIndex.value = null;
1084
  isGenerating.value = true;
1085
 
1086
  try {
@@ -1117,12 +1213,15 @@
1117
  viewMode,
1118
  activePageId,
1119
  activePageCells,
1120
- selectedCellIndex,
1121
  inputBuffer,
1122
  icons,
1123
  colors,
 
1124
  selectedColor,
 
1125
  applyColor,
 
1126
  textInputRef,
1127
  isGenerating,
1128
  switchToPage,
@@ -1154,7 +1253,8 @@
1154
  customGridConfig,
1155
  openCustomGridModal,
1156
  confirmCustomGrid,
1157
- isCustomGridActive
 
1158
  };
1159
  }
1160
  }).mount('#app');
 
115
  <div class="p-6 border-b border-slate-100 bg-slate-50">
116
  <div class="flex justify-between items-center mb-2">
117
  <h1 class="text-2xl font-bold text-indigo-600 flex items-center gap-2">
 
118
  <span class="text-3xl">🎩</span>
119
  摺紙魔術設計師
120
  </h1>
 
171
  @click="clearAllContent"
172
  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"
173
  >
174
+ <i class="fa-solid fa-trash-can mr-1"></i>
 
 
 
175
  一鍵清空所有內容
176
  </button>
177
  </div>
 
197
  @click="exportProject"
198
  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"
199
  >
200
+ <i class="fa-solid fa-download mr-1"></i>
 
201
  匯出存檔
202
  </button>
203
  <button
204
  @click="triggerImport"
205
  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"
206
  >
207
+ <i class="fa-solid fa-upload mr-1"></i>
 
208
  匯入舊檔
209
  </button>
210
  <input
 
224
  <li v-if="viewMode === 'overview'"><strong>點擊頁面:</strong>進入該頁編輯模式</li>
225
  <li v-else><strong>目前編輯:</strong>第 {{ activePageId }} 頁</li>
226
  <li><strong>點擊格子:</strong>選取編輯</li>
227
+ <li><strong>Ctrl + 點擊:</strong>可多選格子一起編輯</li>
228
+ <li><strong>再次點擊:</strong>單選時旋轉 90°</li>
229
  </ul>
230
  </div>
231
 
232
  <!-- Editing Controls -->
233
  <div v-if="viewMode === 'edit'" class="space-y-8 animate-fade-in pb-10">
234
 
235
+ <!-- Selection Summary -->
236
+ <div v-if="selectedCellIndices.size > 1" class="bg-indigo-100 px-4 py-2 rounded-lg text-indigo-700 font-bold text-sm text-center">
237
+ 已選取 {{ selectedCellIndices.size }} 個格子
238
+ </div>
239
+
240
+ <!-- Colors -->
241
+ <div class="space-y-4">
242
+ <!-- Text Color -->
243
+ <div>
244
+ <label class="block text-sm font-bold text-slate-700 mb-2">1. 文字/圖示顏色</label>
245
+ <div class="flex flex-wrap gap-2">
246
+ <button
247
+ v-for="color in colors"
248
+ :key="color"
249
+ @click="applyColor(color)"
250
+ :disabled="selectedCellIndices.size === 0"
251
+ class="w-8 h-8 rounded-full border-2 transition-all shadow-sm active:scale-95 hover:scale-110"
252
+ :class="{'ring-2 ring-indigo-400 ring-offset-2': selectedColor === color && selectedCellIndices.size > 0, 'opacity-50 cursor-not-allowed': selectedCellIndices.size === 0}"
253
+ :style="{ backgroundColor: color, borderColor: color === '#ffffff' ? '#e2e8f0' : 'transparent' }"
254
+ ></button>
255
+ </div>
256
+ </div>
257
+ <!-- Background Color (Macaron) -->
258
+ <div>
259
+ <label class="block text-sm font-bold text-slate-700 mb-2">2. 背景填充 (淺色系)</label>
260
+ <div class="flex flex-wrap gap-2">
261
+ <button
262
+ v-for="bgColor in bgColors"
263
+ :key="bgColor"
264
+ @click="applyBgColor(bgColor)"
265
+ :disabled="selectedCellIndices.size === 0"
266
+ class="w-8 h-8 rounded border transition-all shadow-sm active:scale-95 hover:scale-110 relative"
267
+ :class="{'ring-2 ring-indigo-400 ring-offset-2': selectedBgColor === bgColor && selectedCellIndices.size > 0, 'opacity-50 cursor-not-allowed': selectedCellIndices.size === 0}"
268
+ :style="{ backgroundColor: bgColor, borderColor: '#e2e8f0' }"
269
+ >
270
+ <!-- Slash for transparent/white -->
271
+ <div v-if="bgColor === '#ffffff'" class="absolute inset-0 flex items-center justify-center pointer-events-none">
272
+ <div class="w-full h-px bg-slate-300 transform rotate-45"></div>
273
+ </div>
274
+ </button>
275
+ </div>
276
  </div>
277
  </div>
278
 
279
  <!-- Text Input -->
280
  <div>
281
+ <label class="block text-sm font-bold text-slate-700 mb-2">3. 文字輸入 (最多3字)</label>
282
  <input
283
  ref="textInputRef"
284
  type="text"
285
  v-model="inputBuffer"
286
  @input="updateSelectedCellText"
287
  placeholder="先點選右側格子..."
288
+ maxlength="3"
289
+ :disabled="selectedCellIndices.size === 0"
290
  class="w-full text-center text-2xl p-3 border-2 rounded-lg focus:outline-none focus:ring-2 transition-all"
291
+ :class="selectedCellIndices.size === 0 ? 'bg-slate-100 border-slate-200 cursor-not-allowed' : 'bg-white border-indigo-300 focus:border-indigo-500 focus:ring-indigo-200'"
292
+ :style="{ color: selectedCellIndices.size > 0 ? selectedColor : '' }"
293
  >
294
+ <p class="text-xs text-slate-400 mt-1">提示:輸入 2~3 個字或數字時,字體會自動縮小。</p>
295
  </div>
296
 
297
  <!-- Icon Picker -->
298
  <div>
299
+ <label class="block text-sm font-bold text-slate-700 mb-3">4. 選擇圖示</label>
300
  <div class="grid grid-cols-4 gap-2 max-h-60 overflow-y-auto p-1 custom-scrollbar">
301
  <button
302
  v-for="(val, name) in icons"
303
  :key="name"
304
  @click="applyIconToCell(name)"
305
+ :disabled="selectedCellIndices.size === 0"
306
  class="aspect-square flex items-center justify-center rounded-lg border hover:bg-indigo-50 active:scale-95 transition-all"
307
+ :class="selectedCellIndices.size === 0 ? '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'"
308
  :title="name"
309
  >
310
  <span class="text-3xl leading-none select-none">{{ val }}</span>
 
315
  <!-- Image Upload -->
316
  <div>
317
  <div class="flex justify-between items-center mb-3">
318
+ <label class="block text-sm font-bold text-slate-700">5. 自定義圖片</label>
319
  <button
320
  @click="triggerImageUpload"
321
  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"
 
334
  v-for="(imgSrc, idx) in customImages"
335
  :key="idx"
336
  @click="applyCustomImageToCell(imgSrc)"
337
+ :disabled="selectedCellIndices.size === 0"
338
  class="aspect-square flex items-center justify-center rounded-lg border overflow-hidden relative hover:opacity-90 active:scale-95 transition-all bg-white"
339
+ :class="selectedCellIndices.size === 0 ? 'border-slate-200 cursor-not-allowed opacity-50' : 'border-slate-300 cursor-pointer hover:border-indigo-400 ring-offset-1'"
340
  >
341
  <img :src="imgSrc" class="w-full h-full object-cover">
342
  <!-- Delete Button -->
 
346
  </div>
347
 
348
  <!-- Cell Info -->
349
+ <div v-if="selectedCellIndices.size > 0" class="bg-slate-100 p-4 rounded-lg flex justify-between items-center">
350
+ <div class="text-sm font-bold text-slate-600">
351
+ 已選 {{ selectedCellIndices.size }}
 
352
  </div>
353
  <button @click="rotateCurrentCell" class="px-3 py-1 bg-white border border-slate-300 rounded shadow-sm text-sm">
354
+ 旋轉選取項目 (90°)
355
  </button>
356
  </div>
357
 
358
  <div>
359
  <button
360
  @click="clearCurrentCell"
361
+ :disabled="selectedCellIndices.size === 0"
362
  class="w-full py-2 text-sm text-red-500 border border-red-200 rounded hover:bg-red-50 disabled:opacity-50 disabled:cursor-not-allowed"
363
  >
364
  清除內容
 
465
  v-for="(cell, index) in pages[pageId-1].cells"
466
  :key="cell.id"
467
  class="grid-cell relative border border-slate-300"
468
+ :style="{ backgroundColor: cell.bgColor }"
469
  >
470
  <div class="rotation-wrapper" :style="{ transform: `rotate(${cell.rotation}deg)` }">
471
+ <span v-if="cell.type === 'text'" class="font-bold cell-text" :class="getFontSizeClass(cell.content)" :style="{ color: cell.color }">{{ cell.content }}</span>
472
 
473
  <!-- Render Icon: Emoji -->
474
  <span v-if="cell.type === 'icon'" class="text-3xl leading-none select-none">{{ icons[cell.content] }}</span>
 
510
  <div
511
  v-for="(cell, index) in activePageCells"
512
  :key="cell.id"
513
+ @click="handleCellClick(index, $event)"
514
  class="grid-cell relative border border-slate-300 cursor-pointer hover:bg-slate-50 select-none"
515
+ :class="{ 'ring-4 ring-indigo-500 ring-inset z-20': selectedCellIndices.has(index) }"
516
+ :style="{ backgroundColor: cell.bgColor }"
517
  >
518
  <div class="rotation-wrapper pointer-events-none" :style="{ transform: `rotate(${cell.rotation}deg)` }">
519
+ <span v-if="cell.type === 'text'" class="font-bold cell-text block" :class="getFontSizeClass(cell.content)" :style="{ color: cell.color }">
520
  {{ cell.content }}
521
  </span>
522
 
 
565
 
566
  // --- Colors ---
567
  const colors = ['#1e293b', '#ef4444', '#f97316', '#f59e0b', '#22c55e', '#14b8a6', '#3b82f6', '#6366f1', '#a855f7', '#ec4899'];
568
+
569
+ // --- Background Colors (Macaron) ---
570
+ const bgColors = ['#ffffff', '#fecaca', '#fed7aa', '#fef08a', '#bbf7d0', '#a5f3fc', '#bfdbfe', '#ddd6fe', '#fbcfe8', '#e2e8f0'];
571
 
572
  // --- Icons (Unified to Unicode Emoji) ---
573
  const icons = {
 
596
  type: 'text',
597
  content: '',
598
  rotation: 0,
599
+ color: '#1e293b', // Default text color
600
+ bgColor: '#ffffff' // Default bg color
601
  }));
602
 
603
  const pages = ref([
 
607
 
608
  const viewMode = ref('overview');
609
  const activePageId = ref(1);
610
+
611
+ // Replaced single index with Set for multi-select
612
+ const selectedCellIndices = ref(new Set());
613
+
614
  const inputBuffer = ref('');
615
  const selectedColor = ref('#1e293b');
616
+ const selectedBgColor = ref('#ffffff');
617
  const isGenerating = ref(false);
618
 
619
  // Refs
 
637
 
638
  const activePageCells = computed(() => pages.value[activePageId.value - 1].cells);
639
 
 
640
  const isCustomGridActive = computed(() => {
 
641
  return !gridOptions.some(opt => opt.label === currentGrid.value.label);
642
  });
643
 
644
+ // --- Helper: Dynamic Font Size ---
645
+ const getFontSizeClass = (content) => {
646
+ const len = content ? content.length : 0;
647
+ if (len >= 3) return 'text-3xl md:text-5xl';
648
+ if (len === 2) return 'text-4xl md:text-6xl';
649
+ return 'text-5xl md:text-7xl';
650
+ };
651
+
652
+ const getFontSizeForPdf = (content) => {
653
+ const len = content ? content.length : 0;
654
+ if (len >= 3) return "40";
655
+ if (len === 2) return "50";
656
+ return "60";
657
+ };
658
+
659
  // --- Actions ---
660
 
661
  const changeGridSize = (conf) => {
 
674
  { id: 2, cells: createPageCells(totalCells, conf.rows, conf.cols) }
675
  ];
676
 
677
+ selectedCellIndices.value.clear();
678
  activePageId.value = 1;
679
  viewMode.value = 'overview';
680
  };
 
686
  if(!confirm("設定自訂網格將會清空您目前的設計,確定要��續嗎?")) return;
687
  }
688
 
 
689
  customGridConfig.value.rows = currentGrid.value.rows;
690
  customGridConfig.value.cols = currentGrid.value.cols;
691
  showGridModal.value = true;
 
709
  { id: 2, cells: createPageCells(totalCells, r, c) }
710
  ];
711
 
712
+ selectedCellIndices.value.clear();
713
  activePageId.value = 1;
714
  viewMode.value = 'overview';
715
  showGridModal.value = false;
 
732
  { id: 2, cells: createPageCells(totalCells, rows, cols) }
733
  ];
734
 
735
+ selectedCellIndices.value.clear();
736
  inputBuffer.value = '';
737
  alert("內容已清空!");
738
  }
739
  };
740
 
741
+ const setCell = (pageIndex, row, col, type, content, rotation = 0, color = '#1e293b', bgColor='#ffffff') => {
742
  if (row > currentGrid.value.rows || col > currentGrid.value.cols) return;
743
  const cells = pages.value[pageIndex].cells;
744
  const index = (row - 1) * currentGrid.value.cols + (col - 1);
 
747
  cells[index].content = content;
748
  cells[index].rotation = rotation;
749
  cells[index].color = color;
750
+ cells[index].bgColor = bgColor;
751
  }
752
  };
753
 
754
  const applyColor = (color) => {
 
 
 
755
  selectedColor.value = color;
756
+ selectedCellIndices.value.forEach(index => {
757
+ activePageCells.value[index].color = color;
758
+ });
759
+ };
760
+
761
+ const applyBgColor = (bgColor) => {
762
+ selectedBgColor.value = bgColor;
763
+ selectedCellIndices.value.forEach(index => {
764
+ activePageCells.value[index].bgColor = bgColor;
765
+ });
766
  };
767
 
768
  // --- Image Upload Logic ---
 
819
  };
820
 
821
  const applyCustomImageToCell = (imgSrc) => {
822
+ selectedCellIndices.value.forEach(index => {
823
+ const cell = activePageCells.value[index];
824
+ cell.type = 'image';
825
+ cell.content = imgSrc;
826
+ });
827
  inputBuffer.value = '';
828
  };
829
 
 
837
 
838
  const exportProject = () => {
839
  const projectData = {
840
+ version: '1.6',
841
  timestamp: new Date().toISOString(),
842
  grid: currentGrid.value,
843
  pages: pages.value,
 
877
  customImages.value = importedData.customImages;
878
  }
879
 
880
+ selectedCellIndices.value.clear();
881
  activePageId.value = 1;
882
  viewMode.value = 'overview';
883
 
 
911
  // Page 1
912
  setCell(0, 1, 3, 'text', '最', 180);
913
  setCell(0, 1, 4, 'text', '是', 180);
914
+ setCell(0, 2, 3, 'icon', '幸運草', 0, '#22c55e');
915
+ setCell(0, 2, 4, 'icon', '幸運草', 0, '#22c55e');
916
+ setCell(0, 3, 3, 'icon', '幸運草', 0, '#22c55e');
917
+ setCell(0, 3, 4, 'icon', '幸運草', 0, '#22c55e');
918
  setCell(0, 3, 5, 'text', '遇', 0);
919
  setCell(0, 3, 6, 'text', '幸', 0);
920
+ setCell(0, 6, 3, 'icon', '幸運草', 0, '#22c55e');
921
+ setCell(0, 6, 4, 'icon', '幸運草', 0, '#22c55e');
922
  setCell(0, 6, 5, 'text', '見', 0);
923
  setCell(0, 6, 6, 'text', '運', 0);
924
+ setCell(0, 7, 3, 'icon', '幸運草', 0, '#22c55e');
925
+ setCell(0, 7, 4, 'icon', '幸運草', 0, '#22c55e');
926
  setCell(0, 8, 3, 'text', '就', 180);
927
  setCell(0, 8, 4, 'text', '你', 180);
928
 
929
  // Page 2
930
  setCell(1, 1, 6, 'text', '茫', 180);
931
+ setCell(1, 2, 6, 'icon', '幸運草', 0, '#22c55e');
932
  setCell(1, 5, 5, 'text', '茫', 0);
933
+ setCell(1, 5, 6, 'icon', '幸運草', 0, '#22c55e');
934
  setCell(1, 6, 5, 'text', '人', 0);
935
+ setCell(1, 6, 6, 'icon', '幸運草', 0, '#22c55e');
936
+ setCell(1, 7, 6, 'icon', '幸運草', 0, '#22c55e');
937
  setCell(1, 8, 6, 'text', '海', 180);
938
  }
939
 
940
+ selectedCellIndices.value.clear();
941
  activePageId.value = 1;
942
  viewMode.value = 'overview';
943
 
 
952
  const switchToPage = (pageId) => {
953
  activePageId.value = pageId;
954
  viewMode.value = 'edit';
955
+ selectedCellIndices.value.clear();
956
  inputBuffer.value = '';
957
  };
958
 
959
+ // Enhanced Click Handler for Multi-select
960
+ const handleCellClick = (index, event) => {
961
+ // If Ctrl/Meta key is pressed, toggle selection
962
+ if (event && (event.ctrlKey || event.metaKey)) {
963
+ if (selectedCellIndices.value.has(index)) {
964
+ selectedCellIndices.value.delete(index);
965
+ } else {
966
+ selectedCellIndices.value.add(index);
967
+ }
968
+
969
+ // If we have single selection after toggle, setup buffer
970
+ if (selectedCellIndices.value.size === 1) {
971
+ const idx = [...selectedCellIndices.value][0];
972
+ const cell = activePageCells.value[idx];
973
+ inputBuffer.value = (cell.type === 'text') ? cell.content : '';
974
+ selectedColor.value = cell.color;
975
+ selectedBgColor.value = cell.bgColor;
976
+ nextTick(() => { if (textInputRef.value) textInputRef.value.focus(); });
977
+ } else {
978
+ inputBuffer.value = ''; // Multi-select clears buffer to avoid confusion
979
+ }
980
+ }
981
+ else {
982
+ // Normal click without modifiers
983
+
984
+ // If clicking an already selected cell when it is the ONLY selected cell -> Rotate
985
+ if (selectedCellIndices.value.has(index) && selectedCellIndices.value.size === 1) {
986
+ rotateCurrentCell();
987
+ }
988
+ else {
989
+ // Otherwise, reset selection to just this one
990
+ selectedCellIndices.value.clear();
991
+ selectedCellIndices.value.add(index);
992
+
993
+ const cell = activePageCells.value[index];
994
+ inputBuffer.value = (cell.type === 'text') ? cell.content : '';
995
+ selectedColor.value = cell.color;
996
+ selectedBgColor.value = cell.bgColor;
997
+ nextTick(() => { if (textInputRef.value) textInputRef.value.focus(); });
998
+ }
999
  }
1000
  };
1001
 
1002
  const rotateCurrentCell = () => {
1003
+ if (selectedCellIndices.value.size === 0) return;
1004
+ selectedCellIndices.value.forEach(index => {
1005
+ const cell = activePageCells.value[index];
1006
+ cell.rotation = (cell.rotation + 90) % 360;
1007
+ });
1008
  };
1009
 
1010
  const updateSelectedCellText = () => {
1011
+ if (selectedCellIndices.value.size === 0) return;
1012
+ selectedCellIndices.value.forEach(index => {
1013
+ const cell = activePageCells.value[index];
1014
+ cell.type = 'text';
1015
+ cell.content = inputBuffer.value;
1016
+ cell.color = selectedColor.value;
1017
+ });
1018
  };
1019
 
1020
  const applyIconToCell = (iconName) => {
1021
+ if (selectedCellIndices.value.size === 0) return;
1022
+ selectedCellIndices.value.forEach(index => {
1023
+ const cell = activePageCells.value[index];
1024
+ cell.type = 'icon';
1025
+ cell.content = iconName;
1026
+ cell.color = selectedColor.value;
1027
+ });
1028
  inputBuffer.value = '';
1029
  };
1030
 
1031
  const clearCurrentCell = () => {
1032
+ if (selectedCellIndices.value.size === 0) return;
1033
+ selectedCellIndices.value.forEach(index => {
1034
+ const cell = activePageCells.value[index];
1035
+ cell.content = '';
1036
+ cell.type = 'text';
1037
+ cell.rotation = 0;
1038
+ cell.color = '#1e293b';
1039
+ cell.bgColor = '#ffffff'; // Reset BG
1040
+ });
1041
  inputBuffer.value = '';
1042
  selectedColor.value = '#1e293b';
1043
+ selectedBgColor.value = '#ffffff';
1044
  };
1045
 
1046
  // --- Auto Save & Init ---
 
1078
  });
1079
 
1080
  // Helper to replace text element with SVG for perfect centering
1081
+ const replaceWithSvgText = (el, content, color, fontFamily, fontWeight, fontSize) => {
1082
  const ns = "http://www.w3.org/2000/svg";
1083
  const svg = document.createElementNS(ns, "svg");
1084
  svg.setAttribute("width", "100%");
 
1096
  textNode.setAttribute("fill", color);
1097
  textNode.setAttribute("font-family", fontFamily);
1098
  textNode.setAttribute("font-weight", fontWeight);
1099
+ textNode.setAttribute("font-size", fontSize);
1100
  textNode.textContent = content;
1101
 
1102
  svg.appendChild(textNode);
 
1126
 
1127
  pageData.cells.forEach((cell, idx) => {
1128
  let contentHtml = '';
1129
+ // Dynamic Font Size for Export
1130
+ const fontSize = getFontSizeForPdf(cell.content);
1131
+
1132
  if (cell.type === 'text') {
1133
  // Text uses the SVG text replacement trick for perfect centering
1134
+ contentHtml = `<span class="export-text" data-color="${cell.color}" data-size="${fontSize}" style="font-size: ${fontSize}px; font-weight: bold; color: ${cell.color}; font-family: 'Noto Sans TC', sans-serif;">${cell.content}</span>`;
1135
  } else if (cell.type === 'icon') {
1136
  // Icon (Emoji) -> Treat as Text for Export to fix offset
 
1137
  const iconChar = icons[cell.content];
1138
+ contentHtml = `<span class="export-text" data-color="${cell.color}" data-size="50" style="font-size: 50px; line-height: 1; user-select: none;">${iconChar}</span>`;
1139
  } else if (cell.type === 'image') {
1140
  contentHtml = `<img src="${cell.content}" style="width: 80%; height: 80%; object-fit: contain;">`;
1141
  }
1142
 
 
 
1143
  gridHtml += `
1144
+ <div class="grid-cell" style="position: relative; border: 1px solid #cbd5e1; background-color: ${cell.bgColor}; display: flex; align-items: center; justify-content: center; overflow: hidden;">
1145
  <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; transform: rotate(${cell.rotation}deg);">
1146
  ${contentHtml}
1147
  </div>
 
1165
  textElements.forEach(el => {
1166
  const textContent = el.innerText;
1167
  const color = el.getAttribute('data-color');
1168
+ const size = el.getAttribute('data-size') || "60";
1169
  if (!textContent) return;
1170
+ replaceWithSvgText(el, textContent, color, "'Noto Sans TC', sans-serif", "bold", size);
1171
  });
1172
  }
1173
  });
 
1176
  };
1177
 
1178
  const exportPDF = async () => {
1179
+ if (selectedCellIndices.value.size > 0) selectedCellIndices.value.clear();
1180
  isGenerating.value = true;
1181
 
1182
  try {
 
1213
  viewMode,
1214
  activePageId,
1215
  activePageCells,
1216
+ selectedCellIndices,
1217
  inputBuffer,
1218
  icons,
1219
  colors,
1220
+ bgColors,
1221
  selectedColor,
1222
+ selectedBgColor,
1223
  applyColor,
1224
+ applyBgColor,
1225
  textInputRef,
1226
  isGenerating,
1227
  switchToPage,
 
1253
  customGridConfig,
1254
  openCustomGridModal,
1255
  confirmCustomGrid,
1256
+ isCustomGridActive,
1257
+ getFontSizeClass
1258
  };
1259
  }
1260
  }).mount('#app');