/* App 外殼 + 登入 — app.jsx */ const { useState: useAppState, useEffect: useAppEffect, useMemo: useAppMemo } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "深藍主控台", "accent": "#2f6fed", "fontScale": 100, "radius": "圓潤" }/*EDITMODE-END*/; const ROLE_LABELS = { teacher: '老師', assistant: '小老師', student: '學生' }; /* 角色 → 可用的導覽分頁 */ const NAV = { teacher: [ { key: 'class', label: '全班成績', icon: 'users' }, { key: 'input', label: '輸入成績', icon: 'pencil' }, { key: 'import', label: '匯入成績', icon: 'upload' } ], assistant: [ { key: 'mine', label: '我的成績', icon: 'chart' }, { key: 'input', label: '輸入成績', icon: 'pencil' }, { key: 'analysis', label: '學習分析', icon: 'spark' } ], student: [ { key: 'mine', label: '我的成績', icon: 'chart' }, { key: 'analysis', label: '學習分析', icon: 'spark' } ] }; /* ── 全域載入畫面 ── */ function LoadingScreen({ message }) { return (
{message || '載入中…'}
); } /* ── 登入畫面 ── */ function LoginScreen({ onLogin }) { const [email, setEmail] = useAppState(''); const [password, setPassword] = useAppState(''); const [loading, setLoading] = useAppState(false); const [error, setError] = useAppState(null); const [forgot, setForgot] = useAppState(false); const [forgotEmail, setForgotEmail] = useAppState(''); const [resetSent, setResetSent] = useAppState(false); const [resetLoading, setResetLoading] = useAppState(false); async function handleLogin(e) { if (e) e.preventDefault(); if (!email || !password) return; setLoading(true); setError(null); try { await onLogin(email, password); } catch (err) { setError('帳號或密碼錯誤,請再試一次。'); setLoading(false); } } async function handleReset() { if (!forgotEmail) return; setResetLoading(true); try { await API.resetPassword(forgotEmail); setResetSent(true); } catch { setResetSent(true); /* 不揭露帳號是否存在 */ } finally { setResetLoading(false); } } return (
數學科成績查詢系統
七年三班・平時成績
請輸入帳號密碼登入
{error &&
{error}
}
系統由老師設定帳號
{/* 忘記密碼 Modal */} {forgot && (
setForgot(false)}>
e.stopPropagation()}>

重設密碼

{resetSent ? (

若此 Email 已註冊,重設連結將寄送至信箱,請查收。

) : ( <>

輸入你的帳號 Email,系統將寄送密碼重設連結。

setForgotEmail(e.target.value)} /> )}
)}
); } /* ── 主 App ── */ function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [appReady, setAppReady] = useAppState(false); /* 初始 session 檢查完畢 */ const [user, setUser] = useAppState(null); const [tab, setTab] = useAppState('mine'); const [grades, setGrades] = useAppState([]); const [types, setTypes] = useAppState(window.APPDATA.assessmentTypes.slice()); const [loadErr, setLoadErr] = useAppState(null); /* 套用主題 */ useAppEffect(() => { const root = document.documentElement; root.dataset.theme = t.theme === '淨白學院' ? 'light' : 'dark'; root.dataset.radius = t.radius === '方正' ? 'sharp' : 'round'; root.style.setProperty('--accent', t.accent); root.style.setProperty('--font-scale', t.fontScale / 100); }, [t.theme, t.accent, t.fontScale, t.radius]); /* 初次載入:檢查是否有現有 session */ useAppEffect(() => { API.getSession().then(function (session) { if (session) { return loadUserData(session.user); } setAppReady(true); }).catch(function (err) { console.error('Session 檢查失敗', err); setAppReady(true); }); /* 監聽 auth 狀態變化(登入 / 登出) */ var sub = API.onAuthChange(function (event, session) { if (event === 'SIGNED_OUT') { setUser(null); setGrades([]); window.APPDATA.students = []; setAppReady(true); } }); return function () { if (sub && sub.data) sub.data.subscription.unsubscribe(); }; }, []); async function loadUserData(authUser) { setLoadErr(null); try { const profile = await API.getProfile(authUser.id); const [students, fetchedGrades] = await Promise.all([ API.getStudents(), API.getGrades(profile.role, profile.student_id) ]); /* 更新全域 APPDATA.students 供 screens 使用 */ window.APPDATA.students = students; setUser({ id: authUser.id, email: authUser.email, role: profile.role, roleLabel: profile.role_label || ROLE_LABELS[profile.role] || profile.role, name: profile.name, studentId: profile.student_id }); setGrades(fetchedGrades); setTab(NAV[profile.role][0].key); } catch (err) { console.error('載入資料失敗', err); setLoadErr('載入資料失敗:' + (err.message || '請稍後再試')); await API.signOut(); } finally { setAppReady(true); } } async function login(email, password) { setAppReady(false); const data = await API.signIn(email, password); /* 若失敗會 throw */ await loadUserData(data.user); } async function logout() { await API.signOut(); setUser(null); setGrades([]); window.APPDATA.students = []; setAppReady(true); } /* ── 成績操作 ── */ async function addGrade(g) { const dbGrade = { id: 'g' + Date.now() + '_' + Math.floor(Math.random() * 9999), student_id: g.studentId, type: g.type, title: g.title, date: g.date, score: g.score, created_by: user.id }; const saved = await API.addGrade(dbGrade); setGrades(prev => [...prev, Object.assign({}, saved, { addedThisSession: true })]); } async function deleteGrade(id) { await API.deleteGrade(id); setGrades(prev => prev.filter(g => g.id !== id)); } function addType(t2) { setTypes(prev => prev.includes(t2) ? prev : [...prev, t2]); } async function commitImport(rows) { const students = window.APPDATA.students; const dbRows = rows.map(function (r, i) { const st = students.find(s => s.seat === r.seat); if (!st) return null; return { id: 'imp' + Date.now() + '_' + i, student_id: st.id, type: r.type, title: r.title, date: r.date, score: r.score, created_by: user.id }; }).filter(Boolean); if (!dbRows.length) return; const saved = await API.addGrades(dbRows); setGrades(prev => [ ...prev, ...saved.map(g => Object.assign({}, g, { addedThisSession: true })) ]); } /* ── Tweaks 面板 ── */ const panel = ( setTweak('theme', v)} /> setTweak('accent', v)} /> setTweak('radius', v)} /> setTweak('fontScale', v)} /> ); /* 初始 session 還在確認中 */ if (!appReady) return <>{panel}; /* 未登入 */ if (!user) { return ( <> {loadErr &&
{loadErr}
} {panel} ); } const nav = NAV[user.role]; const sid = user.studentId; return (
{/* 頂部列 */}
數學科成績查詢系統
{user.name} {user.roleLabel}
{/* 行動版導覽 */}
{tab === 'mine' && } {tab === 'analysis' && } {tab === 'input' && } {tab === 'class' && } {tab === 'import' && }
{panel}
); } ReactDOM.createRoot(document.getElementById('root')).render();