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