| <!DOCTYPE html> |
| <html lang="zh-TW"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>7人共同空檔協調行事曆</title> |
| <style> |
| :root { |
| --primary-color: #4a90e2; |
| --success-color: #2ecc71; |
| --bg-color: #f4f7f6; |
| --card-bg: #ffffff; |
| --border-color: #e0e0e0; |
| } |
| |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; |
| background-color: var(--bg-color); |
| color: #333; |
| margin: 0; |
| padding: 20px; |
| } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| } |
| |
| h1 { |
| text-align: center; |
| color: #2c3e50; |
| margin-bottom: 10px; |
| } |
| |
| .status-indicator { |
| text-align: center; |
| font-size: 0.9rem; |
| color: #666; |
| margin-bottom: 20px; |
| height: 20px; |
| } |
| |
| |
| .intersection-card { |
| background-color: #e8f8f5; |
| border: 2px solid var(--success-color); |
| border-radius: 8px; |
| padding: 15px 20px; |
| margin-bottom: 25px; |
| box-shadow: 0 2px 5px rgba(0,0,0,0.05); |
| } |
| |
| .intersection-card h2 { |
| margin-top: 0; |
| color: #27ae60; |
| font-size: 1.2rem; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| |
| .available-tags { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 10px; |
| margin-top: 10px; |
| } |
| |
| .tag { |
| background-color: var(--success-color); |
| color: white; |
| padding: 6px 12px; |
| border-radius: 20px; |
| font-weight: bold; |
| font-size: 0.95rem; |
| } |
| |
| .no-match { |
| color: #7f8c8d; |
| font-style: italic; |
| } |
| |
| |
| .calendar-grid { |
| display: flex; |
| flex-direction: column; |
| gap: 15px; |
| } |
| |
| .date-card { |
| background: var(--card-bg); |
| border-radius: 8px; |
| border: 1px solid var(--border-color); |
| padding: 15px; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.02); |
| transition: all 0.3s ease; |
| } |
| |
| .date-card.all-available { |
| border: 2px solid var(--success-color); |
| background-color: #f9fffb; |
| } |
| |
| .date-header { |
| font-size: 1.1rem; |
| font-weight: bold; |
| color: #34495e; |
| margin-bottom: 12px; |
| padding-bottom: 8px; |
| border-bottom: 1px solid #eee; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| |
| .status-badge { |
| font-size: 0.85rem; |
| padding: 3px 8px; |
| border-radius: 4px; |
| background-color: #eee; |
| color: #666; |
| } |
| |
| .date-card.all-available .status-badge { |
| background-color: var(--success-color); |
| color: white; |
| } |
| |
| |
| .members-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); |
| gap: 10px; |
| } |
| |
| .member-col { |
| background: #f8f9fa; |
| border: 1px solid #e9ecef; |
| border-radius: 6px; |
| padding: 8px; |
| display: flex; |
| flex-direction: column; |
| gap: 6px; |
| } |
| |
| .member-name-row { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| font-weight: bold; |
| color: #495057; |
| font-size: 0.95rem; |
| } |
| |
| .member-col label { |
| display: flex; |
| align-items: center; |
| gap: 4px; |
| font-size: 0.8rem; |
| color: #6c757d; |
| cursor: pointer; |
| } |
| |
| .member-col input[type="text"] { |
| width: 100%; |
| padding: 6px; |
| border: 1px solid #ced4da; |
| border-radius: 4px; |
| font-size: 0.85rem; |
| box-sizing: border-box; |
| } |
| |
| .member-col input[type="text"]:focus { |
| outline: none; |
| border-color: var(--primary-color); |
| box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2); |
| } |
| |
| @media (max-width: 768px) { |
| .members-grid { |
| grid-template-columns: repeat(2, 1fr); |
| } |
| } |
| </style> |
|
|
| |
| <script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-app-compat.js"></script> |
| <script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-database-compat.js"></script> |
| </head> |
| <body> |
|
|
| <div class="container"> |
| <h1>📅 7人空檔時間協調行事曆 (8/14 - 9/2)</h1> |
| <div id="sync-status" class="status-indicator">📡 連線中...</div> |
|
|
| |
| <div class="intersection-card"> |
| <h2>🎉 全員皆有空的日期(交集):</h2> |
| <div id="intersection-results" class="available-tags"> |
| <span class="no-match">讀取中...</span> |
| </div> |
| </div> |
|
|
| |
| <div id="calendar" class="calendar-grid"></div> |
| </div> |
|
|
| <script> |
| |
| const firebaseConfig = { |
| databaseURL: "https://bolobolobolomi-66014-default-rtdb.asia-southeast1.firebasedatabase.app/" |
| }; |
| firebase.initializeApp(firebaseConfig); |
| const db = firebase.database(); |
| const scheduleRef = db.ref("schedule_2026"); |
| |
| const members = ["桑", "吳", "張", "趙", "謝", "黃", "康"]; |
| const startDate = new Date("2026-08-14"); |
| const endDate = new Date("2026-09-02"); |
| |
| |
| const dateList = []; |
| let curr = new Date(startDate); |
| while (curr <= endDate) { |
| const year = curr.getFullYear(); |
| const month = String(curr.getMonth() + 1).padStart(2, '0'); |
| const day = String(curr.getDate()).padStart(2, '0'); |
| const dateStr = `${year}-${month}-${day}`; |
| |
| const weekDays = ["日", "一", "二", "三", "四", "五", "六"]; |
| const dayOfWeek = weekDays[curr.getDay()]; |
| |
| dateList.push({ dateStr, displayStr: `${month}/${day} (${dayOfWeek})` }); |
| curr.setDate(curr.getDate() + 1); |
| } |
| |
| |
| const calendarContainer = document.getElementById("calendar"); |
| const syncStatus = document.getElementById("sync-status"); |
| |
| dateList.forEach(({ dateStr, displayStr }) => { |
| const dateCard = document.createElement("div"); |
| dateCard.className = "date-card"; |
| dateCard.id = `card-${dateStr}`; |
| |
| let membersHTML = ""; |
| members.forEach(name => { |
| membersHTML += ` |
| <div class="member-col"> |
| <div class="member-name-row"> |
| <span>${name}</span> |
| <label> |
| <input type="checkbox" id="chk-${dateStr}-${name}" onchange="saveData('${dateStr}', '${name}')"> |
| 有空 |
| </label> |
| </div> |
| <input type="text" id="txt-${dateStr}-${name}" placeholder="輸入時間..." oninput="saveData('${dateStr}', '${name}')"> |
| </div> |
| `; |
| }); |
| |
| dateCard.innerHTML = ` |
| <div class="date-header"> |
| <span>${displayStr}</span> |
| <span class="status-badge" id="badge-${dateStr}">計算中...</span> |
| </div> |
| <div class="members-grid"> |
| ${membersHTML} |
| </div> |
| `; |
| |
| calendarContainer.appendChild(dateCard); |
| }); |
| |
| |
| const debounceTimers = {}; |
| function saveData(dateStr, memberName) { |
| syncStatus.innerText = "⏳ 儲存中..."; |
| syncStatus.style.color = "#e67e22"; |
| |
| const key = `${dateStr}-${memberName}`; |
| clearTimeout(debounceTimers[key]); |
| |
| debounceTimers[key] = setTimeout(() => { |
| const isAvailable = document.getElementById(`chk-${dateStr}-${memberName}`).checked; |
| const textNote = document.getElementById(`txt-${dateStr}-${memberName}`).value; |
| |
| scheduleRef.child(dateStr).child(memberName).set({ |
| available: isAvailable, |
| note: textNote |
| }, (error) => { |
| if (error) { |
| syncStatus.innerText = "❌ 儲存失敗!請檢查 Firebase 規則"; |
| syncStatus.style.color = "#e74c3c"; |
| console.error("Firebase 保存失敗:", error); |
| } else { |
| syncStatus.innerText = "✅ 所有變更已即時同步保存"; |
| syncStatus.style.color = "#27ae60"; |
| } |
| }); |
| }, 300); |
| } |
| |
| |
| scheduleRef.on("value", (snapshot) => { |
| syncStatus.innerText = "✅ 資料庫已連線,即時同步中"; |
| syncStatus.style.color = "#27ae60"; |
| |
| const data = snapshot.val() || {}; |
| const fullAvailableDates = []; |
| |
| dateList.forEach(({ dateStr, displayStr }) => { |
| const dateData = data[dateStr] || {}; |
| let availableCount = 0; |
| |
| members.forEach(name => { |
| const memberData = dateData[name] || { available: false, note: "" }; |
| |
| const chkEl = document.getElementById(`chk-${dateStr}-${name}`); |
| const txtEl = document.getElementById(`txt-${dateStr}-${name}`); |
| |
| |
| if (document.activeElement !== txtEl) { |
| txtEl.value = memberData.note || ""; |
| } |
| chkEl.checked = !!memberData.available; |
| |
| if (memberData.available) { |
| availableCount++; |
| } |
| }); |
| |
| const cardEl = document.getElementById(`card-${dateStr}`); |
| const badgeEl = document.getElementById(`badge-${dateStr}`); |
| |
| if (availableCount === members.length) { |
| cardEl.classList.add("all-available"); |
| badgeEl.innerText = "🎉 全員皆有空!"; |
| fullAvailableDates.push(displayStr); |
| } else { |
| cardEl.classList.remove("all-available"); |
| badgeEl.innerText = `有空人數:${availableCount}/${members.length}`; |
| } |
| }); |
| |
| |
| const resultsContainer = document.getElementById("intersection-results"); |
| if (fullAvailableDates.length > 0) { |
| resultsContainer.innerHTML = fullAvailableDates |
| .map(d => `<span class="tag">${d}</span>`) |
| .join(""); |
| } else { |
| resultsContainer.innerHTML = `<span class="no-match">目前尚無所有人都有空的日期</span>`; |
| } |
| }, (error) => { |
| syncStatus.innerText = "❌ 無法讀取資料,請至 Firebase 設定 Rules 權限!"; |
| syncStatus.style.color = "#e74c3c"; |
| console.error("Firebase 讀取失敗:", error); |
| }); |
| </script> |
| </body> |
| </html> |