Download src/view_admin.py from sidaier/fc: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/sidaier/fc/resolve/main/src/view_admin.py
- Command line
-
hf download hf://spaces/sidaier/fc/src/view_admin.py
-
curl -L -o view_admin.py https://huggingface.co/spaces/sidaier/fc/resolve/main/src/view_admin.py
10.4 kB
| """管理视图:录入新比赛、比赛管理、队员名单、新增队员、重置密码、清空数据。""" | |
| 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", | |
| ), | |
| ) | |