/* 各角色畫面 — screens.jsx */ const { useState, useMemo } = React; const { Icon, Card, StatCard, TypeBadge, ScoreChip, RiskLight, Button, SectionTitle, FilterChip, Avatar, DistBar, TrendLine } = window; const D = window.APPDATA; const fmtDate = (s) => s.slice(5).replace("-", "/"); const TODAY = "2026-05-31"; /* 把成績依日期排序 */ function byDate(a, b) { return a.date < b.date ? -1 : a.date > b.date ? 1 : 0; } /* ============================================================ 我的成績(學生 / 小老師) ============================================================ */ function MyGradesScreen({ studentId, grades }) { const [typeFilter, setTypeFilter] = useState("全部"); const student = D.students.find(s => s.id === studentId); const myGrades = useMemo( () => grades.filter(g => g.studentId === studentId).slice().sort(byDate), [grades, studentId] ); const stats = useMemo(() => { const sc = myGrades.map(g => g.score); return { count: sc.length, average: sc.length ? +(sc.reduce((a, b) => a + b, 0) / sc.length).toFixed(1) : 0, highest: sc.length ? Math.max(...sc) : 0, passRate: sc.length ? Math.round(sc.filter(s => s >= 60).length / sc.length * 100) : 0 }; }, [myGrades]); const risk = useMemo(() => D.riskFor(studentId, grades), [grades, studentId]); const types = ["全部", ...D.assessmentTypes]; const shown = typeFilter === "全部" ? myGrades : myGrades.filter(g => g.type === typeFilter); return (
嗨,{student.name} 👋
這是你目前的數學科平時成績,只有你看得到。
= 80 ? "good" : stats.passRate >= 60 ? "warn" : "fail"} />
{/* AI 風險卡 */} {risk && (
AI 期末預測 {risk.headline}
{risk.predicted}
預估期末平均

{risk.advice}

)} {/* 明細 */} {types.map(t => ( setTypeFilter(t)}>{t} ))}
}>成績明細
日期
類型
評量項目
得分
{shown.slice().reverse().map(g => (
{fmtDate(g.date)}
{g.title}
))} {!shown.length &&
此類型尚無成績。
}
); } /* ============================================================ 輸入成績(老師 / 小老師) ============================================================ */ function InputGradesScreen({ grades, addGrade, deleteGrade, types, addType, currentUser }) { const [form, setForm] = useState({ studentId: D.students[0].id, type: D.assessmentTypes[0], title: "", score: "", date: TODAY }); const [adding, setAdding] = useState(false); const [newType, setNewType] = useState(""); const [toast, setToast] = useState(null); const sessionRecords = useMemo( () => grades.filter(g => g.addedThisSession).slice().reverse(), [grades] ); function set(k, v) { setForm(f => ({ ...f, [k]: v })); } function submit() { const sc = parseInt(form.score, 10); if (!form.title.trim()) return flash("請輸入評量項目名稱", "warn"); if (isNaN(sc) || sc < 0 || sc > 100) return flash("得分需介於 0–100", "warn"); const st = D.students.find(s => s.id === form.studentId); addGrade({ id: "n" + Date.now(), studentId: form.studentId, type: form.type, title: form.title.trim(), score: sc, date: form.date, addedThisSession: true, by: currentUser.name }); flash(st.name + "・" + form.title.trim() + " " + sc + " 分 已新增", "good"); setForm(f => ({ ...f, title: "", score: "" })); } function flash(msg, tone) { setToast({ msg, tone }); setTimeout(() => setToast(null), 2400); } function confirmType() { const t = newType.trim(); if (t && !types.includes(t)) { addType(t); set("type", t); } setNewType(""); setAdding(false); } return (
輸入成績
{currentUser.role === "assistant" ? "小老師:可為班上任何同學登記成績。" : "選擇學生與評量項目,登記平時成績。"}
{/* 表單 */} 登記新成績
評量類型
{types.map(t => ( set("type", t)}>{t} ))} {adding ? ( setNewType(e.target.value)} onKeyDown={e => e.key === "Enter" && confirmType()} /> ) : ( )}
{/* 本次新增 */} {sessionRecords.length} 筆 }>本次登入新增的紀錄 {sessionRecords.length ? (
學生
類型
得分
{sessionRecords.map(g => { const st = D.students.find(s => s.id === g.studentId); return (
{st ? st.name : "—"} {g.title}
); })}
) : (

本次登入還沒有新增任何成績。

新增的紀錄會列在這裡,可隨時刪除。
)}
{toast &&
{toast.msg}
}
); } /* ============================================================ 全班成績(老師) ============================================================ */ function ClassGradesScreen({ grades, types }) { const [typeFilter, setTypeFilter] = useState("全部"); const [studentFilter, setStudentFilter] = useState("all"); const filtered = useMemo(() => { return grades.filter(g => (typeFilter === "全部" || g.type === typeFilter) && (studentFilter === "all" || g.studentId === studentFilter) ); }, [grades, typeFilter, studentFilter]); const cs = useMemo(() => { const sc = filtered.map(g => g.score); return { average: sc.length ? +(sc.reduce((a, b) => a + b, 0) / sc.length).toFixed(1) : 0, passRate: sc.length ? Math.round(sc.filter(s => s >= 60).length / sc.length * 100) : 0, count: sc.length, students: D.students.length }; }, [filtered]); const buckets = useMemo(() => { const sc = filtered.map(g => g.score); return [ { key: "fail", label: "不及格", count: sc.filter(s => s < 60).length }, { key: "warn", label: "待加強", count: sc.filter(s => s >= 60 && s < 80).length }, { key: "good", label: "良好", count: sc.filter(s => s >= 80).length } ]; }, [filtered]); // 每位學生平均(依目前類型篩選) const roster = useMemo(() => { return D.students.map(s => { const gs = grades.filter(g => g.studentId === s.id && (typeFilter === "全部" || g.type === typeFilter)); const sc = gs.map(g => g.score); const avg = sc.length ? +(sc.reduce((a, b) => a + b, 0) / sc.length).toFixed(1) : 0; return { ...s, avg, count: sc.length }; }); }, [grades, typeFilter]); const selStudent = studentFilter === "all" ? null : D.students.find(s => s.id === studentFilter); const selGrades = selStudent ? grades.filter(g => g.studentId === selStudent.id && (typeFilter === "全部" || g.type === typeFilter)).slice().sort(byDate) : []; const typeOpts = ["全部", ...types]; return (
全班成績
七年三班・數學科|共 {D.students.length} 位學生
{typeOpts.map(t => ( setTypeFilter(t)}>{t} ))}
= 80 ? "good" : cs.passRate >= 60 ? "warn" : "fail"} />
{selStudent ? ( /* 單一學生明細 */ setStudentFilter("all")}>← 回全班 }>{selStudent.name}・成績明細
日期
類型
評量項目
得分
{selGrades.slice().reverse().map(g => (
{fmtDate(g.date)}
{g.title}
))}
) : (
學生平均一覽
{roster.map(s => ( ))}
成績分佈
良好 ≥80 待加強 60–79 不及格 <60
)}
); } window.MyGradesScreen = MyGradesScreen; window.InputGradesScreen = InputGradesScreen; window.ClassGradesScreen = ClassGradesScreen; window.fmtDate = fmtDate; window.TODAY = TODAY;