Update index.html
Browse files- index.html +211 -35
index.html
CHANGED
|
@@ -224,7 +224,7 @@
|
|
| 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
|
| 228 |
<li><strong>再次點擊:</strong>單選時旋轉 90°</li>
|
| 229 |
</ul>
|
| 230 |
</div>
|
|
@@ -232,9 +232,39 @@
|
|
| 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 >
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
</div>
|
| 239 |
|
| 240 |
<!-- Colors -->
|
|
@@ -278,8 +308,24 @@
|
|
| 278 |
|
| 279 |
<!-- Text Input -->
|
| 280 |
<div>
|
| 281 |
-
<label class="block text-sm font-bold text-slate-700 mb-2">3. 文字輸入 (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
<input
|
|
|
|
| 283 |
ref="textInputRef"
|
| 284 |
type="text"
|
| 285 |
v-model="inputBuffer"
|
|
@@ -291,7 +337,8 @@
|
|
| 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 -->
|
|
@@ -345,16 +392,6 @@
|
|
| 345 |
</div>
|
| 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"
|
|
@@ -375,7 +412,7 @@
|
|
| 375 |
<!-- Footer Action -->
|
| 376 |
<div class="p-6 border-t border-slate-200 bg-slate-50">
|
| 377 |
<button
|
| 378 |
-
@click="
|
| 379 |
:disabled="isGenerating"
|
| 380 |
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"
|
| 381 |
>
|
|
@@ -435,6 +472,35 @@
|
|
| 435 |
</div>
|
| 436 |
</div>
|
| 437 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
</div>
|
| 439 |
|
| 440 |
<!-- =======================
|
|
@@ -540,6 +606,14 @@
|
|
| 540 |
<a href="https://www.facebook.com/groups/1554372228718393" target="_blank" class="text-indigo-500 hover:text-indigo-700 font-bold hover:underline">萬物皆數</a>、
|
| 541 |
<a href="https://www.facebook.com/groups/108923286120994" target="_blank" class="text-indigo-500 hover:text-indigo-700 font-bold hover:underline">藝數摺學</a>
|
| 542 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
</div>
|
| 544 |
</div>
|
| 545 |
|
|
@@ -609,15 +683,19 @@
|
|
| 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
|
| 620 |
const textInputRef = ref(null);
|
|
|
|
| 621 |
const fileInputRef = ref(null);
|
| 622 |
const imageUploadInput = ref(null);
|
| 623 |
const cropperImgRef = ref(null);
|
|
@@ -626,6 +704,11 @@
|
|
| 626 |
const showGridModal = ref(false);
|
| 627 |
const customGridConfig = ref({ rows: 10, cols: 10 });
|
| 628 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
// Image Upload & Crop State
|
| 630 |
const showCropper = ref(false);
|
| 631 |
const tempImageSrc = ref('');
|
|
@@ -681,7 +764,7 @@
|
|
| 681 |
{ id: 2, cells: createPageCells(totalCells, conf.rows, conf.cols) }
|
| 682 |
];
|
| 683 |
|
| 684 |
-
|
| 685 |
activePageId.value = 1;
|
| 686 |
viewMode.value = 'overview';
|
| 687 |
};
|
|
@@ -716,7 +799,7 @@
|
|
| 716 |
{ id: 2, cells: createPageCells(totalCells, r, c) }
|
| 717 |
];
|
| 718 |
|
| 719 |
-
|
| 720 |
activePageId.value = 1;
|
| 721 |
viewMode.value = 'overview';
|
| 722 |
showGridModal.value = false;
|
|
@@ -739,8 +822,7 @@
|
|
| 739 |
{ id: 2, cells: createPageCells(totalCells, rows, cols) }
|
| 740 |
];
|
| 741 |
|
| 742 |
-
|
| 743 |
-
inputBuffer.value = '';
|
| 744 |
alert("內容已清空!");
|
| 745 |
}
|
| 746 |
};
|
|
@@ -832,6 +914,7 @@
|
|
| 832 |
cell.content = imgSrc;
|
| 833 |
});
|
| 834 |
inputBuffer.value = '';
|
|
|
|
| 835 |
};
|
| 836 |
|
| 837 |
const removeCustomImage = (idx) => {
|
|
@@ -884,7 +967,7 @@
|
|
| 884 |
customImages.value = importedData.customImages;
|
| 885 |
}
|
| 886 |
|
| 887 |
-
|
| 888 |
activePageId.value = 1;
|
| 889 |
viewMode.value = 'overview';
|
| 890 |
|
|
@@ -944,7 +1027,7 @@
|
|
| 944 |
setCell(1, 8, 6, 'text', '海', 180);
|
| 945 |
}
|
| 946 |
|
| 947 |
-
|
| 948 |
activePageId.value = 1;
|
| 949 |
viewMode.value = 'overview';
|
| 950 |
|
|
@@ -959,21 +1042,31 @@
|
|
| 959 |
const switchToPage = (pageId) => {
|
| 960 |
activePageId.value = pageId;
|
| 961 |
viewMode.value = 'edit';
|
| 962 |
-
|
| 963 |
inputBuffer.value = '';
|
| 964 |
};
|
| 965 |
|
| 966 |
-
// Enhanced Click Handler for Multi-select
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 967 |
const handleCellClick = (index, event) => {
|
| 968 |
-
//
|
| 969 |
-
|
|
|
|
|
|
|
| 970 |
if (selectedCellIndices.value.has(index)) {
|
| 971 |
selectedCellIndices.value.delete(index);
|
| 972 |
} else {
|
| 973 |
selectedCellIndices.value.add(index);
|
| 974 |
}
|
| 975 |
|
| 976 |
-
//
|
| 977 |
if (selectedCellIndices.value.size === 1) {
|
| 978 |
const idx = [...selectedCellIndices.value][0];
|
| 979 |
const cell = activePageCells.value[idx];
|
|
@@ -982,18 +1075,19 @@
|
|
| 982 |
selectedBgColor.value = cell.bgColor;
|
| 983 |
nextTick(() => { if (textInputRef.value) textInputRef.value.focus(); });
|
| 984 |
} else {
|
| 985 |
-
inputBuffer.value = '';
|
|
|
|
| 986 |
}
|
| 987 |
}
|
| 988 |
else {
|
| 989 |
-
// Normal
|
| 990 |
|
| 991 |
-
// If clicking an already selected cell
|
| 992 |
if (selectedCellIndices.value.has(index) && selectedCellIndices.value.size === 1) {
|
| 993 |
rotateCurrentCell();
|
| 994 |
}
|
| 995 |
else {
|
| 996 |
-
//
|
| 997 |
selectedCellIndices.value.clear();
|
| 998 |
selectedCellIndices.value.add(index);
|
| 999 |
|
|
@@ -1006,6 +1100,14 @@
|
|
| 1006 |
}
|
| 1007 |
};
|
| 1008 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1009 |
const rotateCurrentCell = () => {
|
| 1010 |
if (selectedCellIndices.value.size === 0) return;
|
| 1011 |
selectedCellIndices.value.forEach(index => {
|
|
@@ -1014,6 +1116,7 @@
|
|
| 1014 |
});
|
| 1015 |
};
|
| 1016 |
|
|
|
|
| 1017 |
const updateSelectedCellText = () => {
|
| 1018 |
if (selectedCellIndices.value.size === 0) return;
|
| 1019 |
selectedCellIndices.value.forEach(index => {
|
|
@@ -1024,6 +1127,34 @@
|
|
| 1024 |
});
|
| 1025 |
};
|
| 1026 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1027 |
const applyIconToCell = (iconName) => {
|
| 1028 |
if (selectedCellIndices.value.size === 0) return;
|
| 1029 |
selectedCellIndices.value.forEach(index => {
|
|
@@ -1033,6 +1164,7 @@
|
|
| 1033 |
cell.color = selectedColor.value;
|
| 1034 |
});
|
| 1035 |
inputBuffer.value = '';
|
|
|
|
| 1036 |
};
|
| 1037 |
|
| 1038 |
const clearCurrentCell = () => {
|
|
@@ -1046,6 +1178,7 @@
|
|
| 1046 |
cell.bgColor = '#ffffff'; // Reset BG
|
| 1047 |
});
|
| 1048 |
inputBuffer.value = '';
|
|
|
|
| 1049 |
selectedColor.value = '#1e293b';
|
| 1050 |
selectedBgColor.value = '#ffffff';
|
| 1051 |
};
|
|
@@ -1115,6 +1248,15 @@
|
|
| 1115 |
};
|
| 1116 |
|
| 1117 |
// ... PDF Export Logic ...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1118 |
const renderPageToCanvas = async (pageId) => {
|
| 1119 |
const pageData = pages.value[pageId - 1];
|
| 1120 |
const rows = currentGrid.value.rows;
|
|
@@ -1157,7 +1299,13 @@
|
|
| 1157 |
});
|
| 1158 |
gridHtml += `</div>`;
|
| 1159 |
|
| 1160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1161 |
|
| 1162 |
wrapper.innerHTML = gridHtml;
|
| 1163 |
container.appendChild(wrapper);
|
|
@@ -1183,9 +1331,24 @@
|
|
| 1183 |
};
|
| 1184 |
|
| 1185 |
const exportPDF = async () => {
|
| 1186 |
-
if (selectedCellIndices.value.size > 0)
|
| 1187 |
isGenerating.value = true;
|
| 1188 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1189 |
try {
|
| 1190 |
const pdf = new jsPDF('p', 'mm', 'a4');
|
| 1191 |
const pdfWidth = 210;
|
|
@@ -1200,7 +1363,7 @@
|
|
| 1200 |
const imgData2 = canvas2.toDataURL('image/jpeg', 0.95);
|
| 1201 |
pdf.addImage(imgData2, 'JPEG', 0, 0, pdfWidth, pdfHeight);
|
| 1202 |
|
| 1203 |
-
pdf.save(
|
| 1204 |
|
| 1205 |
} catch (err) {
|
| 1206 |
console.error(err);
|
|
@@ -1222,6 +1385,7 @@
|
|
| 1222 |
activePageCells,
|
| 1223 |
selectedCellIndices,
|
| 1224 |
inputBuffer,
|
|
|
|
| 1225 |
icons,
|
| 1226 |
colors,
|
| 1227 |
bgColors,
|
|
@@ -1230,11 +1394,13 @@
|
|
| 1230 |
applyColor,
|
| 1231 |
applyBgColor,
|
| 1232 |
textInputRef,
|
|
|
|
| 1233 |
isGenerating,
|
| 1234 |
switchToPage,
|
| 1235 |
handleCellClick,
|
| 1236 |
rotateCurrentCell,
|
| 1237 |
updateSelectedCellText,
|
|
|
|
| 1238 |
applyIconToCell,
|
| 1239 |
clearCurrentCell,
|
| 1240 |
clearAllContent,
|
|
@@ -1262,7 +1428,17 @@
|
|
| 1262 |
confirmCustomGrid,
|
| 1263 |
isCustomGridActive,
|
| 1264 |
getFontSizeClass,
|
| 1265 |
-
getOverviewFontSizeClass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1266 |
};
|
| 1267 |
}
|
| 1268 |
}).mount('#app');
|
|
|
|
| 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>複選模式:</strong>開啟下方開關可連續點擊選取</li>
|
| 228 |
<li><strong>再次點擊:</strong>單選時旋轉 90°</li>
|
| 229 |
</ul>
|
| 230 |
</div>
|
|
|
|
| 232 |
<!-- Editing Controls -->
|
| 233 |
<div v-if="viewMode === 'edit'" class="space-y-8 animate-fade-in pb-10">
|
| 234 |
|
| 235 |
+
<!-- Multi-Select Mode Toggle (NEW) -->
|
| 236 |
+
<div class="flex items-center justify-between bg-white p-3 border border-slate-200 rounded-lg shadow-sm">
|
| 237 |
+
<span class="text-sm font-bold text-slate-700 flex items-center gap-2">
|
| 238 |
+
<i class="fa-solid fa-check-double text-indigo-500"></i>
|
| 239 |
+
複選模式
|
| 240 |
+
</span>
|
| 241 |
+
<button
|
| 242 |
+
@click="toggleMultiSelectMode"
|
| 243 |
+
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
| 244 |
+
:class="isMultiSelectMode ? 'bg-indigo-600' : 'bg-slate-200'"
|
| 245 |
+
>
|
| 246 |
+
<span
|
| 247 |
+
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
| 248 |
+
:class="isMultiSelectMode ? 'translate-x-6' : 'translate-x-1'"
|
| 249 |
+
/>
|
| 250 |
+
</button>
|
| 251 |
+
</div>
|
| 252 |
+
|
| 253 |
<!-- Selection Summary -->
|
| 254 |
+
<div v-if="selectedCellIndices.size > 0" class="bg-indigo-50 px-4 py-3 rounded-lg border border-indigo-100">
|
| 255 |
+
<div class="flex justify-between items-center mb-2">
|
| 256 |
+
<div class="text-sm font-bold text-indigo-800">
|
| 257 |
+
已選取 {{ selectedCellIndices.size }} 個格子
|
| 258 |
+
</div>
|
| 259 |
+
<button @click="clearSelection" class="text-xs text-slate-500 hover:text-red-500 underline">
|
| 260 |
+
取消選取
|
| 261 |
+
</button>
|
| 262 |
+
</div>
|
| 263 |
+
<div class="flex gap-2">
|
| 264 |
+
<button @click="rotateCurrentCell" class="flex-1 px-3 py-1.5 bg-white border border-indigo-200 text-indigo-700 font-bold rounded shadow-sm text-xs hover:bg-indigo-50 transition-all">
|
| 265 |
+
<i class="fa-solid fa-rotate-right mr-1"></i> 旋轉 90°
|
| 266 |
+
</button>
|
| 267 |
+
</div>
|
| 268 |
</div>
|
| 269 |
|
| 270 |
<!-- Colors -->
|
|
|
|
| 308 |
|
| 309 |
<!-- Text Input -->
|
| 310 |
<div>
|
| 311 |
+
<label class="block text-sm font-bold text-slate-700 mb-2">3. 文字輸入 (單格最多3字)</label>
|
| 312 |
+
<div class="flex gap-2 mb-2" v-if="selectedCellIndices.size > 1">
|
| 313 |
+
<input
|
| 314 |
+
ref="batchInputRef"
|
| 315 |
+
type="text"
|
| 316 |
+
v-model="batchInputBuffer"
|
| 317 |
+
placeholder="輸入句子依序填入..."
|
| 318 |
+
class="flex-1 min-w-0 border-2 border-indigo-200 rounded-lg p-2 text-sm focus:border-indigo-500 focus:outline-none"
|
| 319 |
+
>
|
| 320 |
+
<button
|
| 321 |
+
@click="applyBatchText"
|
| 322 |
+
class="px-3 py-2 bg-indigo-600 text-white font-bold rounded-lg text-sm hover:bg-indigo-700 transition-all whitespace-nowrap"
|
| 323 |
+
>
|
| 324 |
+
依序填入
|
| 325 |
+
</button>
|
| 326 |
+
</div>
|
| 327 |
<input
|
| 328 |
+
v-else
|
| 329 |
ref="textInputRef"
|
| 330 |
type="text"
|
| 331 |
v-model="inputBuffer"
|
|
|
|
| 337 |
: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'"
|
| 338 |
:style="{ color: selectedCellIndices.size > 0 ? selectedColor : '' }"
|
| 339 |
>
|
| 340 |
+
<p class="text-xs text-slate-400 mt-1" v-if="selectedCellIndices.size <= 1">提示:輸入 2~3 個字或數字時,字體會自動縮小。</p>
|
| 341 |
+
<p class="text-xs text-indigo-500 mt-1 font-bold" v-else>批次模式:按照點選順序(1→{{selectedCellIndices.size}})依序填入文字。</p>
|
| 342 |
</div>
|
| 343 |
|
| 344 |
<!-- Icon Picker -->
|
|
|
|
| 392 |
</div>
|
| 393 |
</div>
|
| 394 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
<div>
|
| 396 |
<button
|
| 397 |
@click="clearCurrentCell"
|
|
|
|
| 412 |
<!-- Footer Action -->
|
| 413 |
<div class="p-6 border-t border-slate-200 bg-slate-50">
|
| 414 |
<button
|
| 415 |
+
@click="openExportModal"
|
| 416 |
:disabled="isGenerating"
|
| 417 |
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"
|
| 418 |
>
|
|
|
|
| 472 |
</div>
|
| 473 |
</div>
|
| 474 |
|
| 475 |
+
<!-- Export PDF Modal -->
|
| 476 |
+
<div v-if="showExportModal" class="absolute inset-0 z-50 bg-slate-900/50 flex items-center justify-center p-4 animate-fade-in">
|
| 477 |
+
<div class="bg-white rounded-xl shadow-2xl w-full max-w-sm overflow-hidden">
|
| 478 |
+
<div class="bg-indigo-600 px-6 py-4">
|
| 479 |
+
<h3 class="text-lg font-bold text-white flex items-center gap-2">
|
| 480 |
+
<i class="fa-solid fa-print"></i> 匯出 PDF 設定
|
| 481 |
+
</h3>
|
| 482 |
+
</div>
|
| 483 |
+
<div class="p-6 space-y-4">
|
| 484 |
+
<p class="text-sm text-slate-600 mb-2">請輸入資訊以產生專屬浮水印(可略過):</p>
|
| 485 |
+
<div>
|
| 486 |
+
<label class="block text-sm font-bold text-slate-700 mb-1">姓名 (選填)</label>
|
| 487 |
+
<input type="text" v-model="exportName" placeholder="例如:王小明" class="w-full border-2 border-slate-200 rounded-lg p-2 focus:border-indigo-500 focus:outline-none">
|
| 488 |
+
</div>
|
| 489 |
+
<div>
|
| 490 |
+
<label class="block text-sm font-bold text-slate-700 mb-1">作品名稱 (選填)</label>
|
| 491 |
+
<input type="text" v-model="exportProjectName" placeholder="例如:我的幸運草" class="w-full border-2 border-slate-200 rounded-lg p-2 focus:border-indigo-500 focus:outline-none">
|
| 492 |
+
</div>
|
| 493 |
+
</div>
|
| 494 |
+
<div class="bg-slate-50 px-6 py-4 flex justify-end gap-2 border-t border-slate-100">
|
| 495 |
+
<button @click="showExportModal = false" class="px-4 py-2 text-slate-600 font-bold hover:bg-slate-200 rounded-lg transition-all">取消</button>
|
| 496 |
+
<button @click="processExport" class="px-4 py-2 bg-indigo-600 text-white font-bold rounded-lg hover:bg-indigo-700 transition-all shadow-md flex items-center gap-2">
|
| 497 |
+
<span v-if="isGenerating"><i class="fa-solid fa-circle-notch fa-spin"></i> 處理中</span>
|
| 498 |
+
<span v-else>確認下載</span>
|
| 499 |
+
</button>
|
| 500 |
+
</div>
|
| 501 |
+
</div>
|
| 502 |
+
</div>
|
| 503 |
+
|
| 504 |
</div>
|
| 505 |
|
| 506 |
<!-- =======================
|
|
|
|
| 606 |
<a href="https://www.facebook.com/groups/1554372228718393" target="_blank" class="text-indigo-500 hover:text-indigo-700 font-bold hover:underline">萬物皆數</a>、
|
| 607 |
<a href="https://www.facebook.com/groups/108923286120994" target="_blank" class="text-indigo-500 hover:text-indigo-700 font-bold hover:underline">藝數摺學</a>
|
| 608 |
</p>
|
| 609 |
+
<!-- Feedback Link -->
|
| 610 |
+
<p class="mt-2 pt-2 border-t border-slate-200">
|
| 611 |
+
如果您有任何建議或回饋,歡迎至
|
| 612 |
+
<a href="https://padlet.com/hccedu/padlet-5rkzrebd75t7al13" target="_blank" class="text-indigo-500 hover:text-indigo-700 font-bold hover:underline flex items-center gap-1 inline-flex">
|
| 613 |
+
<i class="fa-regular fa-comments"></i> 意見回饋留言板
|
| 614 |
+
</a> 留言。
|
| 615 |
+
<br>若您留下姓名,我們將會把您加入致謝名單中!
|
| 616 |
+
</p>
|
| 617 |
</div>
|
| 618 |
</div>
|
| 619 |
|
|
|
|
| 683 |
const activePageId = ref(1);
|
| 684 |
|
| 685 |
// Replaced single index with Set for multi-select
|
| 686 |
+
// For ordered batch fill, we need to track ORDER
|
| 687 |
const selectedCellIndices = ref(new Set());
|
| 688 |
+
const isMultiSelectMode = ref(false); // Toggle for tablet friendly mode
|
| 689 |
|
| 690 |
const inputBuffer = ref('');
|
| 691 |
+
const batchInputBuffer = ref(''); // For batch string input
|
| 692 |
const selectedColor = ref('#1e293b');
|
| 693 |
const selectedBgColor = ref('#ffffff');
|
| 694 |
const isGenerating = ref(false);
|
| 695 |
|
| 696 |
// Refs
|
| 697 |
const textInputRef = ref(null);
|
| 698 |
+
const batchInputRef = ref(null);
|
| 699 |
const fileInputRef = ref(null);
|
| 700 |
const imageUploadInput = ref(null);
|
| 701 |
const cropperImgRef = ref(null);
|
|
|
|
| 704 |
const showGridModal = ref(false);
|
| 705 |
const customGridConfig = ref({ rows: 10, cols: 10 });
|
| 706 |
|
| 707 |
+
// Export Modal State
|
| 708 |
+
const showExportModal = ref(false);
|
| 709 |
+
const exportName = ref('');
|
| 710 |
+
const exportProjectName = ref('');
|
| 711 |
+
|
| 712 |
// Image Upload & Crop State
|
| 713 |
const showCropper = ref(false);
|
| 714 |
const tempImageSrc = ref('');
|
|
|
|
| 764 |
{ id: 2, cells: createPageCells(totalCells, conf.rows, conf.cols) }
|
| 765 |
];
|
| 766 |
|
| 767 |
+
clearSelection();
|
| 768 |
activePageId.value = 1;
|
| 769 |
viewMode.value = 'overview';
|
| 770 |
};
|
|
|
|
| 799 |
{ id: 2, cells: createPageCells(totalCells, r, c) }
|
| 800 |
];
|
| 801 |
|
| 802 |
+
clearSelection();
|
| 803 |
activePageId.value = 1;
|
| 804 |
viewMode.value = 'overview';
|
| 805 |
showGridModal.value = false;
|
|
|
|
| 822 |
{ id: 2, cells: createPageCells(totalCells, rows, cols) }
|
| 823 |
];
|
| 824 |
|
| 825 |
+
clearSelection();
|
|
|
|
| 826 |
alert("內容已清空!");
|
| 827 |
}
|
| 828 |
};
|
|
|
|
| 914 |
cell.content = imgSrc;
|
| 915 |
});
|
| 916 |
inputBuffer.value = '';
|
| 917 |
+
batchInputBuffer.value = '';
|
| 918 |
};
|
| 919 |
|
| 920 |
const removeCustomImage = (idx) => {
|
|
|
|
| 967 |
customImages.value = importedData.customImages;
|
| 968 |
}
|
| 969 |
|
| 970 |
+
clearSelection();
|
| 971 |
activePageId.value = 1;
|
| 972 |
viewMode.value = 'overview';
|
| 973 |
|
|
|
|
| 1027 |
setCell(1, 8, 6, 'text', '海', 180);
|
| 1028 |
}
|
| 1029 |
|
| 1030 |
+
clearSelection();
|
| 1031 |
activePageId.value = 1;
|
| 1032 |
viewMode.value = 'overview';
|
| 1033 |
|
|
|
|
| 1042 |
const switchToPage = (pageId) => {
|
| 1043 |
activePageId.value = pageId;
|
| 1044 |
viewMode.value = 'edit';
|
| 1045 |
+
clearSelection();
|
| 1046 |
inputBuffer.value = '';
|
| 1047 |
};
|
| 1048 |
|
| 1049 |
+
// Enhanced Click Handler for Multi-select with Toggle Switch
|
| 1050 |
+
const toggleMultiSelectMode = () => {
|
| 1051 |
+
isMultiSelectMode.value = !isMultiSelectMode.value;
|
| 1052 |
+
if (!isMultiSelectMode.value) {
|
| 1053 |
+
// When turning off, if multiple selected, keep them.
|
| 1054 |
+
// Or we could clear. Keeping seems safer.
|
| 1055 |
+
}
|
| 1056 |
+
};
|
| 1057 |
+
|
| 1058 |
const handleCellClick = (index, event) => {
|
| 1059 |
+
// Check if Ctrl key is pressed (override toggle) OR Toggle is ON
|
| 1060 |
+
const isMultiSelect = (event && (event.ctrlKey || event.metaKey)) || isMultiSelectMode.value;
|
| 1061 |
+
|
| 1062 |
+
if (isMultiSelect) {
|
| 1063 |
if (selectedCellIndices.value.has(index)) {
|
| 1064 |
selectedCellIndices.value.delete(index);
|
| 1065 |
} else {
|
| 1066 |
selectedCellIndices.value.add(index);
|
| 1067 |
}
|
| 1068 |
|
| 1069 |
+
// Setup buffer logic similar to before
|
| 1070 |
if (selectedCellIndices.value.size === 1) {
|
| 1071 |
const idx = [...selectedCellIndices.value][0];
|
| 1072 |
const cell = activePageCells.value[idx];
|
|
|
|
| 1075 |
selectedBgColor.value = cell.bgColor;
|
| 1076 |
nextTick(() => { if (textInputRef.value) textInputRef.value.focus(); });
|
| 1077 |
} else {
|
| 1078 |
+
inputBuffer.value = '';
|
| 1079 |
+
batchInputBuffer.value = '';
|
| 1080 |
}
|
| 1081 |
}
|
| 1082 |
else {
|
| 1083 |
+
// Normal Single Click Mode
|
| 1084 |
|
| 1085 |
+
// If clicking an already selected cell (single), Rotate it
|
| 1086 |
if (selectedCellIndices.value.has(index) && selectedCellIndices.value.size === 1) {
|
| 1087 |
rotateCurrentCell();
|
| 1088 |
}
|
| 1089 |
else {
|
| 1090 |
+
// Reset to single selection
|
| 1091 |
selectedCellIndices.value.clear();
|
| 1092 |
selectedCellIndices.value.add(index);
|
| 1093 |
|
|
|
|
| 1100 |
}
|
| 1101 |
};
|
| 1102 |
|
| 1103 |
+
const clearSelection = () => {
|
| 1104 |
+
selectedCellIndices.value.clear();
|
| 1105 |
+
inputBuffer.value = '';
|
| 1106 |
+
batchInputBuffer.value = '';
|
| 1107 |
+
selectedColor.value = '#1e293b';
|
| 1108 |
+
selectedBgColor.value = '#ffffff';
|
| 1109 |
+
};
|
| 1110 |
+
|
| 1111 |
const rotateCurrentCell = () => {
|
| 1112 |
if (selectedCellIndices.value.size === 0) return;
|
| 1113 |
selectedCellIndices.value.forEach(index => {
|
|
|
|
| 1116 |
});
|
| 1117 |
};
|
| 1118 |
|
| 1119 |
+
// Single Cell Text Update
|
| 1120 |
const updateSelectedCellText = () => {
|
| 1121 |
if (selectedCellIndices.value.size === 0) return;
|
| 1122 |
selectedCellIndices.value.forEach(index => {
|
|
|
|
| 1127 |
});
|
| 1128 |
};
|
| 1129 |
|
| 1130 |
+
// Batch Text Update (Sequential)
|
| 1131 |
+
const applyBatchText = () => {
|
| 1132 |
+
const text = batchInputBuffer.value;
|
| 1133 |
+
if (!text || selectedCellIndices.value.size === 0) return;
|
| 1134 |
+
|
| 1135 |
+
// We need to respect the ORDER of selection.
|
| 1136 |
+
// However, Set iteration order is insertion order in JS.
|
| 1137 |
+
// So [...Set] preserves click order.
|
| 1138 |
+
const indices = [...selectedCellIndices.value];
|
| 1139 |
+
const chars = text.split(''); // Split string into chars.
|
| 1140 |
+
// Note: Complex emojis or surrogate pairs might need better splitting if advanced usage,
|
| 1141 |
+
// but for general text split('') is okay-ish, or use [...text] for better unicode support.
|
| 1142 |
+
const charArray = [...text];
|
| 1143 |
+
|
| 1144 |
+
indices.forEach((cellIndex, i) => {
|
| 1145 |
+
if (i < charArray.length) {
|
| 1146 |
+
const cell = activePageCells.value[cellIndex];
|
| 1147 |
+
cell.type = 'text';
|
| 1148 |
+
cell.content = charArray[i];
|
| 1149 |
+
cell.color = selectedColor.value;
|
| 1150 |
+
}
|
| 1151 |
+
});
|
| 1152 |
+
|
| 1153 |
+
// Clear buffers after apply
|
| 1154 |
+
batchInputBuffer.value = '';
|
| 1155 |
+
inputBuffer.value = '';
|
| 1156 |
+
};
|
| 1157 |
+
|
| 1158 |
const applyIconToCell = (iconName) => {
|
| 1159 |
if (selectedCellIndices.value.size === 0) return;
|
| 1160 |
selectedCellIndices.value.forEach(index => {
|
|
|
|
| 1164 |
cell.color = selectedColor.value;
|
| 1165 |
});
|
| 1166 |
inputBuffer.value = '';
|
| 1167 |
+
batchInputBuffer.value = '';
|
| 1168 |
};
|
| 1169 |
|
| 1170 |
const clearCurrentCell = () => {
|
|
|
|
| 1178 |
cell.bgColor = '#ffffff'; // Reset BG
|
| 1179 |
});
|
| 1180 |
inputBuffer.value = '';
|
| 1181 |
+
batchInputBuffer.value = '';
|
| 1182 |
selectedColor.value = '#1e293b';
|
| 1183 |
selectedBgColor.value = '#ffffff';
|
| 1184 |
};
|
|
|
|
| 1248 |
};
|
| 1249 |
|
| 1250 |
// ... PDF Export Logic ...
|
| 1251 |
+
const openExportModal = () => {
|
| 1252 |
+
showExportModal.value = true;
|
| 1253 |
+
};
|
| 1254 |
+
|
| 1255 |
+
const processExport = () => {
|
| 1256 |
+
showExportModal.value = false;
|
| 1257 |
+
exportPDF();
|
| 1258 |
+
};
|
| 1259 |
+
|
| 1260 |
const renderPageToCanvas = async (pageId) => {
|
| 1261 |
const pageData = pages.value[pageId - 1];
|
| 1262 |
const rows = currentGrid.value.rows;
|
|
|
|
| 1299 |
});
|
| 1300 |
gridHtml += `</div>`;
|
| 1301 |
|
| 1302 |
+
// --- Watermark Logic ---
|
| 1303 |
+
if (exportName.value || exportProjectName.value) {
|
| 1304 |
+
const watermarkText = `${exportName.value ? exportName.value + '的' : ''}${exportProjectName.value || ''}`;
|
| 1305 |
+
if (watermarkText) {
|
| 1306 |
+
gridHtml += `<div style="position: absolute; bottom: 15mm; right: 15mm; color: #94a3b8; font-size: 12px; font-family: 'Noto Sans TC', sans-serif;">${watermarkText}</div>`;
|
| 1307 |
+
}
|
| 1308 |
+
}
|
| 1309 |
|
| 1310 |
wrapper.innerHTML = gridHtml;
|
| 1311 |
container.appendChild(wrapper);
|
|
|
|
| 1331 |
};
|
| 1332 |
|
| 1333 |
const exportPDF = async () => {
|
| 1334 |
+
if (selectedCellIndices.value.size > 0) clearSelection();
|
| 1335 |
isGenerating.value = true;
|
| 1336 |
|
| 1337 |
+
// Determine filename
|
| 1338 |
+
let fileName = 'magic-origami-booklet.pdf';
|
| 1339 |
+
if (exportName.value || exportProjectName.value) {
|
| 1340 |
+
const namePart = exportName.value ? exportName.value : '';
|
| 1341 |
+
const projPart = exportProjectName.value ? exportProjectName.value : '';
|
| 1342 |
+
|
| 1343 |
+
if (namePart && projPart) {
|
| 1344 |
+
fileName = `${namePart}的${projPart}.pdf`;
|
| 1345 |
+
} else if (namePart) {
|
| 1346 |
+
fileName = `${namePart}的作品.pdf`;
|
| 1347 |
+
} else if (projPart) {
|
| 1348 |
+
fileName = `${projPart}.pdf`;
|
| 1349 |
+
}
|
| 1350 |
+
}
|
| 1351 |
+
|
| 1352 |
try {
|
| 1353 |
const pdf = new jsPDF('p', 'mm', 'a4');
|
| 1354 |
const pdfWidth = 210;
|
|
|
|
| 1363 |
const imgData2 = canvas2.toDataURL('image/jpeg', 0.95);
|
| 1364 |
pdf.addImage(imgData2, 'JPEG', 0, 0, pdfWidth, pdfHeight);
|
| 1365 |
|
| 1366 |
+
pdf.save(fileName);
|
| 1367 |
|
| 1368 |
} catch (err) {
|
| 1369 |
console.error(err);
|
|
|
|
| 1385 |
activePageCells,
|
| 1386 |
selectedCellIndices,
|
| 1387 |
inputBuffer,
|
| 1388 |
+
batchInputBuffer,
|
| 1389 |
icons,
|
| 1390 |
colors,
|
| 1391 |
bgColors,
|
|
|
|
| 1394 |
applyColor,
|
| 1395 |
applyBgColor,
|
| 1396 |
textInputRef,
|
| 1397 |
+
batchInputRef,
|
| 1398 |
isGenerating,
|
| 1399 |
switchToPage,
|
| 1400 |
handleCellClick,
|
| 1401 |
rotateCurrentCell,
|
| 1402 |
updateSelectedCellText,
|
| 1403 |
+
applyBatchText,
|
| 1404 |
applyIconToCell,
|
| 1405 |
clearCurrentCell,
|
| 1406 |
clearAllContent,
|
|
|
|
| 1428 |
confirmCustomGrid,
|
| 1429 |
isCustomGridActive,
|
| 1430 |
getFontSizeClass,
|
| 1431 |
+
getOverviewFontSizeClass,
|
| 1432 |
+
// Export Modal
|
| 1433 |
+
showExportModal,
|
| 1434 |
+
openExportModal,
|
| 1435 |
+
processExport,
|
| 1436 |
+
exportName,
|
| 1437 |
+
exportProjectName,
|
| 1438 |
+
// Multi-select
|
| 1439 |
+
isMultiSelectMode,
|
| 1440 |
+
toggleMultiSelectMode,
|
| 1441 |
+
clearSelection
|
| 1442 |
};
|
| 1443 |
}
|
| 1444 |
}).mount('#app');
|