"""管理视图:录入新比赛、比赛管理、队员名单、新增队员、重置密码、清空数据。""" from __future__ import annotations from datetime import date from typing import Any from shiny import ui from . import config, theme def _admin_panel(user_choices: dict[str, str]) -> ui.Tag: add_match = theme.card( "录入新比赛比分", ui.layout_columns( ui.input_date("m_date", "日期", value=date.today()), ui.input_select("m_ha", "主/客", {"home": "主场", "away": "客场"}), ui.input_text("m_opp", "对手", placeholder="对手球队名"), col_widths=[4, 3, 5], fill=False, ), ui.layout_columns( ui.input_numeric("m_our", "我方进球", 0, min=0, max=99), ui.input_numeric("m_oppscore", "对方进球", 0, min=0, max=99), ui.input_text("m_note", "备注", placeholder="可选,如 联赛第X轮"), col_widths=[3, 3, 6], fill=False, ), ui.input_action_button("btn_add_match", "➕ 添加比赛", class_="btn-primary"), icon="⚽", ) manage_match = theme.card( "比赛管理:改比分 / 删除", ui.div("修改后点下方保存;勾选「删除」并保存即删除该场。", class_="sde-sub"), ui.output_ui("admin_matches"), ui.input_action_button("btn_save_matches", "💾 保存比赛改动", class_="btn-primary"), icon="🗂️", ) mvp_settle = theme.card( "结算本场 MVP", ui.div( "由领队为每场选定「最佳球员 MVP」(可双:两人各计一次)与「铁血防守 MVP」" "(队员投票数仅作参考)。任一奖项有人即结算锁票;全部改回「— 未结算 —」" "并保存可重新开放投票。", class_="sde-sub", ), ui.output_ui("admin_mvp_settle"), ui.input_action_button("btn_save_mvp_lock", "💾 保存结算", class_="btn-primary"), icon="🏅", ) roster = theme.card("队员名单", ui.output_ui("admin_players"), icon="👥") add_player = theme.card( "新增队员", ui.layout_columns( ui.input_text("p_name", "昵称", placeholder="显示名"), ui.input_text("p_user", "登录名", placeholder="英文/拼音,唯一"), ui.input_text("p_jersey", "球衣号", placeholder="可空"), col_widths=[4, 4, 4], fill=False, ), ui.layout_columns( ui.input_text("p_pw", "初始密码", value=config.DEFAULT_PASSWORD), ui.input_switch("p_admin", "设为管理员", value=False), ui.input_switch("p_gk", "门将", value=False), col_widths=[6, 3, 3], fill=False, ), ui.input_action_button("btn_add_player", "➕ 添加队员", class_="btn-primary"), icon="➕", ) edit_player = theme.card( "编辑队员资料与数据", ui.div( "选择队员后可改其 昵称(显示名)/ 球衣号 / 门将标记,并逐场修改 出勤/进球/助攻/扑救。" "例如把 Sunny 改成 晴朗。改完点保存。(登录名不变,不影响登录)", class_="sde-sub", ), ui.input_select("ae_user", "选择队员", user_choices), ui.output_ui("admin_edit_player"), ui.input_action_button("btn_ae_save", "💾 保存该队员资料与数据", class_="btn-primary"), icon="✏️", ) reset_pw = theme.card( "重置密码", ui.layout_columns( ui.input_select("rp_user", "选择队员", user_choices), ui.input_text("rp_pw", "新密码", value=config.DEFAULT_PASSWORD), col_widths=[6, 6], fill=False, ), ui.input_action_button("btn_reset_pw", "🔑 重置密码", class_="btn-primary"), icon="🔑", ) danger = theme.card( "清空示例数据(保留队员)", ui.div( "一键删除所有比赛和进球/助攻记录,队员账号保留。用于清空示例、正式开赛。", class_="sde-sub", ), ui.input_switch("confirm_clear", "我确认要清空所有比赛与数据", value=False), ui.input_action_button("btn_clear_demo", "🧹 清空比赛与数据", class_="btn-danger"), icon="🧹", ) return ui.div( add_match, manage_match, mvp_settle, roster, edit_player, add_player, reset_pw, danger, ) def admin_match_editor(matches: list[dict[str, Any]]) -> ui.Tag: if not matches: return ui.div("还没有比赛,请在上方添加。", class_="sde-empty") head = ui.tags.tr( *[ui.tags.th(h) for h in ["日期", "主客", "对手", "我方", "对方", "删除"]] ) body = [] for m in matches: mid = m["id"] body.append( ui.tags.tr( ui.tags.td(m["date"]), ui.tags.td( ui.input_select( f"mha_{mid}", "", {"home": "主", "away": "客"}, selected=m["home_away"], width="78px", ) ), ui.tags.td(ui.input_text(f"mopp_{mid}", None, value=m["opponent"], width="150px")), ui.tags.td(ui.input_numeric(f"mo_{mid}", None, m["our"], min=0, max=99, width="72px")), ui.tags.td(ui.input_numeric(f"mp_{mid}", None, m["opp"], min=0, max=99, width="72px")), ui.tags.td(ui.input_checkbox(f"mdel_{mid}", "", value=False)), ) ) return ui.div( ui.tags.table( ui.tags.thead(head), ui.tags.tbody(*body), class_="sde-table sde-form-table" ), class_="sde-table-wrap", ) def admin_mvp_settle_view( matches: list[dict[str, Any]], vote_summary: dict[int, dict[str, Any]], player_choices: dict[str, str], ) -> ui.Tag: """每场结算表:投票参考 + 最佳球员双下拉 + 铁血防守下拉。 id:mvpsel_{mid} / mvpsel2_{mid} / dmvpsel_{mid};任一有人=结算锁定,全 none=重开。 """ if not matches: return ui.div("还没有比赛,请先在上方添加。", class_="sde-empty") choices = {"none": "— 未结算 —", **player_choices} head = ui.tags.tr( *[ui.tags.th(h) for h in ["日期", "对手", "投票参考", "最佳球员 ①", "最佳球员 ②(可选)", "🛡️ 铁血防守"]] ) body = [] for m in matches: mid = m["id"] win = vote_summary.get(mid) leader = f'{"、".join(win["names"])}({win["votes"]} 票)' if win else "暂无投票" sels = [] for key, field in ( (f"mvpsel_{mid}", "mvp_user_id"), (f"mvpsel2_{mid}", "mvp2_user_id"), (f"dmvpsel_{mid}", "dmvp_user_id"), ): sel = str(m.get(field) or "none") if sel not in choices: sel = "none" sels.append( ui.tags.td(ui.input_select(key, "", choices, selected=sel, width="150px")) ) body.append( ui.tags.tr( ui.tags.td(m["date"]), ui.tags.td(m["opponent"]), ui.tags.td(leader), *sels, ) ) return ui.div( ui.tags.table( ui.tags.thead(head), ui.tags.tbody(*body), class_="sde-table sde-form-table", ), class_="sde-table-wrap", ) def admin_players_view(users: list[dict[str, Any]]) -> ui.Tag: head = ui.tags.tr(*[ui.tags.th(h) for h in ["#", "昵称", "登录名", "角色", "门将"]]) body = [] for u in users: body.append( ui.tags.tr( ui.tags.td(f'#{u["jersey"]}' if u["jersey"] else "—", class_="sde-rank"), ui.tags.td(u["display_name"]), ui.tags.td(ui.tags.code(u["username"])), ui.tags.td("领队" if u["is_admin"] else "球员"), ui.tags.td("🧤" if u.get("is_goalkeeper") else "—"), ) ) return ui.div( ui.tags.table(ui.tags.thead(head), ui.tags.tbody(*body), class_="sde-table"), class_="sde-table-wrap", ) def admin_edit_player_view( matchlog: list[dict[str, Any]], is_gk: bool, display_name: str = "", jersey: Any = None, ) -> ui.Tag: """管理员编辑某队员:昵称 / 球衣号 / 门将开关 + 逐场 出勤/进球/助攻/扑救。 输入 id 用 a 前缀(ae_name/ae_jersey/aatt_/ag_/aa_/asv_),与队员本人录入的 id 不冲突。 """ profile = ui.layout_columns( ui.input_text("ae_name", "昵称(显示名)", value=display_name or ""), ui.input_text("ae_jersey", "球衣号", value=("" if jersey in (None, "") else str(jersey))), col_widths=[6, 6], fill=False, ) gk_switch = ui.input_switch( "ae_gk", "该队员是门将(计入扑救榜,可录扑救)", value=bool(is_gk) ) if not matchlog: return ui.div( profile, gk_switch, ui.div("该队员暂无可编辑比赛,请先录入比赛。", class_="sde-empty"), ) head = ui.tags.tr( *[ui.tags.th(h) for h in ["日期", "对手", "出勤", "进球", "助攻", "扑救"]] ) body = [] for m in matchlog: mid = m["id"] body.append( ui.tags.tr( ui.tags.td(m["date"]), ui.tags.td(m["opponent"]), ui.tags.td(ui.input_checkbox(f"aatt_{mid}", "", value=bool(m["attended"]))), ui.tags.td(ui.input_numeric(f"ag_{mid}", None, int(m["goals"]), min=0, max=99, width="70px")), ui.tags.td(ui.input_numeric(f"aa_{mid}", None, int(m["assists"]), min=0, max=99, width="70px")), ui.tags.td(ui.input_numeric(f"asv_{mid}", None, int(m["saves"]), min=0, max=99, width="70px")), ) ) return ui.div( profile, gk_switch, ui.div( ui.tags.table( ui.tags.thead(head), ui.tags.tbody(*body), class_="sde-table sde-form-table", ), class_="sde-table-wrap", ), )