在前面的天數中,我們成功打造了具備 SPA 頁面切換、即時記帳、動態月曆、Chart.js 統計圖表與 CSV 匯出的完整記帳網頁。然而,當一個網站開始記錄使用者的財務數據(開銷、餘額、消費習慣)時,一個致命的問題就浮現了——「隱私與資安」。如果筆記型電腦借給朋友、或是與室友共用電腦,任何人打開網頁都能對你的每一筆花費一覽無遺,所以,在後半段的部分我將新增前端會員登入與密碼保護系統。
在網頁最外層加入一個全螢幕的「登入解鎖遮罩(Lock Screen UI)」。透過高權重的 z-index,在使用者輸入正確密碼前,完整保護底下的記帳數據與圖表。
<!-- Day 15:全螢幕解鎖登入遮罩 -->
<div id="lock-screen" style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: rgba(240, 244, 248, 0.95); z-index: 9999; display: flex; justify-content: center; align-items: center; flex-direction: column;">
<div style="background: white; padding: 30px 40px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); text-align: center; max-width: 350px; width: 100%;">
<h2 id="lock-title" style="margin-top: 0; color: #333;">金庫已上鎖</h2>
<p id="lock-desc" style="color: #666; font-size: 14px; margin-bottom: 20px;">請輸入您的密碼以解鎖您的專屬記帳本</p>
<input type="password" id="lock-password-input" placeholder="請輸入密碼" style="width: 100%; padding: 10px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; font-size: 16px;">
<button id="lock-submit-btn" style="width: 100%; padding: 10px; background-color: #1967d2; color: white; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 16px;">解鎖 / 設定密碼</button>
</div>
</div>
▲ 說明:利用 position: fixed 與 vw/vh 讓遮罩完美覆蓋整個畫面,搭配 z-index: 9999 確保它永遠浮在最上層,阻擋所有未授權的操作。
const lockScreen = document.getElementById('lock-screen');
const lockTitle = document.getElementById('lock-title');
const lockDesc = document.getElementById('lock-desc');
const lockPasswordInput = document.getElementById('lock-password-input');
const lockSubmitBtn = document.getElementById('lock-submit-btn');
// 初始化檢查
function initAuthSystem() {
const savedPassword = localStorage.getItem('my_vault_password');
const isUnlocked = sessionStorage.getItem('vault_unlocked');
if (!savedPassword) {
lockTitle.textContent = " 初始化您的金庫密碼";
lockDesc.textContent = "這是您第一次使用,請設定一組解鎖密碼:";
lockSubmitBtn.textContent = "確認並建立密碼";
} else if (isUnlocked === 'true') {
lockScreen.style.display = 'none';
}
}
// 點擊解鎖或設定密碼
lockSubmitBtn.addEventListener('click', function() {
const inputVal = lockPasswordInput.value.trim();
const savedPassword = localStorage.getItem('my_vault_password');
if (!inputVal) { alert("密碼不能為空白!"); return; }
if (!savedPassword) {
localStorage.setItem('my_vault_password', inputVal);
sessionStorage.setItem('vault_unlocked', 'true');
lockScreen.style.display = 'none';
alert("密碼設定成功!");
} else {
if (inputVal === savedPassword) {
sessionStorage.setItem('vault_unlocked', 'true');
lockScreen.style.display = 'none';
} else {
alert("密碼錯誤,請重新輸入!");
lockPasswordInput.value = '';
}
}
});
window.addEventListener('DOMContentLoaded', initAuthSystem);
▲ 說明:透過 localStorage 長期儲存密碼(重開瀏覽器還在),並用 sessionStorage 記錄「當前分頁是否已解鎖」,讓使用者在同一個瀏覽行程中不用重複輸入密碼。
// 支援按下 Enter 鍵快速解鎖
lockPasswordInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
lockSubmitBtn.click();
}
});
▲ 說明:監聽密碼輸入框的 keypress 事件,當偵測到鍵盤按鍵為 Enter 時,自動觸發按鈕的 .click() 行為,大幅提升表單操作的流暢度。
HTML(加在側邊欄下方):
<a id="lock-system-btn" style="margin-top: auto; background-color: #e74c3c; color: white; text-align: center; cursor: pointer; border-radius: 6px; padding: 10px;">🔒 鎖定金庫</a>
JS:
const lockSystemBtn = document.getElementById('lock-system-btn');
if (lockSystemBtn) {
lockSystemBtn.addEventListener('click', function() {
sessionStorage.removeItem('vault_unlocked');
lockPasswordInput.value = '';
lockTitle.textContent = "金庫已上鎖";
lockDesc.textContent = "請輸入您的密碼以解鎖您的專屬記帳本";
lockSubmitBtn.textContent = "解鎖";
lockScreen.style.display = 'flex';
});
}
▲ 說明:點擊後透過 sessionStorage.removeItem() 清除解鎖狀態,並將遮罩重新顯示,達到隨時隨地手動上鎖的效果。
今日要做的事:
將登入檢核與初始化邏輯進行模組化封裝,確保網頁載入時執行順序流暢,不會發生畫面閃爍或權限漏洞。
程式碼說明與省思:
將所有驗證邏輯收斂至 initAuthSystem() 與事件監聽函式中。利用 DOM 載入生命週期確保畫面元素就緒後才進行身分判斷,提升程式碼的可讀性與日後維護性。
<!-- 側邊欄選單新增「設定」 -->
<div class="sidebar">
<a class="menu-btn" data-target="settings-page">設定</a> <!-- 【新增】設定頁面按鈕 -->
</div>
<!-- 主畫面中新增設定頁面容器 -->
<div id="settings-page" class="page-view">
<div class="section-box">
<h3>帳號與資安設定</h3>
<p style="color: #666; margin-bottom: 20px;">在此管理您的金庫登入狀態與密碼保護:</p>
<div id="settings-content" style="display: flex; flex-direction: column; gap: 15px; max-width: 400px;">
</div>
</div>
</div>
▲ 說明:配合前面建立的 SPA 單頁式架構,我們在側邊欄加上 data-target="settings-page",並在右側主畫面建立對應的 ,點擊時就能平滑切換。
function renderSettingsPage() {
const settingsContainer = document.getElementById('settings-content');
if (!settingsContainer) return;
const isUnlocked = sessionStorage.getItem('vault_unlocked');
const savedPassword = localStorage.getItem('my_vault_password');
settingsContainer.innerHTML = `
<div style="padding: 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
<h4 style="margin: 0 0 10px 0; color: #333;">目前金庫狀態</h4>
<p style="margin: 0; font-size: 14px; color: ${isUnlocked === 'true' ? '#27ae60' : '#e74c3c'}; font-weight: bold;">
${isUnlocked === 'true' ? ' 目前狀態:已解鎖' : ' 目前狀態:已上鎖'}
</p>
</div>
<button id="settings-logout-btn" style="background-color: #e74c3c; color: white; padding: 10px; border: none; border-radius: 6px; font-weight: bold; cursor: pointer;">
立即登出 / 鎖定金庫
</button>
`;
// 綁定設定頁面的登出按鈕事件
document.getElementById('settings-logout-btn').addEventListener('click', function() {
sessionStorage.removeItem('vault_unlocked');
alert("已成功登出金庫!");
location.reload(); // 重新整理並跳出全螢幕解鎖畫面
});
}
// 當切換到設定頁面時觸發渲染
// (假設你的切換按鈕邏輯會呼叫頁面渲染,可在切換分頁時執行 renderSettingsPage())
▲ 說明:將原本陽春的側邊欄按鈕移除,改由「設定頁面」統一管理帳號狀態,點擊登出後會清除 sessionStorage 並重新載入畫面,強制返回全螢幕登入遮罩。
// 在 renderSettingsPage 的 HTML 內加入修改密碼區塊
/*
<div style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 20px;">
<h4 style="margin: 0 0 10px 0; color: #333;">變更解鎖密碼</h4>
<input type="password" id="old-password" placeholder="請輸入舊密碼" style="padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
<input type="password" id="new-password" placeholder="請輸入新密碼" style="padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin-top: 8px;">
<button id="change-pwd-btn" style="margin-top: 10px; background-color: #1967d2; color: white; padding: 8px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">確認修改密碼</button>
</div>
*/
// 綁定修改密碼事件
document.addEventListener('click', function(e) {
if (e.target && e.target.id === 'change-pwd-btn') {
const oldPwd = document.getElementById('old-password').value.trim();
const newPwd = document.getElementById('new-password').value.trim();
const savedPassword = localStorage.getItem('my_vault_password');
if (!oldPwd || !newPwd) {
alert("舊密碼與新密碼皆不能為空白!");
return;
}
if (oldPwd !== savedPassword) {
alert("舊密碼輸入錯誤,無法修改!");
return;
}
localStorage.setItem('my_vault_password', newPwd);
alert("密碼修改成功!下次登入請使用新密碼。");
document.getElementById('old-password').value = '';
document.getElementById('new-password').value = '';
}
});

