Update index.html
Browse files- index.html +229 -180
index.html
CHANGED
|
@@ -2,8 +2,8 @@
|
|
| 2 |
<html lang="zh-TW">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>Classroom Focus OS - 旗艦極光看板</title>
|
| 7 |
<style>
|
| 8 |
:root {
|
| 9 |
--bg-color: #050b14;
|
|
@@ -23,6 +23,9 @@
|
|
| 23 |
--card-border: rgba(255, 255, 255, 0.12);
|
| 24 |
--primary-text: #ffffff;
|
| 25 |
--secondary-text: #94a3b8;
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
body[data-theme="aurora-fluid"] {
|
| 28 |
--bg-color: #021a14;
|
|
@@ -30,6 +33,9 @@
|
|
| 30 |
--card-border: rgba(52, 211, 153, 0.25);
|
| 31 |
--primary-text: #ffffff;
|
| 32 |
--secondary-text: #a7f3d0;
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
body[data-theme="star-meteor"] {
|
| 35 |
--bg-color: #030712;
|
|
@@ -37,6 +43,9 @@
|
|
| 37 |
--card-border: rgba(99, 102, 241, 0.25);
|
| 38 |
--primary-text: #f3f4f6;
|
| 39 |
--secondary-text: #9ca3af;
|
|
|
|
|
|
|
|
|
|
| 40 |
}
|
| 41 |
body[data-theme="sunset-glow"] {
|
| 42 |
--bg-color: #1a0b1c;
|
|
@@ -44,6 +53,9 @@
|
|
| 44 |
--card-border: rgba(244, 114, 182, 0.25);
|
| 45 |
--primary-text: #ffffff;
|
| 46 |
--secondary-text: #fbcfe8;
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
body[data-theme="gold-luxury"] {
|
| 49 |
--bg-color: #0a0a0a;
|
|
@@ -51,6 +63,9 @@
|
|
| 51 |
--card-border: rgba(234, 179, 8, 0.3);
|
| 52 |
--primary-text: #fef08a;
|
| 53 |
--secondary-text: #ca8a04;
|
|
|
|
|
|
|
|
|
|
| 54 |
}
|
| 55 |
body[data-theme="minimal-light"] {
|
| 56 |
--bg-color: #e2e8f0;
|
|
@@ -59,6 +74,7 @@
|
|
| 59 |
--card-shadow: 0 15px 35px rgba(0, 0, 0, 0.08);
|
| 60 |
--primary-text: #0f172a;
|
| 61 |
--secondary-text: #64748b;
|
|
|
|
| 62 |
--badge-bg: rgba(0, 0, 0, 0.06);
|
| 63 |
--progress-bg: rgba(0, 0, 0, 0.08);
|
| 64 |
}
|
|
@@ -68,6 +84,7 @@
|
|
| 68 |
margin: 0;
|
| 69 |
padding: 0;
|
| 70 |
user-select: none;
|
|
|
|
| 71 |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans TC", sans-serif;
|
| 72 |
}
|
| 73 |
|
|
@@ -79,6 +96,7 @@
|
|
| 79 |
color: var(--primary-text);
|
| 80 |
position: relative;
|
| 81 |
transition: background 0.4s ease;
|
|
|
|
| 82 |
}
|
| 83 |
|
| 84 |
#aurora-canvas {
|
|
@@ -157,16 +175,8 @@
|
|
| 157 |
text-overflow: ellipsis;
|
| 158 |
position: relative;
|
| 159 |
}
|
| 160 |
-
.quote-capsule::before {
|
| 161 |
-
|
| 162 |
-
color: #38bdf8;
|
| 163 |
-
font-weight: bold;
|
| 164 |
-
}
|
| 165 |
-
.quote-capsule::after {
|
| 166 |
-
content: ' ❞';
|
| 167 |
-
color: #38bdf8;
|
| 168 |
-
font-weight: bold;
|
| 169 |
-
}
|
| 170 |
|
| 171 |
#canvas-workspace {
|
| 172 |
width: 100vw;
|
|
@@ -175,6 +185,7 @@
|
|
| 175 |
top: 0;
|
| 176 |
left: 0;
|
| 177 |
z-index: 2;
|
|
|
|
| 178 |
}
|
| 179 |
|
| 180 |
.board-card {
|
|
@@ -189,6 +200,7 @@
|
|
| 189 |
flex-direction: column;
|
| 190 |
overflow: hidden;
|
| 191 |
transition: border 0.2s, box-shadow 0.2s;
|
|
|
|
| 192 |
}
|
| 193 |
|
| 194 |
.board-card[data-cat="exam"] { --theme-color: #38bdf8; }
|
|
@@ -224,6 +236,7 @@
|
|
| 224 |
flex-direction: column;
|
| 225 |
justify-content: space-between;
|
| 226 |
gap: 10px;
|
|
|
|
| 227 |
}
|
| 228 |
|
| 229 |
.card-top {
|
|
@@ -340,19 +353,25 @@
|
|
| 340 |
|
| 341 |
.is-editing .board-card {
|
| 342 |
border: 1.5px dashed #38bdf8 !important;
|
| 343 |
-
cursor:
|
| 344 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
.resize-handle {
|
| 346 |
display: none;
|
| 347 |
position: absolute;
|
| 348 |
-
right:
|
| 349 |
-
bottom:
|
| 350 |
-
width:
|
| 351 |
-
height:
|
| 352 |
cursor: se-resize;
|
| 353 |
-
background:
|
| 354 |
-
border-bottom-right-radius:
|
| 355 |
z-index: 10;
|
|
|
|
|
|
|
| 356 |
}
|
| 357 |
.is-editing .resize-handle {
|
| 358 |
display: block;
|
|
@@ -381,8 +400,8 @@
|
|
| 381 |
position: fixed;
|
| 382 |
bottom: 20px;
|
| 383 |
right: 20px;
|
| 384 |
-
width:
|
| 385 |
-
height:
|
| 386 |
border-radius: 50%;
|
| 387 |
background: rgba(15, 23, 42, 0.8);
|
| 388 |
border: 1.5px solid rgba(255, 255, 255, 0.25);
|
|
@@ -395,16 +414,14 @@
|
|
| 395 |
opacity: 0.4;
|
| 396 |
transition: all 0.2s;
|
| 397 |
z-index: 500;
|
|
|
|
| 398 |
}
|
| 399 |
-
.settings-trigger:hover {
|
| 400 |
opacity: 1;
|
| 401 |
transform: rotate(30deg);
|
| 402 |
color: #fff;
|
| 403 |
}
|
| 404 |
|
| 405 |
-
/* =========================================
|
| 406 |
-
螢幕密碼盤 Modal (專門解決桌布鍵盤焦點問題)
|
| 407 |
-
========================================= */
|
| 408 |
.modal-backdrop {
|
| 409 |
display: none;
|
| 410 |
position: fixed;
|
|
@@ -425,22 +442,11 @@
|
|
| 425 |
color: #fff;
|
| 426 |
box-shadow: 0 25px 50px rgba(0,0,0,0.6);
|
| 427 |
}
|
| 428 |
-
.pin-modal-box h3 {
|
| 429 |
-
font-size: 1.2rem;
|
| 430 |
-
margin-bottom: 4px;
|
| 431 |
-
}
|
| 432 |
-
.pin-modal-box p {
|
| 433 |
-
font-size: 0.8rem;
|
| 434 |
-
color: #94a3b8;
|
| 435 |
-
margin-bottom: 16px;
|
| 436 |
-
}
|
| 437 |
-
|
| 438 |
-
/* PIN 碼顯示點 */
|
| 439 |
.pin-dots-display {
|
| 440 |
display: flex;
|
| 441 |
justify-content: center;
|
| 442 |
gap: 12px;
|
| 443 |
-
margin
|
| 444 |
height: 24px;
|
| 445 |
align-items: center;
|
| 446 |
}
|
|
@@ -463,8 +469,6 @@
|
|
| 463 |
border-color: #ef4444;
|
| 464 |
box-shadow: 0 0 10px rgba(239, 68, 68, 0.6);
|
| 465 |
}
|
| 466 |
-
|
| 467 |
-
/* 數字鍵盤網格 */
|
| 468 |
.pin-keypad {
|
| 469 |
display: grid;
|
| 470 |
grid-template-columns: repeat(3, 1fr);
|
|
@@ -483,12 +487,12 @@
|
|
| 483 |
display: flex;
|
| 484 |
align-items: center;
|
| 485 |
justify-content: center;
|
|
|
|
| 486 |
transition: all 0.15s ease;
|
| 487 |
}
|
| 488 |
.pin-btn:hover {
|
| 489 |
background: rgba(56, 189, 248, 0.2);
|
| 490 |
border-color: #38bdf8;
|
| 491 |
-
color: #fff;
|
| 492 |
}
|
| 493 |
.pin-btn:active {
|
| 494 |
transform: scale(0.95);
|
|
@@ -507,6 +511,7 @@
|
|
| 507 |
font-weight: 600;
|
| 508 |
cursor: pointer;
|
| 509 |
font-size: 0.85rem;
|
|
|
|
| 510 |
}
|
| 511 |
.btn-primary { background: #38bdf8; color: #0f172a; }
|
| 512 |
.btn-secondary { background: #334155; color: #fff; }
|
|
@@ -594,26 +599,26 @@
|
|
| 594 |
|
| 595 |
<div id="canvas-workspace"></div>
|
| 596 |
|
| 597 |
-
<!-- 編輯
|
| 598 |
<div class="edit-banner" id="editBanner">
|
| 599 |
-
<span style="font-size:0.85rem; color:#38bdf8;">✦ 自由排版中:拖曳卡片或
|
| 600 |
<button class="btn btn-secondary" onclick="randomizeQuote()">隨機金句</button>
|
| 601 |
-
<button class="btn btn-secondary" onclick="openAdminDrawer()">作息與
|
| 602 |
<button class="btn btn-success" onclick="exitEditMode()">完成並鎖定</button>
|
| 603 |
</div>
|
| 604 |
|
| 605 |
-
<!-- 齒輪按鈕 -->
|
| 606 |
<div class="settings-trigger" onclick="openPinModal()" title="開啟看板設定">
|
| 607 |
-
<svg width="
|
| 608 |
<path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>
|
| 609 |
</svg>
|
| 610 |
</div>
|
| 611 |
|
| 612 |
-
<!-- 螢幕數位按鍵鎖
|
| 613 |
<div class="modal-backdrop" id="pinModal">
|
| 614 |
<div class="pin-modal-box">
|
| 615 |
<h3>管理員驗證</h3>
|
| 616 |
-
<p>請點擊螢幕按鈕輸入 PIN 碼 (
|
| 617 |
|
| 618 |
<div class="pin-dots-display" id="pinDots">
|
| 619 |
<div class="pin-dot"></div>
|
|
@@ -636,9 +641,7 @@
|
|
| 636 |
<button class="pin-btn action-btn" onclick="backspaceKey()">⌫</button>
|
| 637 |
</div>
|
| 638 |
|
| 639 |
-
<
|
| 640 |
-
<button class="btn btn-secondary" style="width:100%;" onclick="closePinModal()">取消</button>
|
| 641 |
-
</div>
|
| 642 |
</div>
|
| 643 |
</div>
|
| 644 |
|
|
@@ -650,25 +653,18 @@
|
|
| 650 |
</div>
|
| 651 |
|
| 652 |
<div class="drawer-content">
|
| 653 |
-
<!--
|
| 654 |
<div class="setting-block" style="border: 1px solid #38bdf8;">
|
| 655 |
-
<h4>
|
| 656 |
-
<p style="font-size:0.8rem; color:#94a3b8;">
|
| 657 |
|
| 658 |
<div style="display:flex; flex-direction:column; gap:8px;">
|
| 659 |
-
<
|
| 660 |
-
<div style="display:flex; gap:8px;">
|
| 661 |
-
<button class="btn btn-secondary" style="flex:1;" onclick="exportToClipboard()">📋 複製設定代碼</button>
|
| 662 |
-
<button class="btn btn-primary" style="flex:1;" onclick="importFromClipboard()">📥 一鍵讀取剪貼簿匯入</button>
|
| 663 |
-
</div>
|
| 664 |
-
|
| 665 |
-
<div style="text-align:center; font-size:0.75rem; color:#64748b;">— 或使用實體檔案傳遞 —</div>
|
| 666 |
|
| 667 |
-
<!-- 方案 B:直接下載/讀取 JSON 實體檔 -->
|
| 668 |
<div style="display:flex; gap:8px;">
|
| 669 |
-
<button class="btn btn-secondary" style="flex:1;" onclick="downloadConfigFile()">💾
|
| 670 |
<label class="btn btn-primary" style="flex:1; text-align:center; cursor:pointer; margin:0; line-height:1.2;">
|
| 671 |
-
📂
|
| 672 |
<input type="file" id="configFileUploader" accept=".json" style="display:none;" onchange="importConfigFile(event)">
|
| 673 |
</label>
|
| 674 |
</div>
|
|
@@ -768,7 +764,7 @@
|
|
| 768 |
"每一題訂正,都是在替自己補一個洞。",
|
| 769 |
"不會的問題問出口,它就開始變成你的。",
|
| 770 |
"進步常常藏在最不想面對的那個錯誤裡。",
|
| 771 |
-
"你不需要一次就學會,
|
| 772 |
"你現在的每一天,都是在替考場上的自己鋪路。",
|
| 773 |
"別想一次追完所有進度,只要今天比昨天多懂一點。",
|
| 774 |
"緊張是因為你在乎,把在乎換成多做一題。",
|
|
@@ -829,77 +825,142 @@
|
|
| 829 |
"16:00-16:45"
|
| 830 |
];
|
| 831 |
|
|
|
|
|
|
|
| 832 |
const DEFAULT_DATA = {
|
| 833 |
theme: "dark-slate",
|
| 834 |
schedule: DEFAULT_SCHEDULE,
|
| 835 |
cards: [
|
| 836 |
-
{ id: '1', title: "第一次定期評量", date: "2026-10-
|
| 837 |
-
{ id: '2', title: "校慶運動大會", date: "2026-11-
|
| 838 |
{ id: '3', title: "國中教育會考", date: "2027-05-15", cat: "exam", highlight: true, x: 460, y: 150, w: 420, h: 550, totalSpan: 300 }
|
| 839 |
]
|
| 840 |
};
|
|
|
|
| 841 |
|
| 842 |
-
|
|
|
|
|
|
|
| 843 |
let isEditing = false;
|
| 844 |
let isClassBreak = false;
|
| 845 |
let currentPin = "";
|
| 846 |
|
| 847 |
-
|
| 848 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 849 |
return saved ? JSON.parse(saved) : DEFAULT_DATA;
|
| 850 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 851 |
function saveAppData() {
|
| 852 |
-
localStorage.setItem(
|
|
|
|
| 853 |
}
|
| 854 |
|
| 855 |
// =========================================
|
| 856 |
-
//
|
| 857 |
// =========================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
function openPinModal() {
|
| 859 |
currentPin = "";
|
| 860 |
updatePinDots();
|
| 861 |
document.getElementById("pinModal").style.display = "flex";
|
| 862 |
}
|
| 863 |
-
|
| 864 |
function closePinModal() {
|
| 865 |
document.getElementById("pinModal").style.display = "none";
|
| 866 |
currentPin = "";
|
| 867 |
}
|
| 868 |
-
|
| 869 |
function pressKey(num) {
|
| 870 |
if (currentPin.length < 3) {
|
| 871 |
currentPin += num;
|
| 872 |
updatePinDots();
|
| 873 |
-
if (currentPin.length === 3)
|
| 874 |
-
setTimeout(verifyPinCode, 150);
|
| 875 |
-
}
|
| 876 |
}
|
| 877 |
}
|
| 878 |
-
|
| 879 |
function backspaceKey() {
|
| 880 |
if (currentPin.length > 0) {
|
| 881 |
currentPin = currentPin.slice(0, -1);
|
| 882 |
updatePinDots();
|
| 883 |
}
|
| 884 |
}
|
| 885 |
-
|
| 886 |
function clearKey() {
|
| 887 |
currentPin = "";
|
| 888 |
updatePinDots();
|
| 889 |
}
|
| 890 |
-
|
| 891 |
function updatePinDots(isError = false) {
|
| 892 |
const dots = document.querySelectorAll(".pin-dot");
|
| 893 |
dots.forEach((dot, idx) => {
|
| 894 |
dot.className = "pin-dot";
|
| 895 |
-
if (isError)
|
| 896 |
-
|
| 897 |
-
} else if (idx < currentPin.length) {
|
| 898 |
-
dot.classList.add("filled");
|
| 899 |
-
}
|
| 900 |
});
|
| 901 |
}
|
| 902 |
-
|
| 903 |
function verifyPinCode() {
|
| 904 |
if (currentPin === "902") {
|
| 905 |
closePinModal();
|
|
@@ -913,46 +974,14 @@
|
|
| 913 |
}
|
| 914 |
}
|
| 915 |
|
| 916 |
-
// 亦支援瀏覽器環境下的實體鍵盤
|
| 917 |
-
window.addEventListener("keydown", (e) => {
|
| 918 |
-
const modal = document.getElementById("pinModal");
|
| 919 |
-
if (modal.style.display === "flex") {
|
| 920 |
-
if (e.key >= "0" && e.key <= "9") {
|
| 921 |
-
pressKey(e.key);
|
| 922 |
-
} else if (e.key === "Backspace") {
|
| 923 |
-
backspaceKey();
|
| 924 |
-
} else if (e.key === "Escape") {
|
| 925 |
-
closePinModal();
|
| 926 |
-
}
|
| 927 |
-
}
|
| 928 |
-
});
|
| 929 |
-
|
| 930 |
-
function exportToClipboard() {
|
| 931 |
-
const jsonStr = JSON.stringify(appState, null, 2);
|
| 932 |
-
navigator.clipboard.writeText(jsonStr).then(() => {
|
| 933 |
-
alert("✓ 設定代碼已複製到系統剪貼簿!回到桌面直接點擊「一鍵讀取剪貼簿匯入」即可。");
|
| 934 |
-
}).catch(() => {
|
| 935 |
-
downloadConfigFile(); // 若剪貼簿受阻,自動降級為檔案下載
|
| 936 |
-
});
|
| 937 |
-
}
|
| 938 |
-
|
| 939 |
-
async function importFromClipboard() {
|
| 940 |
-
try {
|
| 941 |
-
// 直接向 Windows 剪貼簿索取文字,跳過鍵盤 Ctrl+V
|
| 942 |
-
const text = await navigator.clipboard.readText();
|
| 943 |
-
if (!text || text.trim() === "") {
|
| 944 |
-
alert("剪貼簿目前沒有內容,請先在外部瀏覽器複製設定!");
|
| 945 |
-
return;
|
| 946 |
-
}
|
| 947 |
-
applyJsonConfig(text);
|
| 948 |
-
} catch (err) {
|
| 949 |
-
// 部分環境若限制 readText 權限,引導使用實體檔案
|
| 950 |
-
alert("受限於 Windows 桌面安全權限,無法直接讀取剪貼簿。請改用下方的「選擇檔案匯入」!");
|
| 951 |
-
document.getElementById("configFileUploader").click();
|
| 952 |
-
}
|
| 953 |
-
}
|
| 954 |
function downloadConfigFile() {
|
| 955 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 956 |
const url = URL.createObjectURL(blob);
|
| 957 |
const a = document.createElement("a");
|
| 958 |
a.href = url;
|
|
@@ -972,26 +1001,35 @@
|
|
| 972 |
applyJsonConfig(e.target.result);
|
| 973 |
};
|
| 974 |
reader.readAsText(file);
|
| 975 |
-
event.target.value = "";
|
| 976 |
}
|
| 977 |
|
| 978 |
-
// 統一解析與驗證配置核心
|
| 979 |
function applyJsonConfig(jsonString) {
|
| 980 |
try {
|
| 981 |
const parsed = JSON.parse(jsonString);
|
| 982 |
if (parsed.cards && parsed.theme) {
|
| 983 |
-
appState = parsed;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 984 |
saveAppData();
|
|
|
|
|
|
|
|
|
|
| 985 |
renderBoard();
|
|
|
|
| 986 |
closeAdminDrawer();
|
| 987 |
-
alert("✓ 設定已成功
|
| 988 |
} else {
|
| 989 |
-
alert("設定檔
|
| 990 |
}
|
| 991 |
} catch (e) {
|
| 992 |
-
alert("解析失敗
|
| 993 |
}
|
| 994 |
}
|
|
|
|
| 995 |
function updateDailyQuote() {
|
| 996 |
const now = new Date();
|
| 997 |
const start = new Date(now.getFullYear(), 0, 0);
|
|
@@ -1075,65 +1113,73 @@
|
|
| 1075 |
<div class="resize-handle"></div>
|
| 1076 |
`;
|
| 1077 |
|
| 1078 |
-
|
| 1079 |
-
makeResizable(el.querySelector('.resize-handle'), card, el);
|
| 1080 |
ws.appendChild(el);
|
| 1081 |
});
|
| 1082 |
}
|
| 1083 |
|
| 1084 |
-
function
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
if (
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1103 |
cardData.x = newX;
|
| 1104 |
cardData.y = newY;
|
| 1105 |
}
|
| 1106 |
|
| 1107 |
-
function
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
|
|
|
|
|
|
| 1112 |
saveAppData();
|
| 1113 |
}
|
| 1114 |
|
| 1115 |
-
|
| 1116 |
-
|
|
|
|
| 1117 |
});
|
| 1118 |
-
}
|
| 1119 |
-
|
| 1120 |
-
function makeResizable(handle, cardData, cardEl) {
|
| 1121 |
-
let isResizing = false;
|
| 1122 |
-
let startW, startH, startX, startY;
|
| 1123 |
|
| 1124 |
-
|
| 1125 |
if (!isEditing) return;
|
| 1126 |
e.stopPropagation();
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1137 |
cardEl.style.width = `${newW}px`;
|
| 1138 |
cardEl.style.height = `${newH}px`;
|
| 1139 |
cardData.w = newW;
|
|
@@ -1141,15 +1187,18 @@
|
|
| 1141 |
renderBoard();
|
| 1142 |
}
|
| 1143 |
|
| 1144 |
-
function
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
|
|
|
|
|
|
| 1148 |
saveAppData();
|
| 1149 |
}
|
| 1150 |
|
| 1151 |
-
|
| 1152 |
-
|
|
|
|
| 1153 |
});
|
| 1154 |
}
|
| 1155 |
|
|
|
|
| 2 |
<html lang="zh-TW">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 6 |
+
<title>Classroom Focus OS - 班級旗艦極光看板</title>
|
| 7 |
<style>
|
| 8 |
:root {
|
| 9 |
--bg-color: #050b14;
|
|
|
|
| 23 |
--card-border: rgba(255, 255, 255, 0.12);
|
| 24 |
--primary-text: #ffffff;
|
| 25 |
--secondary-text: #94a3b8;
|
| 26 |
+
--accent-color: #38bdf8;
|
| 27 |
+
--badge-bg: rgba(255, 255, 255, 0.08);
|
| 28 |
+
--progress-bg: rgba(255, 255, 255, 0.1);
|
| 29 |
}
|
| 30 |
body[data-theme="aurora-fluid"] {
|
| 31 |
--bg-color: #021a14;
|
|
|
|
| 33 |
--card-border: rgba(52, 211, 153, 0.25);
|
| 34 |
--primary-text: #ffffff;
|
| 35 |
--secondary-text: #a7f3d0;
|
| 36 |
+
--accent-color: #34d399;
|
| 37 |
+
--badge-bg: rgba(52, 211, 153, 0.12);
|
| 38 |
+
--progress-bg: rgba(255, 255, 255, 0.12);
|
| 39 |
}
|
| 40 |
body[data-theme="star-meteor"] {
|
| 41 |
--bg-color: #030712;
|
|
|
|
| 43 |
--card-border: rgba(99, 102, 241, 0.25);
|
| 44 |
--primary-text: #f3f4f6;
|
| 45 |
--secondary-text: #9ca3af;
|
| 46 |
+
--accent-color: #818cf8;
|
| 47 |
+
--badge-bg: rgba(99, 102, 241, 0.15);
|
| 48 |
+
--progress-bg: rgba(255, 255, 255, 0.1);
|
| 49 |
}
|
| 50 |
body[data-theme="sunset-glow"] {
|
| 51 |
--bg-color: #1a0b1c;
|
|
|
|
| 53 |
--card-border: rgba(244, 114, 182, 0.25);
|
| 54 |
--primary-text: #ffffff;
|
| 55 |
--secondary-text: #fbcfe8;
|
| 56 |
+
--accent-color: #f472b6;
|
| 57 |
+
--badge-bg: rgba(244, 114, 182, 0.15);
|
| 58 |
+
--progress-bg: rgba(255, 255, 255, 0.1);
|
| 59 |
}
|
| 60 |
body[data-theme="gold-luxury"] {
|
| 61 |
--bg-color: #0a0a0a;
|
|
|
|
| 63 |
--card-border: rgba(234, 179, 8, 0.3);
|
| 64 |
--primary-text: #fef08a;
|
| 65 |
--secondary-text: #ca8a04;
|
| 66 |
+
--accent-color: #eab308;
|
| 67 |
+
--badge-bg: rgba(234, 179, 8, 0.15);
|
| 68 |
+
--progress-bg: rgba(255, 255, 255, 0.1);
|
| 69 |
}
|
| 70 |
body[data-theme="minimal-light"] {
|
| 71 |
--bg-color: #e2e8f0;
|
|
|
|
| 74 |
--card-shadow: 0 15px 35px rgba(0, 0, 0, 0.08);
|
| 75 |
--primary-text: #0f172a;
|
| 76 |
--secondary-text: #64748b;
|
| 77 |
+
--accent-color: #2563eb;
|
| 78 |
--badge-bg: rgba(0, 0, 0, 0.06);
|
| 79 |
--progress-bg: rgba(0, 0, 0, 0.08);
|
| 80 |
}
|
|
|
|
| 84 |
margin: 0;
|
| 85 |
padding: 0;
|
| 86 |
user-select: none;
|
| 87 |
+
-webkit-user-select: none;
|
| 88 |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans TC", sans-serif;
|
| 89 |
}
|
| 90 |
|
|
|
|
| 96 |
color: var(--primary-text);
|
| 97 |
position: relative;
|
| 98 |
transition: background 0.4s ease;
|
| 99 |
+
touch-action: none;
|
| 100 |
}
|
| 101 |
|
| 102 |
#aurora-canvas {
|
|
|
|
| 175 |
text-overflow: ellipsis;
|
| 176 |
position: relative;
|
| 177 |
}
|
| 178 |
+
.quote-capsule::before { content: '❝ '; color: #38bdf8; font-weight: bold; }
|
| 179 |
+
.quote-capsule::after { content: ' ❞'; color: #38bdf8; font-weight: bold; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
#canvas-workspace {
|
| 182 |
width: 100vw;
|
|
|
|
| 185 |
top: 0;
|
| 186 |
left: 0;
|
| 187 |
z-index: 2;
|
| 188 |
+
touch-action: none;
|
| 189 |
}
|
| 190 |
|
| 191 |
.board-card {
|
|
|
|
| 200 |
flex-direction: column;
|
| 201 |
overflow: hidden;
|
| 202 |
transition: border 0.2s, box-shadow 0.2s;
|
| 203 |
+
touch-action: none;
|
| 204 |
}
|
| 205 |
|
| 206 |
.board-card[data-cat="exam"] { --theme-color: #38bdf8; }
|
|
|
|
| 236 |
flex-direction: column;
|
| 237 |
justify-content: space-between;
|
| 238 |
gap: 10px;
|
| 239 |
+
pointer-events: none;
|
| 240 |
}
|
| 241 |
|
| 242 |
.card-top {
|
|
|
|
| 353 |
|
| 354 |
.is-editing .board-card {
|
| 355 |
border: 1.5px dashed #38bdf8 !important;
|
| 356 |
+
cursor: grab;
|
| 357 |
}
|
| 358 |
+
.is-editing .board-card:active {
|
| 359 |
+
cursor: grabbing;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
.resize-handle {
|
| 363 |
display: none;
|
| 364 |
position: absolute;
|
| 365 |
+
right: 0px;
|
| 366 |
+
bottom: 0px;
|
| 367 |
+
width: 38px;
|
| 368 |
+
height: 38px;
|
| 369 |
cursor: se-resize;
|
| 370 |
+
background: radial-gradient(circle at 75% 75%, #38bdf8 0%, transparent 65%);
|
| 371 |
+
border-bottom-right-radius: 20px;
|
| 372 |
z-index: 10;
|
| 373 |
+
touch-action: none;
|
| 374 |
+
pointer-events: auto;
|
| 375 |
}
|
| 376 |
.is-editing .resize-handle {
|
| 377 |
display: block;
|
|
|
|
| 400 |
position: fixed;
|
| 401 |
bottom: 20px;
|
| 402 |
right: 20px;
|
| 403 |
+
width: 52px;
|
| 404 |
+
height: 52px;
|
| 405 |
border-radius: 50%;
|
| 406 |
background: rgba(15, 23, 42, 0.8);
|
| 407 |
border: 1.5px solid rgba(255, 255, 255, 0.25);
|
|
|
|
| 414 |
opacity: 0.4;
|
| 415 |
transition: all 0.2s;
|
| 416 |
z-index: 500;
|
| 417 |
+
touch-action: manipulation;
|
| 418 |
}
|
| 419 |
+
.settings-trigger:hover, .settings-trigger:active {
|
| 420 |
opacity: 1;
|
| 421 |
transform: rotate(30deg);
|
| 422 |
color: #fff;
|
| 423 |
}
|
| 424 |
|
|
|
|
|
|
|
|
|
|
| 425 |
.modal-backdrop {
|
| 426 |
display: none;
|
| 427 |
position: fixed;
|
|
|
|
| 442 |
color: #fff;
|
| 443 |
box-shadow: 0 25px 50px rgba(0,0,0,0.6);
|
| 444 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
.pin-dots-display {
|
| 446 |
display: flex;
|
| 447 |
justify-content: center;
|
| 448 |
gap: 12px;
|
| 449 |
+
margin: 16px 0 20px;
|
| 450 |
height: 24px;
|
| 451 |
align-items: center;
|
| 452 |
}
|
|
|
|
| 469 |
border-color: #ef4444;
|
| 470 |
box-shadow: 0 0 10px rgba(239, 68, 68, 0.6);
|
| 471 |
}
|
|
|
|
|
|
|
| 472 |
.pin-keypad {
|
| 473 |
display: grid;
|
| 474 |
grid-template-columns: repeat(3, 1fr);
|
|
|
|
| 487 |
display: flex;
|
| 488 |
align-items: center;
|
| 489 |
justify-content: center;
|
| 490 |
+
touch-action: manipulation;
|
| 491 |
transition: all 0.15s ease;
|
| 492 |
}
|
| 493 |
.pin-btn:hover {
|
| 494 |
background: rgba(56, 189, 248, 0.2);
|
| 495 |
border-color: #38bdf8;
|
|
|
|
| 496 |
}
|
| 497 |
.pin-btn:active {
|
| 498 |
transform: scale(0.95);
|
|
|
|
| 511 |
font-weight: 600;
|
| 512 |
cursor: pointer;
|
| 513 |
font-size: 0.85rem;
|
| 514 |
+
touch-action: manipulation;
|
| 515 |
}
|
| 516 |
.btn-primary { background: #38bdf8; color: #0f172a; }
|
| 517 |
.btn-secondary { background: #334155; color: #fff; }
|
|
|
|
| 599 |
|
| 600 |
<div id="canvas-workspace"></div>
|
| 601 |
|
| 602 |
+
<!-- 編輯狀態控制列 -->
|
| 603 |
<div class="edit-banner" id="editBanner">
|
| 604 |
+
<span style="font-size:0.85rem; color:#38bdf8;">✦ 自由排版中:拖曳卡片或拉動右下角角標</span>
|
| 605 |
<button class="btn btn-secondary" onclick="randomizeQuote()">隨機金句</button>
|
| 606 |
+
<button class="btn btn-secondary" onclick="openAdminDrawer()">作息與設定</button>
|
| 607 |
<button class="btn btn-success" onclick="exitEditMode()">完成並鎖定</button>
|
| 608 |
</div>
|
| 609 |
|
| 610 |
+
<!-- 齒輪解鎖按鈕 -->
|
| 611 |
<div class="settings-trigger" onclick="openPinModal()" title="開啟看板設定">
|
| 612 |
+
<svg width="26" height="26" fill="currentColor" viewBox="0 0 24 24">
|
| 613 |
<path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>
|
| 614 |
</svg>
|
| 615 |
</div>
|
| 616 |
|
| 617 |
+
<!-- 螢幕數位按鍵鎖 -->
|
| 618 |
<div class="modal-backdrop" id="pinModal">
|
| 619 |
<div class="pin-modal-box">
|
| 620 |
<h3>管理員驗證</h3>
|
| 621 |
+
<p style="font-size:0.8rem; color:#94a3b8; margin-top:4px;">請點擊螢幕按鈕輸入 PIN 碼 (902)</p>
|
| 622 |
|
| 623 |
<div class="pin-dots-display" id="pinDots">
|
| 624 |
<div class="pin-dot"></div>
|
|
|
|
| 641 |
<button class="pin-btn action-btn" onclick="backspaceKey()">⌫</button>
|
| 642 |
</div>
|
| 643 |
|
| 644 |
+
<button class="btn btn-secondary" style="width:100%;" onclick="closePinModal()">取消</button>
|
|
|
|
|
|
|
| 645 |
</div>
|
| 646 |
</div>
|
| 647 |
|
|
|
|
| 653 |
</div>
|
| 654 |
|
| 655 |
<div class="drawer-content">
|
| 656 |
+
<!-- 固化預設值與資料備份核心 -->
|
| 657 |
<div class="setting-block" style="border: 1px solid #38bdf8;">
|
| 658 |
+
<h4>永久固化排版 (解決重開機重設問題)</h4>
|
| 659 |
+
<p style="font-size:0.8rem; color:#94a3b8;">排版好後,點擊下方「固化為全新預設檔」��將下載的檔案直接取代原本的 `index.html`,重開機保證 100% 永不跑位。</p>
|
| 660 |
|
| 661 |
<div style="display:flex; flex-direction:column; gap:8px;">
|
| 662 |
+
<button class="btn btn-success" style="padding:10px; font-size:0.9rem;" onclick="exportAsHardcodedHtml()">⭐ 固化為全新預設檔 (下載 index.html)</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
|
|
|
|
| 664 |
<div style="display:flex; gap:8px;">
|
| 665 |
+
<button class="btn btn-secondary" style="flex:1;" onclick="downloadConfigFile()">💾 備份設定 (.json)</button>
|
| 666 |
<label class="btn btn-primary" style="flex:1; text-align:center; cursor:pointer; margin:0; line-height:1.2;">
|
| 667 |
+
📂 匯入設定檔
|
| 668 |
<input type="file" id="configFileUploader" accept=".json" style="display:none;" onchange="importConfigFile(event)">
|
| 669 |
</label>
|
| 670 |
</div>
|
|
|
|
| 764 |
"每一題訂正,都是在替自己補一個洞。",
|
| 765 |
"不會的問題問出口,它就開始變成你的。",
|
| 766 |
"進步常常藏在最不想面對的那個錯誤裡。",
|
| 767 |
+
"你不需要一次就學會,你只需要沒停下來學。",
|
| 768 |
"你現在的每一天,都是在替考場上的自己鋪路。",
|
| 769 |
"別想一次追完所有進度,只要今天比昨天多懂一點。",
|
| 770 |
"緊張是因為你在乎,把在乎換成多做一題。",
|
|
|
|
| 825 |
"16:00-16:45"
|
| 826 |
];
|
| 827 |
|
| 828 |
+
// 本地預設種子(可透過一鍵固化功能直接改寫)
|
| 829 |
+
/*START_DEFAULT_DATA*/
|
| 830 |
const DEFAULT_DATA = {
|
| 831 |
theme: "dark-slate",
|
| 832 |
schedule: DEFAULT_SCHEDULE,
|
| 833 |
cards: [
|
| 834 |
+
{ id: '1', title: "第一次定期評量", date: "2026-10-15", cat: "exam", highlight: false, x: 100, y: 150, w: 320, h: 260, totalSpan: 60 },
|
| 835 |
+
{ id: '2', title: "校慶運動大會", date: "2026-11-20", cat: "sports", highlight: false, x: 100, y: 440, w: 320, h: 260, totalSpan: 90 },
|
| 836 |
{ id: '3', title: "國中教育會考", date: "2027-05-15", cat: "exam", highlight: true, x: 460, y: 150, w: 420, h: 550, totalSpan: 300 }
|
| 837 |
]
|
| 838 |
};
|
| 839 |
+
/*END_DEFAULT_DATA*/
|
| 840 |
|
| 841 |
+
const STORAGE_KEY = "classroom_focus_master_data";
|
| 842 |
+
|
| 843 |
+
let appState = loadInitialData();
|
| 844 |
let isEditing = false;
|
| 845 |
let isClassBreak = false;
|
| 846 |
let currentPin = "";
|
| 847 |
|
| 848 |
+
// =========================================
|
| 849 |
+
// IndexedDB 持久化雙重快取架構
|
| 850 |
+
// =========================================
|
| 851 |
+
function openDB() {
|
| 852 |
+
return new Promise((resolve) => {
|
| 853 |
+
const req = indexedDB.open("ClassroomFocusDB", 1);
|
| 854 |
+
req.onupgradeneeded = (e) => {
|
| 855 |
+
const db = e.target.result;
|
| 856 |
+
if (!db.objectStoreNames.contains("settings")) {
|
| 857 |
+
db.createObjectStore("settings");
|
| 858 |
+
}
|
| 859 |
+
};
|
| 860 |
+
req.onsuccess = (e) => resolve(e.target.result);
|
| 861 |
+
req.onerror = () => resolve(null);
|
| 862 |
+
});
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
async function saveToIndexedDB(data) {
|
| 866 |
+
const db = await openDB();
|
| 867 |
+
if (!db) return;
|
| 868 |
+
const tx = db.transaction("settings", "readwrite");
|
| 869 |
+
tx.objectStore("settings").put(data, "master_data");
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
async function loadFromIndexedDB() {
|
| 873 |
+
const db = await openDB();
|
| 874 |
+
if (!db) return null;
|
| 875 |
+
return new Promise((resolve) => {
|
| 876 |
+
const tx = db.transaction("settings", "readonly");
|
| 877 |
+
const req = tx.objectStore("settings").get("master_data");
|
| 878 |
+
req.onsuccess = () => resolve(req.result || null);
|
| 879 |
+
req.onerror = () => resolve(null);
|
| 880 |
+
});
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
+
function loadInitialData() {
|
| 884 |
+
const saved = localStorage.getItem(STORAGE_KEY);
|
| 885 |
return saved ? JSON.parse(saved) : DEFAULT_DATA;
|
| 886 |
}
|
| 887 |
+
|
| 888 |
+
async function initStorageSync() {
|
| 889 |
+
const idbData = await loadFromIndexedDB();
|
| 890 |
+
if (idbData && idbData.cards) {
|
| 891 |
+
appState = idbData;
|
| 892 |
+
saveAppData();
|
| 893 |
+
renderBoard();
|
| 894 |
+
document.body.setAttribute("data-theme", appState.theme);
|
| 895 |
+
}
|
| 896 |
+
}
|
| 897 |
+
initStorageSync();
|
| 898 |
+
|
| 899 |
function saveAppData() {
|
| 900 |
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(appState));
|
| 901 |
+
saveToIndexedDB(appState);
|
| 902 |
}
|
| 903 |
|
| 904 |
// =========================================
|
| 905 |
+
// 固化為全新預設檔 (下載即可徹底解決跑位)
|
| 906 |
// =========================================
|
| 907 |
+
function exportAsHardcodedHtml() {
|
| 908 |
+
let currentHtml = document.documentElement.outerHTML;
|
| 909 |
+
const targetDataString = JSON.stringify(appState, null, 2);
|
| 910 |
+
|
| 911 |
+
// 正則精準替換 DEFAULT_DATA 變數區塊
|
| 912 |
+
const regex = /\/\*START_DEFAULT_DATA\*\/[\s\S]*?\/\*END_DEFAULT_DATA\*\//;
|
| 913 |
+
const replacement = `/*START_DEFAULT_DATA*/\n const DEFAULT_DATA = ${targetDataString};\n /*END_DEFAULT_DATA*/`;
|
| 914 |
+
|
| 915 |
+
const newHtml = "<!DOCTYPE html>\n<html lang=\"zh-TW\">\n" + currentHtml.replace(regex, replacement) + "\n</html>";
|
| 916 |
+
|
| 917 |
+
const blob = new Blob([newHtml], { type: "text/html;charset=utf-8" });
|
| 918 |
+
const url = URL.createObjectURL(blob);
|
| 919 |
+
const a = document.createElement("a");
|
| 920 |
+
a.href = url;
|
| 921 |
+
a.download = `index.html`;
|
| 922 |
+
document.body.appendChild(a);
|
| 923 |
+
a.click();
|
| 924 |
+
document.body.removeChild(a);
|
| 925 |
+
URL.revokeObjectURL(url);
|
| 926 |
+
|
| 927 |
+
alert("✓ 固化成功!已為您下載最新的 `index.html`。\n請用這個檔案直接覆蓋 Lively 中的原檔,以後開機將原生讀取這份排版,永不重設!");
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
function openPinModal() {
|
| 931 |
currentPin = "";
|
| 932 |
updatePinDots();
|
| 933 |
document.getElementById("pinModal").style.display = "flex";
|
| 934 |
}
|
|
|
|
| 935 |
function closePinModal() {
|
| 936 |
document.getElementById("pinModal").style.display = "none";
|
| 937 |
currentPin = "";
|
| 938 |
}
|
|
|
|
| 939 |
function pressKey(num) {
|
| 940 |
if (currentPin.length < 3) {
|
| 941 |
currentPin += num;
|
| 942 |
updatePinDots();
|
| 943 |
+
if (currentPin.length === 3) setTimeout(verifyPinCode, 150);
|
|
|
|
|
|
|
| 944 |
}
|
| 945 |
}
|
|
|
|
| 946 |
function backspaceKey() {
|
| 947 |
if (currentPin.length > 0) {
|
| 948 |
currentPin = currentPin.slice(0, -1);
|
| 949 |
updatePinDots();
|
| 950 |
}
|
| 951 |
}
|
|
|
|
| 952 |
function clearKey() {
|
| 953 |
currentPin = "";
|
| 954 |
updatePinDots();
|
| 955 |
}
|
|
|
|
| 956 |
function updatePinDots(isError = false) {
|
| 957 |
const dots = document.querySelectorAll(".pin-dot");
|
| 958 |
dots.forEach((dot, idx) => {
|
| 959 |
dot.className = "pin-dot";
|
| 960 |
+
if (isError) dot.classList.add("error");
|
| 961 |
+
else if (idx < currentPin.length) dot.classList.add("filled");
|
|
|
|
|
|
|
|
|
|
| 962 |
});
|
| 963 |
}
|
|
|
|
| 964 |
function verifyPinCode() {
|
| 965 |
if (currentPin === "902") {
|
| 966 |
closePinModal();
|
|
|
|
| 974 |
}
|
| 975 |
}
|
| 976 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 977 |
function downloadConfigFile() {
|
| 978 |
+
const fullPackage = {
|
| 979 |
+
theme: appState.theme,
|
| 980 |
+
schedule: appState.schedule,
|
| 981 |
+
cards: appState.cards
|
| 982 |
+
};
|
| 983 |
+
const jsonStr = JSON.stringify(fullPackage, null, 2);
|
| 984 |
+
const blob = new Blob([jsonStr], { type: "application/json" });
|
| 985 |
const url = URL.createObjectURL(blob);
|
| 986 |
const a = document.createElement("a");
|
| 987 |
a.href = url;
|
|
|
|
| 1001 |
applyJsonConfig(e.target.result);
|
| 1002 |
};
|
| 1003 |
reader.readAsText(file);
|
| 1004 |
+
event.target.value = "";
|
| 1005 |
}
|
| 1006 |
|
|
|
|
| 1007 |
function applyJsonConfig(jsonString) {
|
| 1008 |
try {
|
| 1009 |
const parsed = JSON.parse(jsonString);
|
| 1010 |
if (parsed.cards && parsed.theme) {
|
| 1011 |
+
appState.theme = parsed.theme;
|
| 1012 |
+
appState.cards = parsed.cards;
|
| 1013 |
+
if (parsed.schedule && Array.isArray(parsed.schedule)) {
|
| 1014 |
+
appState.schedule = parsed.schedule;
|
| 1015 |
+
}
|
| 1016 |
+
|
| 1017 |
saveAppData();
|
| 1018 |
+
document.body.setAttribute("data-theme", appState.theme);
|
| 1019 |
+
document.getElementById("themeSelect").value = appState.theme;
|
| 1020 |
+
|
| 1021 |
renderBoard();
|
| 1022 |
+
updateClock();
|
| 1023 |
closeAdminDrawer();
|
| 1024 |
+
alert("✓ 設定已成功匯入並寫入持久快取!");
|
| 1025 |
} else {
|
| 1026 |
+
alert("設定檔格式不符。");
|
| 1027 |
}
|
| 1028 |
} catch (e) {
|
| 1029 |
+
alert("JSON 解析失敗。");
|
| 1030 |
}
|
| 1031 |
}
|
| 1032 |
+
|
| 1033 |
function updateDailyQuote() {
|
| 1034 |
const now = new Date();
|
| 1035 |
const start = new Date(now.getFullYear(), 0, 0);
|
|
|
|
| 1113 |
<div class="resize-handle"></div>
|
| 1114 |
`;
|
| 1115 |
|
| 1116 |
+
attachTouchAndPointerEvents(el, card);
|
|
|
|
| 1117 |
ws.appendChild(el);
|
| 1118 |
});
|
| 1119 |
}
|
| 1120 |
|
| 1121 |
+
function attachTouchAndPointerEvents(cardEl, cardData) {
|
| 1122 |
+
const resizeHandle = cardEl.querySelector('.resize-handle');
|
| 1123 |
+
|
| 1124 |
+
cardEl.addEventListener('pointerdown', (e) => {
|
| 1125 |
+
if (!isEditing) return;
|
| 1126 |
+
if (e.target === resizeHandle || e.target.classList.contains('resize-handle')) return;
|
| 1127 |
+
|
| 1128 |
+
e.preventDefault();
|
| 1129 |
+
cardEl.setPointerCapture(e.pointerId);
|
| 1130 |
+
|
| 1131 |
+
let startX = e.clientX;
|
| 1132 |
+
let startY = e.clientY;
|
| 1133 |
+
let initLeft = cardEl.offsetLeft;
|
| 1134 |
+
let initTop = cardEl.offsetTop;
|
| 1135 |
+
cardEl.style.zIndex = 100;
|
| 1136 |
+
|
| 1137 |
+
function onPointerMove(ev) {
|
| 1138 |
+
if (ev.pointerId !== e.pointerId) return;
|
| 1139 |
+
ev.preventDefault();
|
| 1140 |
+
const dx = ev.clientX - startX;
|
| 1141 |
+
const dy = ev.clientY - startY;
|
| 1142 |
+
const newX = Math.max(0, Math.min(window.innerWidth - cardEl.offsetWidth, initLeft + dx));
|
| 1143 |
+
const newY = Math.max(0, Math.min(window.innerHeight - cardEl.offsetHeight, initTop + dy));
|
| 1144 |
+
|
| 1145 |
+
cardEl.style.left = `${newX}px`;
|
| 1146 |
+
cardEl.style.top = `${newY}px`;
|
| 1147 |
cardData.x = newX;
|
| 1148 |
cardData.y = newY;
|
| 1149 |
}
|
| 1150 |
|
| 1151 |
+
function onPointerUp(ev) {
|
| 1152 |
+
if (ev.pointerId !== e.pointerId) return;
|
| 1153 |
+
try { cardEl.releasePointerCapture(e.pointerId); } catch(err) {}
|
| 1154 |
+
cardEl.style.zIndex = '';
|
| 1155 |
+
cardEl.removeEventListener('pointermove', onPointerMove);
|
| 1156 |
+
cardEl.removeEventListener('pointerup', onPointerUp);
|
| 1157 |
+
cardEl.removeEventListener('pointercancel', onPointerUp);
|
| 1158 |
saveAppData();
|
| 1159 |
}
|
| 1160 |
|
| 1161 |
+
cardEl.addEventListener('pointermove', onPointerMove);
|
| 1162 |
+
cardEl.addEventListener('pointerup', onPointerUp);
|
| 1163 |
+
cardEl.addEventListener('pointercancel', onPointerUp);
|
| 1164 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1165 |
|
| 1166 |
+
resizeHandle.addEventListener('pointerdown', (e) => {
|
| 1167 |
if (!isEditing) return;
|
| 1168 |
e.stopPropagation();
|
| 1169 |
+
e.preventDefault();
|
| 1170 |
+
resizeHandle.setPointerCapture(e.pointerId);
|
| 1171 |
+
|
| 1172 |
+
let startX = e.clientX;
|
| 1173 |
+
let startY = e.clientY;
|
| 1174 |
+
let startW = cardEl.offsetWidth;
|
| 1175 |
+
let startH = cardEl.offsetHeight;
|
| 1176 |
+
|
| 1177 |
+
function onResizeMove(ev) {
|
| 1178 |
+
if (ev.pointerId !== e.pointerId) return;
|
| 1179 |
+
ev.preventDefault();
|
| 1180 |
+
const newW = Math.max(240, startW + (ev.clientX - startX));
|
| 1181 |
+
const newH = Math.max(180, startH + (ev.clientY - startY));
|
| 1182 |
+
|
| 1183 |
cardEl.style.width = `${newW}px`;
|
| 1184 |
cardEl.style.height = `${newH}px`;
|
| 1185 |
cardData.w = newW;
|
|
|
|
| 1187 |
renderBoard();
|
| 1188 |
}
|
| 1189 |
|
| 1190 |
+
function onResizeUp(ev) {
|
| 1191 |
+
if (ev.pointerId !== e.pointerId) return;
|
| 1192 |
+
try { resizeHandle.releasePointerCapture(e.pointerId); } catch(err) {}
|
| 1193 |
+
resizeHandle.removeEventListener('pointermove', onResizeMove);
|
| 1194 |
+
resizeHandle.removeEventListener('pointerup', onResizeUp);
|
| 1195 |
+
resizeHandle.removeEventListener('pointercancel', onResizeUp);
|
| 1196 |
saveAppData();
|
| 1197 |
}
|
| 1198 |
|
| 1199 |
+
resizeHandle.addEventListener('pointermove', onResizeMove);
|
| 1200 |
+
resizeHandle.addEventListener('pointerup', onResizeUp);
|
| 1201 |
+
resizeHandle.addEventListener('pointercancel', onResizeUp);
|
| 1202 |
});
|
| 1203 |
}
|
| 1204 |
|