Download style.css from Lashtw/Countdown: direct link, hf CLI and curl.
- Browser
- Download file 25.6 kB
-
https://huggingface.co/spaces/Lashtw/Countdown/resolve/main/style.css
- Command line
-
hf download hf://spaces/Lashtw/Countdown/style.css
-
curl -L -o style.css https://huggingface.co/spaces/Lashtw/Countdown/resolve/main/style.css
25.6 kB
| <!DOCTYPE html> | |
| <html lang="zh-TW"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Classroom Hub - 自適應倒數看板</title> | |
| <style> | |
| :root { | |
| --bg-gradient: radial-gradient(circle at 15% 15%, #0f172a 0%, #020617 100%); | |
| --card-bg: rgba(30, 41, 59, 0.55); | |
| --card-border: rgba(255, 255, 255, 0.12); | |
| --card-shadow: 0 20px 40px rgba(0, 0, 0, 0.45); | |
| --primary-text: #ffffff; | |
| --secondary-text: #94a3b8; | |
| --badge-bg: rgba(255, 255, 255, 0.08); | |
| } | |
| /* 全域主題樣式切換 */ | |
| body[data-theme="dark-slate"] { | |
| --bg-gradient: radial-gradient(circle at 15% 15%, #1e293b 0%, #020617 100%); | |
| --card-bg: rgba(30, 41, 59, 0.55); | |
| --card-border: rgba(255, 255, 255, 0.12); | |
| --primary-text: #ffffff; | |
| --secondary-text: #94a3b8; | |
| --badge-bg: rgba(255, 255, 255, 0.08); | |
| } | |
| body[data-theme="midnight-purple"] { | |
| --bg-gradient: radial-gradient(circle at 20% 20%, #2e1065 0%, #0b0217 100%); | |
| --card-bg: rgba(46, 16, 101, 0.4); | |
| --card-border: rgba(192, 132, 252, 0.2); | |
| --primary-text: #ffffff; | |
| --secondary-text: #d8b4fe; | |
| --badge-bg: rgba(192, 132, 252, 0.12); | |
| } | |
| body[data-theme="forest-calm"] { | |
| --bg-gradient: radial-gradient(circle at 20% 20%, #064e3b 0%, #021a14 100%); | |
| --card-bg: rgba(6, 78, 59, 0.4); | |
| --card-border: rgba(52, 211, 153, 0.2); | |
| --primary-text: #ffffff; | |
| --secondary-text: #a7f3d0; | |
| --badge-bg: rgba(52, 211, 153, 0.12); | |
| } | |
| body[data-theme="minimal-light"] { | |
| --bg-gradient: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); | |
| --card-bg: rgba(255, 255, 255, 0.85); | |
| --card-border: rgba(203, 213, 225, 0.8); | |
| --card-shadow: 0 15px 30px rgba(0, 0, 0, 0.08); | |
| --primary-text: #0f172a; | |
| --secondary-text: #64748b; | |
| --badge-bg: rgba(0, 0, 0, 0.06); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| user-select: none; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans TC", sans-serif; | |
| } | |
| body { | |
| width: 100vw; | |
| height: 100vh; | |
| overflow: hidden; | |
| background: var(--bg-gradient); | |
| color: var(--primary-text); | |
| position: relative; | |
| transition: background 0.4s ease; | |
| } | |
| /* 頂部輕量時鐘 */ | |
| .top-bar { | |
| position: absolute; | |
| top: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| text-align: center; | |
| pointer-events: none; | |
| z-index: 5; | |
| } | |
| .live-clock { | |
| font-size: 2.2rem; | |
| font-weight: 700; | |
| color: var(--primary-text); | |
| letter-spacing: -0.5px; | |
| font-feature-settings: "tnum"; | |
| text-shadow: 0 4px 12px rgba(0,0,0,0.4); | |
| } | |
| .live-date { | |
| font-size: 0.85rem; | |
| color: var(--secondary-text); | |
| letter-spacing: 2px; | |
| } | |
| #canvas-workspace { | |
| width: 100vw; | |
| height: 100vh; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| } | |
| /* ========================================= | |
| 自適應卡片容器 (Container Query 架構) | |
| ========================================= */ | |
| .board-card { | |
| position: absolute; | |
| background: var(--card-bg); | |
| border: 1px solid var(--card-border); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| border-radius: 24px; | |
| box-shadow: var(--card-shadow); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| transition: border 0.2s, box-shadow 0.2s; | |
| container-type: size; /* 讓內部元件能根據卡片尺寸等比自適應 */ | |
| } | |
| /* 分類主題色 */ | |
| .board-card[data-cat="exam"] { --cat-color: #38bdf8; } | |
| .board-card[data-cat="sports"] { --cat-color: #f97316; } | |
| .board-card[data-cat="event"] { --cat-color: #ec4899; } | |
| .board-card[data-cat="life"] { --cat-color: #10b981; } | |
| .board-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: clamp(4px, 1.8cqh, 8px); | |
| background: var(--cat-color); | |
| z-index: 2; | |
| } | |
| .board-card.highlight { | |
| border: 1.8px solid #fbbf24; | |
| box-shadow: 0 15px 35px rgba(251, 191, 36, 0.25); | |
| } | |
| .board-card.highlight::before { | |
| background: #fbbf24; | |
| height: clamp(5px, 2.2cqh, 10px); | |
| } | |
| /* 卡片內部排版:使用容器單位 (cqw, cqh) */ | |
| .card-content-box { | |
| width: 100%; | |
| height: 100%; | |
| padding: clamp(14px, 6cqw, 36px) clamp(16px, 7cqw, 40px); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| } | |
| .card-header-area { | |
| display: flex; | |
| flex-direction: column; | |
| gap: clamp(4px, 1.5cqh, 10px); | |
| } | |
| .card-title-row { | |
| display: flex; | |
| align-items: center; | |
| gap: clamp(8px, 2.5cqw, 16px); | |
| } | |
| .icon-wrapper { | |
| width: clamp(28px, 11cqw, 54px); | |
| height: clamp(28px, 11cqw, 54px); | |
| border-radius: clamp(8px, 2.5cqw, 14px); | |
| background: rgba(255, 255, 255, 0.08); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: var(--cat-color); | |
| flex-shrink: 0; | |
| } | |
| .icon-wrapper svg { | |
| width: 60%; | |
| height: 60%; | |
| } | |
| .board-card.highlight .icon-wrapper { | |
| color: #fbbf24; | |
| background: rgba(251, 191, 36, 0.16); | |
| } | |
| .card-title { | |
| font-size: clamp(1rem, 6.5cqw, 2.2rem); | |
| font-weight: 600; | |
| color: var(--primary-text); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| line-height: 1.2; | |
| } | |
| .card-date { | |
| font-size: clamp(0.75rem, 4.2cqw, 1.15rem); | |
| color: var(--secondary-text); | |
| letter-spacing: 0.5px; | |
| } | |
| /* 倒數主核心:天數動態膨脹填滿中段 */ | |
| .countdown-body { | |
| flex: 1; | |
| display: flex; | |
| align-items: baseline; | |
| justify-content: center; | |
| gap: clamp(4px, 1.5cqw, 12px); | |
| min-height: 0; | |
| } | |
| .countdown-digits { | |
| font-size: clamp(2.8rem, 24cqw, 14rem); /* 隨寬度與高度巨大化放大 */ | |
| font-weight: 800; | |
| line-height: 0.95; | |
| letter-spacing: -2px; | |
| font-feature-settings: "tnum"; | |
| color: var(--primary-text); | |
| } | |
| .board-card.highlight .countdown-digits { | |
| color: #fbbf24; | |
| text-shadow: 0 0 clamp(15px, 5cqw, 35px) rgba(251, 191, 36, 0.45); | |
| } | |
| .countdown-unit { | |
| font-size: clamp(0.9rem, 5.5cqw, 2.2rem); | |
| color: var(--secondary-text); | |
| font-weight: 500; | |
| } | |
| /* 底部狀態膠囊標籤 */ | |
| .card-footer-status { | |
| align-self: center; | |
| font-size: clamp(0.7rem, 3.8cqw, 1.05rem); | |
| padding: clamp(4px, 1.5cqh, 8px) clamp(10px, 4cqw, 24px); | |
| border-radius: 999px; | |
| background: var(--badge-bg); | |
| color: var(--secondary-text); | |
| font-weight: 500; | |
| white-space: nowrap; | |
| } | |
| .board-card.highlight .card-footer-status { | |
| background: rgba(251, 191, 36, 0.15); | |
| color: #fbbf24; | |
| } | |
| /* 編輯排版模式樣式 */ | |
| .is-editing .board-card { | |
| border: 1.5px dashed #38bdf8 ; | |
| cursor: move; | |
| } | |
| .resize-handle { | |
| display: none; | |
| position: absolute; | |
| right: 2px; | |
| bottom: 2px; | |
| width: 24px; | |
| height: 24px; | |
| cursor: se-resize; | |
| background: linear-gradient(135deg, transparent 50%, rgba(56, 189, 248, 0.8) 50%); | |
| border-bottom-right-radius: 18px; | |
| z-index: 10; | |
| } | |
| .is-editing .resize-handle { | |
| display: block; | |
| } | |
| /* 編輯浮動狀態條 */ | |
| .edit-banner { | |
| display: none; | |
| position: fixed; | |
| top: 16px; | |
| right: 20px; | |
| background: rgba(15, 23, 42, 0.95); | |
| border: 1px solid #38bdf8; | |
| backdrop-filter: blur(8px); | |
| padding: 8px 16px; | |
| border-radius: 12px; | |
| z-index: 1000; | |
| align-items: center; | |
| gap: 12px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); | |
| } | |
| .is-editing .edit-banner { | |
| display: flex; | |
| } | |
| /* 齒輪圖示 */ | |
| .settings-trigger { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| width: 46px; | |
| height: 46px; | |
| border-radius: 50%; | |
| background: rgba(15, 23, 42, 0.7); | |
| border: 1px solid rgba(255, 255, 255, 0.15); | |
| backdrop-filter: blur(8px); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| color: #94a3b8; | |
| opacity: 0.25; | |
| transition: all 0.2s; | |
| z-index: 500; | |
| } | |
| .settings-trigger:hover { | |
| opacity: 1; | |
| transform: rotate(30deg); | |
| color: #fff; | |
| } | |
| /* 彈窗與抽屜 */ | |
| .modal-backdrop { | |
| display: none; | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0, 0, 0, 0.75); | |
| backdrop-filter: blur(10px); | |
| z-index: 2000; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .modal-box { | |
| background: #1e293b; | |
| border: 1px solid rgba(255, 255, 255, 0.15); | |
| border-radius: 16px; | |
| padding: 28px; | |
| width: 320px; | |
| text-align: center; | |
| color: #fff; | |
| } | |
| .modal-box input { | |
| width: 100%; | |
| padding: 10px; | |
| border-radius: 8px; | |
| border: 1px solid #334155; | |
| background: #0f172a; | |
| color: #fff; | |
| font-size: 1.2rem; | |
| text-align: center; | |
| letter-spacing: 6px; | |
| margin: 15px 0 20px; | |
| outline: none; | |
| } | |
| .btn { | |
| padding: 8px 16px; | |
| border-radius: 8px; | |
| border: none; | |
| font-weight: 600; | |
| cursor: pointer; | |
| font-size: 0.85rem; | |
| } | |
| .btn-primary { background: #38bdf8; color: #0f172a; } | |
| .btn-secondary { background: #334155; color: #fff; } | |
| .btn-success { background: #10b981; color: #fff; } | |
| .btn-danger { background: #ef4444; color: #fff; } | |
| .admin-drawer { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| right: 0; | |
| width: 520px; | |
| max-width: 92vw; | |
| height: 100vh; | |
| background: #0f172a; | |
| border-left: 1px solid #334155; | |
| box-shadow: -15px 0 40px rgba(0,0,0,0.6); | |
| z-index: 2100; | |
| flex-direction: column; | |
| color: #e2e8f0; | |
| } | |
| .drawer-header { | |
| padding: 20px; | |
| border-bottom: 1px solid #1e293b; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .drawer-content { | |
| padding: 20px; | |
| overflow-y: auto; | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 18px; | |
| } | |
| .theme-control-box { | |
| background: #1e293b; | |
| border: 1px solid #334155; | |
| border-radius: 10px; | |
| padding: 14px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .item-row { | |
| background: #1e293b; | |
| border: 1px solid #334155; | |
| border-radius: 10px; | |
| padding: 14px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .item-inputs { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 8px; | |
| } | |
| .item-row input, .item-row select, .theme-control-box select { | |
| background: #0f172a; | |
| border: 1px solid #334155; | |
| color: #fff; | |
| padding: 7px 10px; | |
| border-radius: 6px; | |
| font-size: 0.85rem; | |
| } | |
| </style> | |
| </head> | |
| <body data-theme="dark-slate"> | |
| <!-- 頂部即時時鐘 --> | |
| <div class="top-bar"> | |
| <div class="live-clock" id="liveClock">00:00:00</div> | |
| <div class="live-date" id="liveDate">LOADING CALENDAR...</div> | |
| </div> | |
| <!-- 自由自適應畫布 --> | |
| <div id="canvas-workspace"></div> | |
| <!-- 編輯狀態控制頂列 --> | |
| <div class="edit-banner" id="editBanner"> | |
| <span style="font-size:0.85rem; color:#38bdf8;">✦ 自由排版中:拖動卡片改變位置,拉右下角角標縮放</span> | |
| <button class="btn btn-secondary" onclick="openAdminDrawer()">設定與項目</button> | |
| <button class="btn btn-success" onclick="exitEditMode()">完成並鎖定</button> | |
| </div> | |
| <!-- 右下角隱密齒輪 --> | |
| <div class="settings-trigger" onclick="openAuthModal()" title="排版設定"> | |
| <svg width="22" height="22" fill="currentColor" viewBox="0 0 24 24"> | |
| <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"/> | |
| </svg> | |
| </div> | |
| <!-- 密碼 Modal --> | |
| <div class="modal-backdrop" id="authModal"> | |
| <div class="modal-box"> | |
| <h3>進入看板管理</h3> | |
| <p style="color:#94a3b8; font-size:0.8rem; margin-top:6px;">請輸入管理員密碼</p> | |
| <input type="password" id="authPassword" placeholder="PIN" maxlength="6" onkeydown="if(event.key==='Enter') verifyPassword()"> | |
| <div style="display:flex; justify-content:center; gap:8px;"> | |
| <button class="btn btn-secondary" onclick="closeAuthModal()">取消</button> | |
| <button class="btn btn-primary" onclick="verifyPassword()">解鎖排版</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 側拉管理抽屜 --> | |
| <div class="admin-drawer" id="adminDrawer"> | |
| <div class="drawer-header"> | |
| <h3>看板設定與項目管理</h3> | |
| <button class="btn btn-secondary" onclick="closeAdminDrawer()">關閉</button> | |
| </div> | |
| <div class="drawer-content"> | |
| <!-- 主題風格選擇區塊 --> | |
| <div class="theme-control-box"> | |
| <span style="font-weight:600;">整體版面風格</span> | |
| <select id="themeSelect" onchange="changeTheme(this.value)"> | |
| <option value="dark-slate">深邃板岩灰 (Dark Slate)</option> | |
| <option value="midnight-purple">星夜紫韻 (Midnight Purple)</option> | |
| <option value="forest-calm">靜謐青綠 (Forest Calm)</option> | |
| <option value="minimal-light">晨光清透 (Minimal Light)</option> | |
| </select> | |
| </div> | |
| <div style="display:flex; justify-content:space-between; align-items:center; margin-top:8px;"> | |
| <span style="font-size:0.85rem; color:#94a3b8;">倒數項目清單</span> | |
| <button class="btn btn-primary" onclick="addNewEvent()">+ 新增卡片</button> | |
| </div> | |
| <div id="editorItemList" style="display:flex; flex-direction:column; gap:10px;"></div> | |
| </div> | |
| <div style="padding:16px; border-top:1px solid #1e293b; display:flex; justify-content:flex-end;"> | |
| <button class="btn btn-success" onclick="saveEditorList()">儲存並發布</button> | |
| </div> | |
| </div> | |
| <script> | |
| // 精緻 SVG 向量圖示庫 | |
| const ICONS = { | |
| exam: `<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>`, | |
| sports: `<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"></path><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"></path><path d="M4 22h16"></path><path d="M10 14.66V17c0 .55-.45 1-1 1H7v4h10v-4h-2c-.55 0-1-.45-1-1v-2.34c3.08-.63 5.4-3.3 5.4-6.66V4H4v4c0 3.36 2.32 6.03 5.4 6.66z"></path></svg>`, | |
| event: `<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line></svg>`, | |
| life: `<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>` | |
| }; | |
| // 初始預設資料 | |
| const DEFAULT_DATA = { | |
| theme: "dark-slate", | |
| cards: [ | |
| { id: '1', title: "第一次定期評量", date: "2026-10-15", cat: "exam", highlight: false, x: 100, y: 150, w: 300, h: 250 }, | |
| { id: '2', title: "校慶運動大會", date: "2026-11-20", cat: "sports", highlight: false, x: 100, y: 430, w: 300, h: 250 }, | |
| { id: '3', title: "國中教育會考", date: "2027-05-15", cat: "exam", highlight: true, x: 440, y: 150, w: 420, h: 530 } | |
| ] | |
| }; | |
| let appState = loadAppData(); | |
| let isEditing = false; | |
| function loadAppData() { | |
| const saved = localStorage.getItem("classroom_dashboard_data"); | |
| return saved ? JSON.parse(saved) : DEFAULT_DATA; | |
| } | |
| function saveAppData() { | |
| localStorage.setItem("classroom_dashboard_data", JSON.stringify(appState)); | |
| } | |
| // 切換主題 | |
| function changeTheme(themeName) { | |
| appState.theme = themeName; | |
| document.body.setAttribute("data-theme", themeName); | |
| } | |
| // 渲染整個自由畫布 | |
| function renderBoard() { | |
| document.body.setAttribute("data-theme", appState.theme); | |
| const ws = document.getElementById("canvas-workspace"); | |
| ws.innerHTML = ""; | |
| const today = new Date(); | |
| today.setHours(0,0,0,0); | |
| appState.cards.forEach(card => { | |
| const target = new Date(card.date + "T00:00:00"); | |
| const diffDays = Math.ceil((target - today) / (1000 * 60 * 60 * 24)); | |
| const displayDays = diffDays >= 0 ? diffDays : 0; | |
| const badgeText = diffDays > 0 ? `倒數 ${diffDays} 天` : (diffDays === 0 ? "🔥 就是今天" : "已圓滿結束"); | |
| const el = document.createElement("div"); | |
| el.className = `board-card ${card.highlight ? 'highlight' : ''}`; | |
| el.id = `card-${card.id}`; | |
| el.setAttribute("data-cat", card.cat); | |
| el.style.left = `${card.x}px`; | |
| el.style.top = `${card.y}px`; | |
| el.style.width = `${card.w}px`; | |
| el.style.height = `${card.h}px`; | |
| el.innerHTML = ` | |
| <div class="card-content-box"> | |
| <div class="card-header-area"> | |
| <div class="card-title-row"> | |
| <div class="icon-wrapper">${ICONS[card.cat] || ICONS.life}</div> | |
| <div class="card-title">${card.title}</div> | |
| </div> | |
| <div class="card-date">${card.date.replace(/-/g, '.')}</div> | |
| </div> | |
| <div class="countdown-body"> | |
| <span class="countdown-digits">${displayDays}</span> | |
| <span class="countdown-unit">天</span> | |
| </div> | |
| <div class="card-footer-status">${badgeText}</div> | |
| </div> | |
| <div class="resize-handle"></div> | |
| `; | |
| makeDraggable(el, card); | |
| makeResizable(el.querySelector('.resize-handle'), card, el); | |
| ws.appendChild(el); | |
| }); | |
| } | |
| // 拖動定位 | |
| function makeDraggable(el, cardData) { | |
| let isDragging = false; | |
| let startX, startY, initLeft, initTop; | |
| el.addEventListener('mousedown', (e) => { | |
| if (!isEditing || e.target.classList.contains('resize-handle')) return; | |
| isDragging = true; | |
| startX = e.clientX; | |
| startY = e.clientY; | |
| initLeft = el.offsetLeft; | |
| initTop = el.offsetTop; | |
| el.style.zIndex = 100; | |
| function onMouseMove(e) { | |
| if (!isDragging) return; | |
| const newX = Math.max(0, Math.min(window.innerWidth - el.offsetWidth, initLeft + (e.clientX - startX))); | |
| const newY = Math.max(0, Math.min(window.innerHeight - el.offsetHeight, initTop + (e.clientY - startY))); | |
| el.style.left = `${newX}px`; | |
| el.style.top = `${newY}px`; | |
| cardData.x = newX; | |
| cardData.y = newY; | |
| } | |
| function onMouseUp() { | |
| isDragging = false; | |
| el.style.zIndex = ''; | |
| window.removeEventListener('mousemove', onMouseMove); | |
| window.removeEventListener('mouseup', onMouseUp); | |
| saveAppData(); | |
| } | |
| window.addEventListener('mousemove', onMouseMove); | |
| window.addEventListener('mouseup', onMouseUp); | |
| }); | |
| } | |
| // 右下角拖拉自適應縮放 | |
| function makeResizable(handle, cardData, cardEl) { | |
| let isResizing = false; | |
| let startW, startH, startX, startY; | |
| handle.addEventListener('mousedown', (e) => { | |
| if (!isEditing) return; | |
| e.stopPropagation(); | |
| isResizing = true; | |
| startX = e.clientX; | |
| startY = e.clientY; | |
| startW = cardEl.offsetWidth; | |
| startH = cardEl.offsetHeight; | |
| function onMouseMove(e) { | |
| if (!isResizing) return; | |
| // 限制最小尺寸,避免過小破版 | |
| const newW = Math.max(200, startW + (e.clientX - startX)); | |
| const newH = Math.max(160, startH + (e.clientY - startY)); | |
| cardEl.style.width = `${newW}px`; | |
| cardEl.style.height = `${newH}px`; | |
| cardData.w = newW; | |
| cardData.h = newH; | |
| // container query 會自動完成內部文字與數字等比自適應放大,不需手動換算 | |
| } | |
| function onMouseUp() { | |
| isResizing = false; | |
| window.removeEventListener('mousemove', onMouseMove); | |
| window.removeEventListener('mouseup', onMouseUp); | |
| saveAppData(); | |
| } | |
| window.addEventListener('mousemove', onMouseMove); | |
| window.addEventListener('mouseup', onMouseUp); | |
| }); | |
| } | |
| // 密碼與狀態 | |
| function openAuthModal() { | |
| document.getElementById("authPassword").value = ""; | |
| document.getElementById("authModal").style.display = "flex"; | |
| document.getElementById("authPassword").focus(); | |
| } | |
| function closeAuthModal() { | |
| document.getElementById("authModal").style.display = "none"; | |
| } | |
| function verifyPassword() { | |
| if (document.getElementById("authPassword").value === "902") { | |
| closeAuthModal(); | |
| enterEditMode(); | |
| } else { | |
| alert("密碼錯誤"); | |
| } | |
| } | |
| function enterEditMode() { | |
| isEditing = true; | |
| document.body.classList.add("is-editing"); | |
| } | |
| function exitEditMode() { | |
| isEditing = false; | |
| document.body.classList.remove("is-editing"); | |
| saveAppData(); | |
| } | |
| // 抽屜面板控制 | |
| function openAdminDrawer() { | |
| document.getElementById("themeSelect").value = appState.theme; | |
| renderDrawerEditor(); | |
| document.getElementById("adminDrawer").style.display = "flex"; | |
| } | |
| function closeAdminDrawer() { | |
| document.getElementById("adminDrawer").style.display = "none"; | |
| } | |
| function renderDrawerEditor() { | |
| const container = document.getElementById("editorItemList"); | |
| container.innerHTML = ""; | |
| appState.cards.forEach((item, index) => { | |
| const row = document.createElement("div"); | |
| row.className = "item-row"; | |
| row.innerHTML = ` | |
| <div class="item-inputs"> | |
| <input type="text" value="${item.title}" placeholder="名稱" onchange="appState.cards[${index}].title=this.value"> | |
| <input type="date" value="${item.date}" onchange="appState.cards[${index}].date=this.value"> | |
| </div> | |
| <div style="display:flex; justify-content:space-between; align-items:center;"> | |
| <select onchange="appState.cards[${index}].cat=this.value"> | |
| <option value="exam" ${item.cat==='exam'?'selected':''}>學業考試 (書本)</option> | |
| <option value="sports" ${item.cat==='sports'?'selected':''}>運動競賽 (獎盃)</option> | |
| <option value="event" ${item.cat==='event'?'selected':''}>校慶活動 (旗幟)</option> | |
| <option value="life" ${item.cat==='life'?'selected':''}>常態生活 (時鐘)</option> | |
| </select> | |
| <label style="font-size:0.8rem; color:#cbd5e1; display:flex; align-items:center; gap:4px; cursor:pointer;"> | |
| <input type="checkbox" ${item.highlight ? 'checked':''} onchange="appState.cards[${index}].highlight=this.checked"> 重要高亮 | |
| </label> | |
| <button class="btn btn-danger" style="padding:4px 8px;" onclick="removeCard(${index})">刪除</button> | |
| </div> | |
| `; | |
| container.appendChild(row); | |
| }); | |
| } | |
| function addNewEvent() { | |
| const todayStr = new Date().toISOString().split('T')[0]; | |
| appState.cards.push({ | |
| id: String(Date.now()), | |
| title: "新倒數目標", | |
| date: todayStr, | |
| cat: "exam", | |
| highlight: false, | |
| x: 100 + (appState.cards.length * 30), | |
| y: 200, | |
| w: 280, | |
| h: 220 | |
| }); | |
| renderDrawerEditor(); | |
| } | |
| function removeCard(idx) { | |
| appState.cards.splice(idx, 1); | |
| renderDrawerEditor(); | |
| } | |
| function saveEditorList() { | |
| saveAppData(); | |
| renderBoard(); | |
| closeAdminDrawer(); | |
| } | |
| // 即時時鐘 | |
| function updateClock() { | |
| const now = new Date(); | |
| const h = String(now.getHours()).padStart(2, '0'); | |
| const m = String(now.getMinutes()).padStart(2, '0'); | |
| const s = String(now.getSeconds()).padStart(2, '0'); | |
| document.getElementById("liveClock").textContent = `${h}:${m}:${s}`; | |
| document.getElementById("liveDate").textContent = now.toLocaleDateString('zh-TW', { | |
| year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' | |
| }); | |
| } | |
| setInterval(updateClock, 1000); | |
| updateClock(); | |
| // 啟動渲染 | |
| renderBoard(); | |
| </script> | |
| </body> | |
| </html> |