剩下最後兩天,打算來優化剩下的小地方:
record.html
自動帶入今天日期、限制輸入範圍、invalid 時禁止送出resetMessage
)加 aria-live
,螢幕閱讀器可讀report.html
如果沒有資料,顯示「尚無資料,請先到填寫頁新增」的友善提示nav
加上 id="navbar"
(讓 script.js
能在登入後插入「提醒」「頭像」)fieldset/legend
、aria-live
的訊息區id
與 hidden
/disabled
控制,完全相容現在的 script.js
novalidate
交給的 script.js
做統一驗錯<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<title>填寫追蹤狀況</title>
<link rel="stylesheet" href="style.css" />
<script src="script.js" defer></script>
</head>
<body>
<!-- 加 id 讓 script.js 能在登入後自動插入「提醒」「頭像」等 -->
<nav id="navbar">
<a href="index.html">首頁</a>
<a href="report.html">紀錄</a>
<a href="record.html">填寫</a>
<a href="signup.html">註冊</a>
<a href="login.html">登入</a>
</nav>
<h1>填寫追蹤狀況</h1>
<!-- 表單狀態訊息(供螢幕閱讀器讀出) -->
<p id="formMessage" class="muted" aria-live="polite"></p>
<form id="recordForm" novalidate>
<!-- 日期 -->
<label for="date">日期:</label>
<input
type="date"
id="date"
name="date"
required
autocomplete="off"
/>
<!-- 體重 -->
<label for="weight">體重(Kg):</label>
<input
type="number"
id="weight"
name="weight"
inputmode="decimal"
step="0.1"
min="20"
max="400"
placeholder="例如:65.5"
required
/>
<!-- 運動 -->
<fieldset class="form-group" style="border:0;padding:0;margin-top:14px;">
<legend style="font-weight:bold;">運動狀況:</legend>
<input type="radio" id="exercise_yes" name="exercise" value="yes" />
<label for="exercise_yes">有</label>
<input type="radio" id="exercise_no" name="exercise" value="no" checked />
<label for="exercise_no">沒有</label>
</fieldset>
<input
type="text"
id="exercise_detail"
name="exercise_detail"
placeholder="請填寫運動內容(例如:快走 30 分鐘)"
class="hidden"
disabled
/>
<!-- 藥物 -->
<fieldset class="form-group" style="border:0;padding:0;margin-top:14px;">
<legend style="font-weight:bold;">藥物使用:</legend>
<input type="radio" id="med_yes" name="medication" value="yes" />
<label for="med_yes">有</label>
<input type="radio" id="med_no" name="medication" value="no" checked />
<label for="med_no">沒有</label>
</fieldset>
<!-- 用藥時間必填(有用藥時),藥名選填 -->
<label for="medication_time" class="hidden">用藥時間:</label>
<input
type="time"
id="medication_time"
name="medication_time"
class="hidden"
disabled
required
/>
<label for="medication_name" class="hidden">藥物名稱(選填):</label>
<input
type="text"
id="medication_name"
name="medication_name"
placeholder="藥物名稱(選填)"
class="hidden"
disabled
/>
<!-- 血糖 -->
<label for="bf_glucose">餐前血糖(mg/dL):</label>
<input
type="number"
id="bf_glucose"
name="bf_glucose"
inputmode="numeric"
min="50"
max="600"
step="1"
required
/>
<label for="af_glucose">餐後血糖(mg/dL):</label>
<input
type="number"
id="af_glucose"
name="af_glucose"
inputmode="numeric"
min="50"
max="600"
step="1"
required
/>
<!-- 備註 -->
<label for="remark">備註:</label>
<textarea id="remark" name="remark" rows="3" placeholder="可填入飲食、心情、身體不適等"></textarea>
<!-- 動作按鈕 -->
<button type="submit">送出</button>
<button type="button" id="clearBtn" class="btn secondary" style="margin-left:8px;">
清除填寫資訊
</button>
</form>
</body>
</html>
<button type="button" class="btn secondary" id="btnTogglePwd">顯示</button>
// ---- 密碼顯示/隱藏通用
(function(){
const input = document.getElementById("password") || document.getElementById("newPassword");
const btn = document.getElementById("btnTogglePwd");
if (input && btn) {
btn.addEventListener("click", ()=>{
const on = input.type === "password";
input.type = on ? "text" : "password";
btn.textContent = on ? "隱藏" : "顯示";
});
}
})();
<p id="resetMessage" aria-live="polite"></p>
records
(可在 report 看到)/* ========== 站內橫幅提醒(頁面開著就有用) ========== */
(function inAppBanner(){
// 建橫幅
const bar = document.createElement('div');
bar.className = 'inapp-alert';
bar.innerHTML = `
<span id="iaText">到時間囉!</span>
<button id="iaSnooze" class="btn secondary">稍後 10 分</button>
<button id="iaDone" class="btn primary">已服用</button>
<button id="iaClose" class="btn secondary">忽略</button>
`;
document.body.appendChild(bar);
const iaText = bar.querySelector('#iaText');
const btnDone = bar.querySelector('#iaDone');
const btnClose= bar.querySelector('#iaClose');
const btnSnooze = bar.querySelector('#iaSnooze');
const LS_REMINDERS = "reminders"; // 提醒清單
const LS_TAKEN = "reminders_taken"; // { 'YYYY-MM-DD': { id: true } }
const LS_SNOOZE = "reminder_snoozed"; // { 'YYYY-MM-DD HH:MM': true }
// ---- 小工具 ----
const pad2 = n => n<10 ? "0"+n : ""+n;
const todayISO = () => new Date().toISOString().slice(0,10);
const nowHHMM = () => { const d=new Date(); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; };
const nowKeyMin= () => new Date().toISOString().slice(0,16); // YYYY-MM-DD HH:MM
const loadJSON = (k, def) => { try { return JSON.parse(localStorage.getItem(k) || JSON.stringify(def)); } catch { return def; } };
const saveJSON = (k, v) => localStorage.setItem(k, JSON.stringify(v));
const loadReminders = () => loadJSON(LS_REMINDERS, []);
const loadTaken = () => loadJSON(LS_TAKEN, {});
const saveTaken = (o) => saveJSON(LS_TAKEN, o);
const loadSnooze = () => loadJSON(LS_SNOOZE, {});
const saveSnooze = (o) => saveJSON(LS_SNOOZE, o);
let currentR = null;
function tick(){
const arr = loadReminders();
if (!arr.length) return;
// 打盹:這分鐘被延後就不彈
const snoozed = loadSnooze();
if (snoozed[nowKeyMin()]) return;
const hhmm = nowHHMM();
const w = new Date().getDay(); // 0-6
const iso = todayISO();
const taken = loadTaken()[iso] || {};
// 找第一個「到點、啟用、今天該吃、且今天尚未標註已服用」
const due = arr.find(r =>
r.enabled !== false &&
r.timeHHMM === hhmm &&
Array.isArray(r.days) && r.days.includes(w) &&
!taken[r.id]
);
if (due) {
currentR = due;
iaText.textContent = `吃藥提醒:${due.title}(${due.timeHHMM})`;
bar.classList.add('show');
// 標題閃爍
const orig = document.title;
let count=0; const t = setInterval(()=>{
document.title = (count++%2===0) ? '⏰ 吃藥提醒!' : orig;
}, 800);
// 關閉
btnClose.onclick = () => { bar.classList.remove('show'); clearInterval(t); document.title = orig; };
// 稍後 10 分鐘
btnSnooze.onclick = () => {
const d = new Date();
d.setMinutes(d.getMinutes() + 10);
const key = d.toISOString().slice(0,16); // 到分鐘
const all = loadSnooze();
all[key] = true;
saveSnooze(all);
bar.classList.remove('show'); clearInterval(t); document.title = orig;
};
// 已服用:標記今天 + 寫一筆到 records
btnDone.onclick = () => {
// 1) 標記今天已服用
const all = loadTaken();
all[iso] = all[iso] || {};
all[iso][currentR.id] = true;
saveTaken(all);
// 2) 寫入一筆用藥紀錄(讓 report 看得到)
try {
const now = new Date();
const rec = {
date: iso,
weight: null,
exercise: "no",
exercise_detail: "",
medication: "yes",
medication_time: now.toTimeString().slice(0,5), // HH:MM
medication_name: currentR ? currentR.title : "未指定",
bf_glucose: null,
af_glucose: null,
remark: "提醒勾選(站內)"
};
const records = loadJSON("records", []);
records.unshift(rec);
saveJSON("records", records);
} catch(e) { console.warn("寫入用藥紀錄失敗", e); }
bar.classList.remove('show'); clearInterval(t); document.title = orig;
};
}
}
// 每分鐘整點檢查
function start(){
tick();
const delay = 60000 - (Date.now() % 60000);
setTimeout(()=>{ tick(); setInterval(tick, 60000); }, delay);
}
start();
})();
records
匯出 CSV 按鈕<button id="btnExportCSV" class="btn secondary">匯出 CSV</button>
// ---- 匯出 records 為 CSV
(function(){
const btn = document.getElementById("btnExportCSV");
if (!btn) return;
btn.addEventListener("click", ()=>{
const rows = JSON.parse(localStorage.getItem("records") || "[]");
if (!rows.length) { alert("目前沒有紀錄可匯出。"); return; }
const header = ["日期","體重","運動","運動說明","用藥","用藥時間","藥名","餐前血糖","餐後血糖","備註"];
const csv = [header]
.concat(rows.map(r=>[
r.date, r.weight ?? "", r.exercise ?? "", r.exercise_detail ?? "",
r.medication ?? "", r.medication_time ?? "", r.medication_name ?? "",
r.bf_glucose ?? "", r.af_glucose ?? "", (r.remark ?? "").replace(/\n/g," ")
]))
.map(row => row.map(x => `"${String(x).replace(/"/g,'""')}"`).join(","))
.join("\r\n");
const blob = new Blob([csv], {type:"text/csv;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = "records.csv"; a.click();
URL.revokeObjectURL(url);
});
})();