iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
自我挑戰組

打造一個糖尿病自我監測小工具:從0開始學前端系列 第 27

Day27打造一個糖尿病自我監測小工具:從0開始學前端

  • 分享至 

  • xImage
  •  

基本的功能感覺已經差不多做完了,所以我決定剩下三天來優化現有的功能跟排版。

先從昨天的部分延續好了,有關密碼的部分,目前是輸入什麼作為密碼都可以,長度、大小寫都可以,但以現在的資安理論與駭客的程度是不行的,所以要加上一些限制:

  1. 即時顯示強度條
  2. 提示規則
  3. 長度≥8
  4. 含數字/大小寫

Style.css

主要顯示使用者輸入密碼的強度

/* ===== 密碼強度條 ===== */
.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; }

JS 邏輯

/***********************
 * 密碼強度檢查
 ***********************/
(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")));
    }
  });
})();

reset 的 JS 邏輯

// 第三步:設定新密碼(放在 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();
});

實作成果

  • 密碼建議
    https://ithelp.ithome.com.tw/upload/images/20250927/20169698G3FNZFiTwV.png
    https://ithelp.ithome.com.tw/upload/images/20250927/20169698qlPcX5WJ7n.png
    https://ithelp.ithome.com.tw/upload/images/20250927/20169698NG5La3paZ5.png
    https://ithelp.ithome.com.tw/upload/images/20250927/20169698ieQUPjH3eE.png
    https://ithelp.ithome.com.tw/upload/images/20250927/2016969879MelB659d.png

上一篇
Day26打造一個糖尿病自我監測小工具:從0開始學前端
系列文
打造一個糖尿病自我監測小工具:從0開始學前端27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言