iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Modern Web

Modern Web × AI《拖延怪日記》:語錄陪伴擺脫拖延系列 第 14

【Day 14】- 入門 JavaScript 網頁架設:精神狀態小測驗(radio button 單選)

  • 分享至 

  • xImage
  •  

摘要
Day 13 我們完成了「互動引導」與「歷史回顧切換」,讓使用者操作體驗更清楚。
今天要加入一個新功能:「精神狀態小測驗」。
透過早上、下午、晚上的三個小問題,讓使用者更了解自己一天中精神的變化。
這能成為後續將製作的「任務排程」基礎,例如:早上高效 → 建議安排行動任務。

為什麼要加精神狀態小測驗?

  • 自我觀察:使用者能回顧當天不同時段的精神狀態。
  • 後續銜接:這些選擇會成為「任務安排」的重要依據。
  • 程式學習:練習 radio button 群組、事件監聽、狀態儲存(localStorage)。

學習重點

  • Radio Button 單選:透過 name 屬性將同一時段的選項歸類,確保只能選一個。
  • 事件監聽:監聽 change 事件,立即回饋使用者的選擇。
  • 狀態儲存:用 localStorage 保存三個時段的答案,重整後能自動回復。
  • 即時回饋:在下方 <p> 立即顯示「今天整體回顧 → 早上:清醒; 下午:疲憊; 晚上:睏了」等訊息。
  • 區塊切換:整合到既有的 showPage(),可以從「精神引導」跳到測驗,或從測驗跳到「歷史回顧」。

核心流程

  1. 顯示測驗:按下「當然!」(btnYes)→ showPage('moodTest') → 顯示小測驗區塊。
  2. 選擇狀態:在「早上/下午/晚上」各選一個 radio button。
  3. 即時更新:change 事件觸發 → 呼叫 saveMood() 存入 localStorage → 呼叫 showFeedback() 單次更新、showAggregateFeedback() 總結。
  4. 自動回復:重新整理頁面時執行 loadMood(),回復之前的選擇。
  5. 切換歷史回顧:按「歷史回顧 →」按鈕 → showPage('history') → 渲染歷史清單。

實作

HTML:

<!-- [Day14-NEW] 精神狀態小測驗 -->
<section id="moodTestSection"
         role="region"
         aria-labelledby="mood-title"
         hidden>
  <h2 id="mood-title">精神狀態小測驗</h2>
  <p>太好了!你今天踏出了不一樣的一步 💪</p>
  <p id="mood-intro">回憶一下,今天這三個時段你的精神狀態怎麼樣?</p>

  <!-- 早上 -->
  <fieldset aria-describedby="mood-intro">
    <legend>早上</legend>
    <div>
      <input id="m-morning-awake" type="radio" name="morning" value="清醒">
      <label for="m-morning-awake">☀️ 清醒</label>
    </div>
    <div>
      <input id="m-morning-ok" type="radio" name="morning" value="普通">
      <label for="m-morning-ok">😑 普通</label>
    </div>
    <div>
      <input id="m-morning-drowsy" type="radio" name="morning" value="昏沉">
      <label for="m-morning-drowsy">😴 昏沉</label>
    </div>
  </fieldset>

  <!-- 下午 -->
  <fieldset aria-describedby="mood-intro">
    <legend>下午</legend>
    <div>
      <input id="m-afternoon-focus" type="radio" name="afternoon" value="高效">
      <label for="m-afternoon-focus">🧠 高效</label>
    </div>
    <div>
      <input id="m-afternoon-tired" type="radio" name="afternoon" value="疲憊">
      <label for="m-afternoon-tired">☕️ 有點疲憊</label>
    </div>
    <div>
      <input id="m-afternoon-distract" type="radio" name="afternoon" value="分心">
      <label for="m-afternoon-distract">🌪️ 分心</label>
    </div>
  </fieldset>

  <!-- 晚上 -->
  <fieldset aria-describedby="mood-intro">
    <legend>晚上</legend>
    <div>
      <input id="m-evening-great" type="radio" name="evening" value="精神超好">
      <label for="m-evening-great">😈 精神超好</label>
    </div>
    <div>
      <input id="m-evening-chill" type="radio" name="evening" value="想放空">
      <label for="m-evening-chill">🛋️ 想放空</label>
    </div>
    <div>
      <input id="m-evening-sleepy" type="radio" name="evening" value="睏了">
      <label for="m-evening-sleepy">🌃 睏了</label>
    </div>
  </fieldset>

  <!-- 以 polite 讀出最新整體回饋 -->
  <p id="moodFeedback" aria-live="polite"></p>

  <div style="margin-top:1rem;">
    <button id="btnHistoryFromMood" aria-controls="historySection">歷史回顧 →</button>
  </div>
</section>

JavaScript:

// === 既有節點(請確認外部已有下列節點) ===
const formSection    = document.getElementById('recordFormSection');
const historySection = document.getElementById('historySection');
const btnYes         = document.getElementById('btnYes'); // 互動引導區的「當然!」按鈕

// === Day14 新增節點 ===
const moodTestSection   = document.getElementById('moodTestSection');
const moodTitle         = document.getElementById('mood-title');
const moodFeedback      = document.getElementById('moodFeedback');
const btnHistoryFromMood= document.getElementById('btnHistoryFromMood');

const MOOD_KEY = 'daily_mood';
const PERIOD_LABEL = { morning: '早上', afternoon: '下午', evening: '晚上' };

