基本的功能感覺已經差不多做完了,所以我決定剩下三天來優化現有的功能跟排版。
先從昨天的部分延續好了,有關密碼的部分,目前是輸入什麼作為密碼都可以,長度、大小寫都可以,但以現在的資安理論與駭客的程度是不行的,所以要加上一些限制:
主要顯示使用者輸入密碼的強度
/* ===== 密碼強度條 ===== */
.pw-wrap { width:100%; max-width:420px; margin-top:6px; }
.pw-meter {
height:8px; background:#e5e7eb; border-radius:6px; overflow:hidden;
box-shadow: inset 0 1px 2px rgba(0,0,0,.06);
}
.pw-bar { height:100%; width:0%; background:#ef4444; transition:width .25s ease; }
.pw-label { margin-top:6px; font-size:.92rem; font-weight:600; }
.pw-tips { margin-top:6px; font-size:.9rem; color:#666; }
.pw-tips ul { margin:4px 0 0 16px; padding:0; }
.pw-tips li { margin:2px 0; }
/***********************
* 密碼強度檢查
***********************/
(function setupPasswordStrength() {
// 1) 規則與評分
const COMMON = new Set([
"123456","12345678","123456789","password","qwerty","111111",
"abc123","123123","iloveyou","000000","admin","welcome"
]);
function calcStrength(pw, ctx = {}) {
const tips = [];
if (!pw) return {score:0, percent:0, label:"空白", color:"#ef4444", tips:["請輸入密碼"]};
const hasLower = /[a-z]/.test(pw);
const hasUpper = /[A-Z]/.test(pw);
const hasDigit = /\d/.test(pw);
const hasSymbol = /[^A-Za-z0-9]/.test(pw);
const length = pw.length;
const repeats = /(.)\1{2,}/.test(pw); // 三連重複
const spaces = /\s/.test(pw);
let score = 0;
// 基礎長度
if (length >= 8) score += 1; else tips.push("長度至少 8 字元");
if (length >= 12) score += 1;
// 字元多樣性
const kinds = [hasLower, hasUpper, hasDigit, hasSymbol].filter(Boolean).length;
if (kinds >= 2) score += 1; else tips.push("混用大小寫 / 數字 / 符號");
if (kinds >= 3) score += 1;
// 懲罰
if (COMMON.has(pw.toLowerCase())) { score -= 2; tips.push("避免常見密碼"); }
if (repeats) { score -= 1; tips.push("避免連續重複字元"); }
if (spaces) { score -= 1; tips.push("避免使用空白"); }
// 與帳號、舊密碼的關聯
if (ctx.username && pw.toLowerCase().includes(String(ctx.username).toLowerCase())) {
score -= 2; tips.push("密碼請勿包含帳號");
}
if (ctx.oldPassword && pw === ctx.oldPassword) {
score -= 2; tips.push("新密碼不能與舊密碼相同");
}
// 正規化
score = Math.max(0, Math.min(5, score));
const percent = [0, 25, 50, 70, 85, 100][score];
const palette = ["#ef4444","#f59e0b","#eab308","#22c55e","#16a34a","#15803d"];
const labels = ["極弱","弱","普通","良好","強","超強"];
return {score, percent, color: palette[score], label: labels[score], tips};
}
// 2) 渲染/掛載
function mountMeterFor(input, ctx = {}) {
if (!input || input.dataset.pwMeterMounted === "1") return;
input.dataset.pwMeterMounted = "1";
// 容器:插在密碼輸入框後
const wrap = document.createElement("div");
wrap.className = "pw-wrap";
wrap.innerHTML = `
<div class="pw-meter"><div class="pw-bar"></div></div>
<div class="pw-label">強度:—</div>
<div class="pw-tips"></div>
`;
input.insertAdjacentElement("afterend", wrap);
const bar = wrap.querySelector(".pw-bar");
const label = wrap.querySelector(".pw-label");
const tipsBox = wrap.querySelector(".pw-tips");
function render() {
const pw = input.value;
const info = calcStrength(pw, typeof ctx === "function" ? ctx() : ctx);
bar.style.width = info.percent + "%";
bar.style.background = info.color;
label.textContent = `強度:${info.label}`;
// 只顯示最多 3 條建議
if (info.tips && info.tips.length) {
tipsBox.innerHTML = "<ul>" + info.tips.slice(0,3).map(t => `<li>${t}</li>`).join("") + "</ul>";
} else {
tipsBox.innerHTML = "";
}
return info;
}
input.addEventListener("input", render);
render(); // 初始化
return { render, getInfo: () => calcStrength(input.value, ctx) };
}
// 3) 自動掛到三個場景
document.addEventListener("DOMContentLoaded", () => {
// signup.html
const signupPwd = document.getElementById("password");
const signupUser = document.getElementById("username");
if (signupPwd) {
mountMeterFor(signupPwd, () => ({ username: signupUser ? signupUser.value : "" }));
}
// reset.html(新密碼)
const resetNew = document.getElementById("newPassword");
const resetUserInput = document.getElementById("resetUsername");
if (resetNew) {
mountMeterFor(resetNew, () => ({ username: resetUserInput ? resetUserInput.value : "" }));
}
// account.html 的修改密碼(舊/新/確認)
const oldPwd = document.getElementById("oldPassword");
const newPwd = document.getElementById("newPassword");
if (newPwd) {
// 嘗試讀出目前登入者(放在 localStorage 裡)
const loggedInUser = localStorage.getItem("loggedInUser") || "";
// 試著同步舊密碼(若頁面上找得到)
let currentPassword = "";
try {
const users = JSON.parse(localStorage.getItem("users") || "{}");
if (loggedInUser && users[loggedInUser]) currentPassword = users[loggedInUser].password || "";
} catch {}
mountMeterFor(newPwd, { username: loggedInUser, oldPassword: oldPwd ? oldPwd.value : currentPassword });
// 若使用者輸入舊密碼,更新一次提示(避免「新舊相同」)
if (oldPwd) oldPwd.addEventListener("input", () => newPwd.dispatchEvent(new Event("input")));
}
});
})();
// 第三步:設定新密碼(放在 script.js)
resetPasswordBtn.addEventListener("click", function () {
const users = JSON.parse(localStorage.getItem("users") || "{}");
const newPassword = (newPasswordInput.value || "").trim();
if (!currentUser || !users[currentUser]) return;
// 強度檢查
if (!newPassword) {
resetMessage.textContent = "新密碼不可為空!";
resetMessage.style.color = "red";
return;
}
const hasLower = /[a-z]/.test(newPassword),
hasUpper = /[A-Z]/.test(newPassword),
hasDigit = /\d/.test(newPassword),
hasSymbol = /[^A-Za-z0-9]/.test(newPassword);
let score = 0;
if (newPassword.length >= 8) score++;
if (newPassword.length >= 12) score++;
const kinds = [hasLower, hasUpper, hasDigit, hasSymbol].filter(Boolean).length;
if (kinds >= 2) score++;
if (kinds >= 3) score++;
if (score < 3) {
resetMessage.textContent = "密碼太弱,請至少 8 碼並混用大小寫/數字/符號。";
resetMessage.style.color = "red";
return;
}
// 寫回
users[currentUser].password = newPassword;
localStorage.setItem("users", JSON.stringify(users));
resetMessage.textContent = "密碼已成功重設!請回到登入頁";
resetMessage.style.color = "green";
// 收尾
newPasswordSection.classList.add("hidden");
securitySection.classList.add("hidden");
resetForm.reset();
});