Lashtw commited on
Commit
0e20069
·
verified ·
1 Parent(s): 6ac072c

Update index.html

Browse files
Files changed (1) hide show
  1. 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
- content: '❝ ';
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: move;
344
  }
 
 
 
 
345
  .resize-handle {
346
  display: none;
347
  position: absolute;
348
- right: 2px;
349
- bottom: 2px;
350
- width: 24px;
351
- height: 24px;
352
  cursor: se-resize;
353
- background: linear-gradient(135deg, transparent 50%, rgba(56, 189, 248, 0.8) 50%);
354
- border-bottom-right-radius: 18px;
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: 48px;
385
- height: 48px;
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-bottom: 20px;
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;">✦ 自由排版中:拖曳卡片或縮放邊角</span>
600
  <button class="btn btn-secondary" onclick="randomizeQuote()">隨機金句</button>
601
- <button class="btn btn-secondary" onclick="openAdminDrawer()">作息與項目</button>
602
  <button class="btn btn-success" onclick="exitEditMode()">完成並鎖定</button>
603
  </div>
604
 
605
- <!-- 齒輪按鈕 -->
606
  <div class="settings-trigger" onclick="openPinModal()" title="開啟看板設定">
607
- <svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
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 碼 (預設 902)</p>
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
- <div style="display:flex; justify-content:center;">
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>設定資料備份與同步 (免鍵盤方案)</h4>
656
- <p style="font-size:0.8rem; color:#94a3b8;">在 Lively 桌面因系統防護無法使用 Ctrl+V 時,請使用以下兩種方式同步:</p>
657
 
658
  <div style="display:flex; flex-direction:column; gap:8px;">
659
- <!-- 方案 A:一鍵從系統剪貼簿讀取 -->
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()">💾 下載設定檔 (.json)</button>
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-14", cat: "exam", highlight: false, x: 100, y: 150, w: 320, h: 260, totalSpan: 60 },
837
- { id: '2', title: "校慶運動大會", date: "2026-11-14", cat: "sports", highlight: false, x: 100, y: 440, w: 320, h: 260, totalSpan: 90 },
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
- let appState = loadAppData();
 
 
843
  let isEditing = false;
844
  let isClassBreak = false;
845
  let currentPin = "";
846
 
847
- function loadAppData() {
848
- const saved = localStorage.getItem("classroom_dashboard_v6");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
849
  return saved ? JSON.parse(saved) : DEFAULT_DATA;
850
  }
 
 
 
 
 
 
 
 
 
 
 
 
851
  function saveAppData() {
852
- localStorage.setItem("classroom_dashboard_v6", JSON.stringify(appState));
 
853
  }
854
 
855
  // =========================================
856
- // 螢幕 PIN 碼鍵盤控制邏輯
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
- dot.classList.add("error");
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 blob = new Blob([JSON.stringify(appState, null, 2)], { type: "application/json" });
 
 
 
 
 
 
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("解析失敗,請確認檔案或剪貼簿內容是否為合法的 JSON 格式。");
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
- makeDraggable(el, card);
1079
- makeResizable(el.querySelector('.resize-handle'), card, el);
1080
  ws.appendChild(el);
1081
  });
1082
  }
1083
 
1084
- function makeDraggable(el, cardData) {
1085
- let isDragging = false;
1086
- let startX, startY, initLeft, initTop;
1087
-
1088
- el.addEventListener('mousedown', (e) => {
1089
- if (!isEditing || e.target.classList.contains('resize-handle')) return;
1090
- isDragging = true;
1091
- startX = e.clientX;
1092
- startY = e.clientY;
1093
- initLeft = el.offsetLeft;
1094
- initTop = el.offsetTop;
1095
- el.style.zIndex = 100;
1096
-
1097
- function onMouseMove(e) {
1098
- if (!isDragging) return;
1099
- const newX = Math.max(0, Math.min(window.innerWidth - el.offsetWidth, initLeft + (e.clientX - startX)));
1100
- const newY = Math.max(0, Math.min(window.innerHeight - el.offsetHeight, initTop + (e.clientY - startY)));
1101
- el.style.left = `${newX}px`;
1102
- el.style.top = `${newY}px`;
 
 
 
 
 
 
 
1103
  cardData.x = newX;
1104
  cardData.y = newY;
1105
  }
1106
 
1107
- function onMouseUp() {
1108
- isDragging = false;
1109
- el.style.zIndex = '';
1110
- window.removeEventListener('mousemove', onMouseMove);
1111
- window.removeEventListener('mouseup', onMouseUp);
 
 
1112
  saveAppData();
1113
  }
1114
 
1115
- window.addEventListener('mousemove', onMouseMove);
1116
- window.addEventListener('mouseup', onMouseUp);
 
1117
  });
1118
- }
1119
-
1120
- function makeResizable(handle, cardData, cardEl) {
1121
- let isResizing = false;
1122
- let startW, startH, startX, startY;
1123
 
1124
- handle.addEventListener('mousedown', (e) => {
1125
  if (!isEditing) return;
1126
  e.stopPropagation();
1127
- isResizing = true;
1128
- startX = e.clientX;
1129
- startY = e.clientY;
1130
- startW = cardEl.offsetWidth;
1131
- startH = cardEl.offsetHeight;
1132
-
1133
- function onMouseMove(e) {
1134
- if (!isResizing) return;
1135
- const newW = Math.max(240, startW + (e.clientX - startX));
1136
- const newH = Math.max(180, startH + (e.clientY - startY));
 
 
 
 
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 onMouseUp() {
1145
- isResizing = false;
1146
- window.removeEventListener('mousemove', onMouseMove);
1147
- window.removeEventListener('mouseup', onMouseUp);
 
 
1148
  saveAppData();
1149
  }
1150
 
1151
- window.addEventListener('mousemove', onMouseMove);
1152
- window.addEventListener('mouseup', onMouseUp);
 
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