// ---- 安全 JSON 解析 ----
function safeParseJSON(raw, fallback) {
  try { return raw ? JSON.parse(raw) : fallback; }
  catch { return fallback; }
}

// ---- 顯示/切換主要區塊 ----
// 主要功能:隱藏舊區塊,只顯示指定的新區塊
// 可選功能(a11y):切換後自動將焦點移到新區塊標題,方便螢幕閱讀器朗讀與鍵盤操作
function showPage(page) {
  // 先全部隱藏
  if (formSection)    formSection.hidden = true;
  if (historySection) historySection.hidden = true;
  if (moodTestSection) moodTestSection.hidden = true;

  // 再依 page 顯示對應的區塊
  if (page === 'history') {
    if (historySection) {
      historySection.hidden = false;
      // 若程式已有 renderHistory,這裡呼叫重新渲染清單
      if (typeof renderHistory === 'function') renderHistory();

      // (可選 a11y)切換後把焦點移到標題,方便螢幕閱讀器朗讀
      const h = document.getElementById('history-title');
      (h?.focus?.()) || historySection.setAttribute('tabindex', '-1') && historySection.focus();
    }
  } else if (page === 'moodTest') {
    if (moodTestSection) {
      moodTestSection.hidden = false;
      // (可選 a11y)同樣將焦點放到標題或區塊本身
      (moodTitle?.focus?.()) || (moodTestSection.setAttribute('tabindex', '-1'), moodTestSection.focus());
    }
  } else {
    if (formSection) {
      formSection.hidden = false;
      // 若有 focusFirstField,優先把焦點放到表單的第一個輸入框
      if (typeof focusFirstField === 'function') focusFirstField();
      // (可選 a11y)否則讓整個表單區塊可聚焦並帶入焦點
      else formSection.setAttribute('tabindex', '-1'), formSection.focus();
    }
  }
}

// ---- 儲存單一時段 ----
function saveMood(period, value) {
  const moodData = safeParseJSON(localStorage.getItem(MOOD_KEY), {});
  moodData[period] = value;
  localStorage.setItem(MOOD_KEY, JSON.stringify(moodData));
}

// ---- 載入並還原所有勾選 ----
function loadMood() {
  const moodData = safeParseJSON(localStorage.getItem(MOOD_KEY), {});
  Object.keys(moodData).forEach(period => {
    const v = moodData[period];
    // 使用更穩健的選擇器:屬性值需加引號
    const input = document.querySelector(`input[name="${period}"][value="${v}"]`);
    if (input) input.checked = true;
  });
  showAggregateFeedback(moodData); // 載入時就更新整體回饋
}

// ---- (可選)單次改變時即時文字回饋:顯示「某時段:某狀態」 ----
function showFeedback(period, value) {
  if (!moodFeedback) return;
  moodFeedback.textContent = `你在【${PERIOD_LABEL[period] || period}】覺得:「${value}」`;
}

// ---- (可選)整體回饋摘要:同時顯示三個時段,提升使用者回顧與螢幕報讀體驗 ----
function showAggregateFeedback(moodData) {
  if (!moodFeedback) return;
  const parts = ['morning', 'afternoon', 'evening']
    .filter(p => moodData[p])
    .map(p => `${PERIOD_LABEL[p]}:${moodData[p]}`);
  moodFeedback.textContent = parts.length
    ? `今天整體回顧 → ${parts.join('; ')}`
    : '(還沒有選擇,先從上面選一個吧)';
}

// ---- 事件綁定 ----
btnYes?.addEventListener('click', () => showPage('moodTest'));

btnHistoryFromMood?.addEventListener('click', () => showPage('history'));

document
  .querySelectorAll('#moodTestSection input[type="radio"]')
  .forEach(radio => {
    radio.addEventListener('change', (e) => {
      const { name, value } = e.target;
      saveMood(name, value);
      showFeedback(name, value); // 單次即時
      // 同步更新整體摘要,方便螢幕報讀一次掌握
      const moodData = safeParseJSON(localStorage.getItem(MOOD_KEY), {});
      showAggregateFeedback(moodData);
    });
  });

// ---- 頁面載入時恢復狀態 ----
loadMood();

驗證

  1. 點「當然!」 → 進入精神狀態小測驗畫面。
  2. 在「早上」選「清醒」 → 下方立即顯示:「今天整體回顧 → 早上:清醒」。
  3. 切換「下午/晚上」選項 → 即時顯示最新回饋。
  4. 重整頁面 → 先前勾選的 radio button 自動保持勾選。
  5. 點「歷史回顧 →」 → 成功切換到歷史區塊,並能看到紀錄清單。

常見錯誤 & 排查

  • 選項無法單選 → 確認同一時段的 radio button name 屬性一致(如 name="morning")。
  • 沒有即時顯示回饋 → 檢查 change 事件是否正確綁定在所有 input[type=radio]。
  • 重整後答案消失 → 檢查 saveMood() 是否正確寫入 localStorage,以及 loadMood() 是否在頁面初始化時呼叫。
  • 切換頁面顯示錯亂 → 確認 showPage() 內有正確隱藏/顯示 formSection、historySection、moodTestSection。
  • feedback 沒有更新 → 檢查 id="moodFeedback" 是否存在,或是否被覆蓋為空字串。

上一篇
【Day 13】- 入門 JavaScript 網頁架設:互動引導與歷史回顧切換
下一篇
【Day 15】- 入門 JavaScript 網頁架設:多時段狀態匯總與分支導航
系列文
Modern Web × AI《拖延怪日記》:語錄陪伴擺脫拖延19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言