""" student_login.py ---------------- 學生登入元件。 用 clientside callback 直接讀 localStorage,避免 Python callback 時序問題。 """ from __future__ import annotations import dash from dash import html, dcc, callback, Input, Output, State, no_update, clientside_callback import dash_mantine_components as dmc from dash_iconify import DashIconify def create_student_store() -> dcc.Store: return dcc.Store(id="student-id-store", storage_type="local", data="") def create_login_modal() -> html.Div: return html.Div([ dmc.Modal( id="student-login-modal", opened=False, centered=True, closeOnClickOutside=False, closeOnEscape=False, withCloseButton=False, title=dmc.Group([ DashIconify(icon="tabler:school", height=24), dmc.Text("歡迎使用數學學習平台", fw=700, size="lg"), ], gap="xs"), children=[ dmc.Text( "請輸入您的學號,系統將記錄您的學習進度。", size="sm", c="dimmed", mb="md", ), dmc.TextInput( id="student-id-input", label="學號", placeholder="例如:B11234567", leftSection=DashIconify(icon="tabler:id-badge", height=16), required=True, mb="md", ), dmc.Alert( id="student-login-error", children="", color="red", variant="light", style={"display": "none"}, mb="sm", ), dmc.Button( "開始學習", id="student-login-btn", fullWidth=True, leftSection=DashIconify(icon="tabler:arrow-right", height=16), ), ], size="sm", ), ]) def create_student_badge() -> html.Div: return html.Div( id="student-badge", style={"display": "none"}, children=dmc.Group([ dmc.Badge( id="student-badge-text", leftSection=DashIconify(icon="tabler:user", height=12), variant="light", color="teal", size="sm", ), dmc.ActionIcon( DashIconify(icon="tabler:logout", height=14), id="student-logout-btn", size="sm", variant="subtle", color="gray", ), ], gap=4), ) @callback( Output("student-id-store", "data", allow_duplicate=True), Input("student-logout-btn", "n_clicks"), prevent_initial_call=True, ) def logout_student(n_clicks): if n_clicks: return "" return no_update # ─── Clientside:頁面載入時直接從 localStorage 判斷是否開啟 modal ─────────── # 完全在瀏覽器端執行,不需要等 Python callback,沒有時序問題 clientside_callback( """ function(storeData) { try { if (storeData && storeData !== "") { return false; } return true; } catch(e) { return true; } } """, Output("student-login-modal", "opened"), Input("student-id-store", "data"), prevent_initial_call=False, ) # ─── Python callback:按下「開始學習」後儲存學號 ────────────────────────── @callback( Output("student-id-store", "data"), Output("student-login-error", "children"), Output("student-login-error", "style"), Input("student-login-btn", "n_clicks"), State("student-id-input", "value"), prevent_initial_call=True, ) def save_student_id(n_clicks, input_value): sid = (input_value or "").strip() if not sid: return no_update, "請輸入學號", {"display": "block"} # 寫入 Firebase try: from firebase_progress import upsert_student upsert_student(sid) except Exception as e: print(f"[Firebase] upsert_student 失敗:{e}") return sid, "", {"display": "none"} # ─── 學生 badge 更新 ───────────────────────────────────────────────────── @callback( Output("student-badge", "style"), Output("student-badge-text", "children"), Input("student-id-store", "data"), ) def update_badge(student_id): if student_id: return {"display": "inline-flex", "alignItems": "center"}, f"學號:{student_id}" return {"display": "none"}, ""