Update index.html
Browse files- index.html +56 -24
index.html
CHANGED
|
@@ -29,7 +29,15 @@
|
|
| 29 |
h1 {
|
| 30 |
text-align: center;
|
| 31 |
color: #2c3e50;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
margin-bottom: 20px;
|
|
|
|
| 33 |
}
|
| 34 |
|
| 35 |
/* 頂部交集結果區塊 */
|
|
@@ -168,7 +176,6 @@
|
|
| 168 |
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
| 169 |
}
|
| 170 |
|
| 171 |
-
/* 響應式微調 */
|
| 172 |
@media (max-width: 768px) {
|
| 173 |
.members-grid {
|
| 174 |
grid-template-columns: repeat(2, 1fr);
|
|
@@ -176,7 +183,7 @@
|
|
| 176 |
}
|
| 177 |
</style>
|
| 178 |
|
| 179 |
-
<!-- Firebase
|
| 180 |
<script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-app-compat.js"></script>
|
| 181 |
<script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-database-compat.js"></script>
|
| 182 |
</head>
|
|
@@ -184,12 +191,13 @@
|
|
| 184 |
|
| 185 |
<div class="container">
|
| 186 |
<h1>📅 7人空檔時間協調行事曆 (8/14 - 9/2)</h1>
|
|
|
|
| 187 |
|
| 188 |
<!-- 共同有空交集區塊 -->
|
| 189 |
<div class="intersection-card">
|
| 190 |
<h2>🎉 全員皆有空的日期(交集):</h2>
|
| 191 |
<div id="intersection-results" class="available-tags">
|
| 192 |
-
<span class="no-match">
|
| 193 |
</div>
|
| 194 |
</div>
|
| 195 |
|
|
@@ -206,12 +214,11 @@
|
|
| 206 |
const db = firebase.database();
|
| 207 |
const scheduleRef = db.ref("schedule_2026");
|
| 208 |
|
| 209 |
-
// 2. 設定參數
|
| 210 |
const members = ["桑", "吳", "張", "趙", "謝", "黃", "康"];
|
| 211 |
const startDate = new Date("2026-08-14");
|
| 212 |
const endDate = new Date("2026-09-02");
|
| 213 |
|
| 214 |
-
// 生成日期
|
| 215 |
const dateList = [];
|
| 216 |
let curr = new Date(startDate);
|
| 217 |
while (curr <= endDate) {
|
|
@@ -227,8 +234,9 @@
|
|
| 227 |
curr.setDate(curr.getDate() + 1);
|
| 228 |
}
|
| 229 |
|
| 230 |
-
//
|
| 231 |
const calendarContainer = document.getElementById("calendar");
|
|
|
|
| 232 |
|
| 233 |
dateList.forEach(({ dateStr, displayStr }) => {
|
| 234 |
const dateCard = document.createElement("div");
|
|
@@ -242,11 +250,11 @@
|
|
| 242 |
<div class="member-name-row">
|
| 243 |
<span>${name}</span>
|
| 244 |
<label>
|
| 245 |
-
<input type="checkbox" id="chk-${dateStr}-${name}" onchange="
|
| 246 |
有空
|
| 247 |
</label>
|
| 248 |
</div>
|
| 249 |
-
<input type="text" id="txt-${dateStr}-${name}" placeholder="
|
| 250 |
</div>
|
| 251 |
`;
|
| 252 |
});
|
|
@@ -254,7 +262,7 @@
|
|
| 254 |
dateCard.innerHTML = `
|
| 255 |
<div class="date-header">
|
| 256 |
<span>${displayStr}</span>
|
| 257 |
-
<span class="status-badge" id="badge-${dateStr}">
|
| 258 |
</div>
|
| 259 |
<div class="members-grid">
|
| 260 |
${membersHTML}
|
|
@@ -264,19 +272,40 @@
|
|
| 264 |
calendarContainer.appendChild(dateCard);
|
| 265 |
});
|
| 266 |
|
| 267 |
-
//
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
}
|
| 277 |
|
| 278 |
-
//
|
| 279 |
scheduleRef.on("value", (snapshot) => {
|
|
|
|
|
|
|
|
|
|
| 280 |
const data = snapshot.val() || {};
|
| 281 |
const fullAvailableDates = [];
|
| 282 |
|
|
@@ -290,7 +319,7 @@
|
|
| 290 |
const chkEl = document.getElementById(`chk-${dateStr}-${name}`);
|
| 291 |
const txtEl = document.getElementById(`txt-${dateStr}-${name}`);
|
| 292 |
|
| 293 |
-
//
|
| 294 |
if (document.activeElement !== txtEl) {
|
| 295 |
txtEl.value = memberData.note || "";
|
| 296 |
}
|
|
@@ -304,10 +333,9 @@
|
|
| 304 |
const cardEl = document.getElementById(`card-${dateStr}`);
|
| 305 |
const badgeEl = document.getElementById(`badge-${dateStr}`);
|
| 306 |
|
| 307 |
-
// 比對交集:7 個人都勾選有空
|
| 308 |
if (availableCount === members.length) {
|
| 309 |
cardEl.classList.add("all-available");
|
| 310 |
-
badgeEl.innerText = "全員皆有空!";
|
| 311 |
fullAvailableDates.push(displayStr);
|
| 312 |
} else {
|
| 313 |
cardEl.classList.remove("all-available");
|
|
@@ -315,15 +343,19 @@
|
|
| 315 |
}
|
| 316 |
});
|
| 317 |
|
| 318 |
-
// 更新頂部交集
|
| 319 |
const resultsContainer = document.getElementById("intersection-results");
|
| 320 |
if (fullAvailableDates.length > 0) {
|
| 321 |
resultsContainer.innerHTML = fullAvailableDates
|
| 322 |
.map(d => `<span class="tag">${d}</span>`)
|
| 323 |
.join("");
|
| 324 |
} else {
|
| 325 |
-
resultsContainer.innerHTML = `<span class="no-match">目前尚無所有人都有空的日期
|
| 326 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
});
|
| 328 |
</script>
|
| 329 |
</body>
|
|
|
|
| 29 |
h1 {
|
| 30 |
text-align: center;
|
| 31 |
color: #2c3e50;
|
| 32 |
+
margin-bottom: 10px;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.status-indicator {
|
| 36 |
+
text-align: center;
|
| 37 |
+
font-size: 0.9rem;
|
| 38 |
+
color: #666;
|
| 39 |
margin-bottom: 20px;
|
| 40 |
+
height: 20px;
|
| 41 |
}
|
| 42 |
|
| 43 |
/* 頂部交集結果區塊 */
|
|
|
|
| 176 |
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
| 177 |
}
|
| 178 |
|
|
|
|
| 179 |
@media (max-width: 768px) {
|
| 180 |
.members-grid {
|
| 181 |
grid-template-columns: repeat(2, 1fr);
|
|
|
|
| 183 |
}
|
| 184 |
</style>
|
| 185 |
|
| 186 |
+
<!-- Firebase SDK -->
|
| 187 |
<script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-app-compat.js"></script>
|
| 188 |
<script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-database-compat.js"></script>
|
| 189 |
</head>
|
|
|
|
| 191 |
|
| 192 |
<div class="container">
|
| 193 |
<h1>📅 7人空檔時間協調行事曆 (8/14 - 9/2)</h1>
|
| 194 |
+
<div id="sync-status" class="status-indicator">📡 連線中...</div>
|
| 195 |
|
| 196 |
<!-- 共同有空交集區塊 -->
|
| 197 |
<div class="intersection-card">
|
| 198 |
<h2>🎉 全員皆有空的日期(交集):</h2>
|
| 199 |
<div id="intersection-results" class="available-tags">
|
| 200 |
+
<span class="no-match">讀取中...</span>
|
| 201 |
</div>
|
| 202 |
</div>
|
| 203 |
|
|
|
|
| 214 |
const db = firebase.database();
|
| 215 |
const scheduleRef = db.ref("schedule_2026");
|
| 216 |
|
|
|
|
| 217 |
const members = ["桑", "吳", "張", "趙", "謝", "黃", "康"];
|
| 218 |
const startDate = new Date("2026-08-14");
|
| 219 |
const endDate = new Date("2026-09-02");
|
| 220 |
|
| 221 |
+
// 生成日期列表
|
| 222 |
const dateList = [];
|
| 223 |
let curr = new Date(startDate);
|
| 224 |
while (curr <= endDate) {
|
|
|
|
| 234 |
curr.setDate(curr.getDate() + 1);
|
| 235 |
}
|
| 236 |
|
| 237 |
+
// 2. 渲染 DOM
|
| 238 |
const calendarContainer = document.getElementById("calendar");
|
| 239 |
+
const syncStatus = document.getElementById("sync-status");
|
| 240 |
|
| 241 |
dateList.forEach(({ dateStr, displayStr }) => {
|
| 242 |
const dateCard = document.createElement("div");
|
|
|
|
| 250 |
<div class="member-name-row">
|
| 251 |
<span>${name}</span>
|
| 252 |
<label>
|
| 253 |
+
<input type="checkbox" id="chk-${dateStr}-${name}" onchange="saveData('${dateStr}', '${name}')">
|
| 254 |
有空
|
| 255 |
</label>
|
| 256 |
</div>
|
| 257 |
+
<input type="text" id="txt-${dateStr}-${name}" placeholder="輸入時間..." oninput="saveData('${dateStr}', '${name}')">
|
| 258 |
</div>
|
| 259 |
`;
|
| 260 |
});
|
|
|
|
| 262 |
dateCard.innerHTML = `
|
| 263 |
<div class="date-header">
|
| 264 |
<span>${displayStr}</span>
|
| 265 |
+
<span class="status-badge" id="badge-${dateStr}">計算中...</span>
|
| 266 |
</div>
|
| 267 |
<div class="members-grid">
|
| 268 |
${membersHTML}
|
|
|
|
| 272 |
calendarContainer.appendChild(dateCard);
|
| 273 |
});
|
| 274 |
|
| 275 |
+
// 3. 儲存資料機制(防抖動)
|
| 276 |
+
const debounceTimers = {};
|
| 277 |
+
function saveData(dateStr, memberName) {
|
| 278 |
+
syncStatus.innerText = "⏳ 儲存中...";
|
| 279 |
+
syncStatus.style.color = "#e67e22";
|
| 280 |
+
|
| 281 |
+
const key = `${dateStr}-${memberName}`;
|
| 282 |
+
clearTimeout(debounceTimers[key]);
|
| 283 |
+
|
| 284 |
+
debounceTimers[key] = setTimeout(() => {
|
| 285 |
+
const isAvailable = document.getElementById(`chk-${dateStr}-${memberName}`).checked;
|
| 286 |
+
const textNote = document.getElementById(`txt-${dateStr}-${memberName}`).value;
|
| 287 |
+
|
| 288 |
+
scheduleRef.child(dateStr).child(memberName).set({
|
| 289 |
+
available: isAvailable,
|
| 290 |
+
note: textNote
|
| 291 |
+
}, (error) => {
|
| 292 |
+
if (error) {
|
| 293 |
+
syncStatus.innerText = "❌ 儲存失敗!請檢查 Firebase 規則";
|
| 294 |
+
syncStatus.style.color = "#e74c3c";
|
| 295 |
+
console.error("Firebase 保存失敗:", error);
|
| 296 |
+
} else {
|
| 297 |
+
syncStatus.innerText = "✅ 所有變更已即時同步保存";
|
| 298 |
+
syncStatus.style.color = "#27ae60";
|
| 299 |
+
}
|
| 300 |
+
});
|
| 301 |
+
}, 300);
|
| 302 |
}
|
| 303 |
|
| 304 |
+
// 4. 即時監聽與畫面更新
|
| 305 |
scheduleRef.on("value", (snapshot) => {
|
| 306 |
+
syncStatus.innerText = "✅ 資料庫已連線,即時同步中";
|
| 307 |
+
syncStatus.style.color = "#27ae60";
|
| 308 |
+
|
| 309 |
const data = snapshot.val() || {};
|
| 310 |
const fullAvailableDates = [];
|
| 311 |
|
|
|
|
| 319 |
const chkEl = document.getElementById(`chk-${dateStr}-${name}`);
|
| 320 |
const txtEl = document.getElementById(`txt-${dateStr}-${name}`);
|
| 321 |
|
| 322 |
+
// 若使用者當前正在輸入該格,不打斷輸入
|
| 323 |
if (document.activeElement !== txtEl) {
|
| 324 |
txtEl.value = memberData.note || "";
|
| 325 |
}
|
|
|
|
| 333 |
const cardEl = document.getElementById(`card-${dateStr}`);
|
| 334 |
const badgeEl = document.getElementById(`badge-${dateStr}`);
|
| 335 |
|
|
|
|
| 336 |
if (availableCount === members.length) {
|
| 337 |
cardEl.classList.add("all-available");
|
| 338 |
+
badgeEl.innerText = "🎉 全員皆有空!";
|
| 339 |
fullAvailableDates.push(displayStr);
|
| 340 |
} else {
|
| 341 |
cardEl.classList.remove("all-available");
|
|
|
|
| 343 |
}
|
| 344 |
});
|
| 345 |
|
| 346 |
+
// 更新頂部交集
|
| 347 |
const resultsContainer = document.getElementById("intersection-results");
|
| 348 |
if (fullAvailableDates.length > 0) {
|
| 349 |
resultsContainer.innerHTML = fullAvailableDates
|
| 350 |
.map(d => `<span class="tag">${d}</span>`)
|
| 351 |
.join("");
|
| 352 |
} else {
|
| 353 |
+
resultsContainer.innerHTML = `<span class="no-match">目前尚無所有人都有空的日期</span>`;
|
| 354 |
}
|
| 355 |
+
}, (error) => {
|
| 356 |
+
syncStatus.innerText = "❌ 無法讀取資料,請至 Firebase 設定 Rules 權限!";
|
| 357 |
+
syncStatus.style.color = "#e74c3c";
|
| 358 |
+
console.error("Firebase 讀取失敗:", error);
|
| 359 |
});
|
| 360 |
</script>
|
| 361 |
</body>
|