const USERS_KEY = "mobile-punch-users-v1"; const SESSION_KEY = "mobile-punch-current-user-v1"; const LEGACY_RECORDS_KEY = "mobile-punch-records-v1"; const loginView = document.querySelector("#loginView"); const appView = document.querySelector("#appView"); const loginForm = document.querySelector("#loginForm"); const usernameInput = document.querySelector("#usernameInput"); const passwordInput = document.querySelector("#passwordInput"); const userText = document.querySelector("#userText"); const logoutBtn = document.querySelector("#logoutBtn"); const todayText = document.querySelector("#todayText"); const clockText = document.querySelector("#clockText"); const clockInBtn = document.querySelector("#clockInBtn"); const clockOutBtn = document.querySelector("#clockOutBtn"); const clockInTime = document.querySelector("#clockInTime"); const clockOutTime = document.querySelector("#clockOutTime"); const clearBtn = document.querySelector("#clearBtn"); const recordList = document.querySelector("#recordList"); const emptyState = document.querySelector("#emptyState"); const toast = document.querySelector("#toast"); let users = loadUsers(); let currentUser = localStorage.getItem(SESSION_KEY) || ""; let records = currentUser ? getUserRecords(currentUser) : {}; let toastTimer = 0; function pad(value) { return String(value).padStart(2, "0"); } function getDateKey(date = new Date()) { return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; } function getTimeText(date = new Date()) { return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; } function getDateText(date = new Date()) { return new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "long", day: "numeric", weekday: "long" }).format(date); } function loadUsers() { try { const stored = localStorage.getItem(USERS_KEY); return stored ? JSON.parse(stored) : {}; } catch { return {}; } } function saveUsers() { localStorage.setItem(USERS_KEY, JSON.stringify(users)); } function normalizeUsername(value) { return value.trim(); } function getUserRecords(username) { return users[username]?.records || {}; } function saveRecords() { if (!currentUser) { return; } users[currentUser].records = records; saveUsers(); } function migrateLegacyRecords(username) { const legacy = localStorage.getItem(LEGACY_RECORDS_KEY); if (!legacy || Object.keys(users[username].records).length > 0) { return; } try { users[username].records = JSON.parse(legacy); localStorage.removeItem(LEGACY_RECORDS_KEY); } catch { users[username].records = {}; } } function ensureTodayRecord() { const key = getDateKey(); if (!records[key]) { records[key] = { clockIn: "", clockOut: "" }; } return records[key]; } function punch(type) { const now = new Date(); const record = ensureTodayRecord(); record[type] = now.toISOString(); saveRecords(); render(); showToast(type === "clockIn" ? "上班打卡成功" : "下班打卡成功"); } function formatStoredTime(value) { if (!value) { return "未打卡"; } return getTimeText(new Date(value)); } function renderToday() { if (!currentUser) { return; } const now = new Date(); const record = records[getDateKey(now)] || {}; userText.textContent = `${currentUser} · 今日打卡`; todayText.textContent = getDateText(now); clockText.textContent = getTimeText(now); clockInTime.textContent = formatStoredTime(record.clockIn); clockOutTime.textContent = formatStoredTime(record.clockOut); } function deleteRecord(dateKey) { if (!confirm(`确定删除 ${dateKey} 的打卡记录吗?`)) { return; } delete records[dateKey]; saveRecords(); render(); showToast("记录已删除"); } function renderRecords() { const entries = Object.entries(records) .sort(([left], [right]) => right.localeCompare(left)); recordList.innerHTML = ""; emptyState.hidden = entries.length > 0; for (const [dateKey, record] of entries) { const item = document.createElement("li"); item.className = "record-item"; const date = document.createElement("div"); date.className = "record-date"; date.textContent = dateKey; const tag = document.createElement("div"); tag.className = "record-tag"; tag.textContent = dateKey === getDateKey() ? "今天" : "历史"; const time = document.createElement("div"); time.className = "record-time"; time.textContent = `上班 ${formatStoredTime(record.clockIn)} · 下班 ${formatStoredTime(record.clockOut)}`; const deleteButton = document.createElement("button"); deleteButton.className = "delete-button"; deleteButton.type = "button"; deleteButton.textContent = "删除"; deleteButton.setAttribute("aria-label", `删除 ${dateKey} 的打卡记录`); deleteButton.addEventListener("click", () => deleteRecord(dateKey)); item.append(date, tag, time, deleteButton); recordList.append(item); } } function render() { if (!currentUser) { loginView.hidden = false; appView.hidden = true; return; } loginView.hidden = true; appView.hidden = false; records = getUserRecords(currentUser); renderToday(); renderRecords(); } function showToast(message) { window.clearTimeout(toastTimer); toast.textContent = message; toast.classList.add("is-visible"); toastTimer = window.setTimeout(() => { toast.classList.remove("is-visible"); }, 1800); } loginForm.addEventListener("submit", (event) => { event.preventDefault(); const username = normalizeUsername(usernameInput.value); const password = passwordInput.value; if (!username || !password) { showToast("请输入用户名和密码"); return; } if (users[username] && users[username].password !== password) { showToast("密码不正确"); return; } if (!users[username]) { users[username] = { password, records: {} }; migrateLegacyRecords(username); saveUsers(); showToast("账号已创建并登录"); } else { showToast("登录成功"); } currentUser = username; localStorage.setItem(SESSION_KEY, currentUser); passwordInput.value = ""; render(); }); logoutBtn.addEventListener("click", () => { currentUser = ""; records = {}; localStorage.removeItem(SESSION_KEY); render(); usernameInput.focus(); }); clockInBtn.addEventListener("click", () => punch("clockIn")); clockOutBtn.addEventListener("click", () => punch("clockOut")); clearBtn.addEventListener("click", () => { if (!confirm("确定要清空当前用户的所有打卡记录吗?")) { return; } records = {}; saveRecords(); render(); showToast("记录已清空"); }); render(); window.setInterval(renderToday, 1000);