Time / index.html
OuiMCB's picture
Update index.html
f65495c verified
Raw
History Blame Contribute Delete
10.3 kB
<!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;
}
/* 七小欄 Grid Layout */
.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>
<!-- Firebase SDK -->
<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>
// 1. 初始化 Firebase Realtime Database
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);
}
// 2. 渲染 DOM
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);
});
// 3. 儲存資料機制(防抖動)
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);
}
// 4. 即時監聽與畫面更新
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>