/* 各角色畫面 — 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}
)}
{/* 明細 */}
{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"
? "小老師:可為班上任何同學登記成績。"
: "選擇學生與評量項目,登記平時成績。"}
{/* 表單 */}
登記新成績
{/* 本次新增 */}
{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;