fc / src /view_admin.py
Chuiguohuang's picture
feat(mvp): 新增铁血防守 MVP(领队结算/红色榜单/🛡️芯片),原 MVP 更名最佳球员
23938a8 verified
Raw History Blame Contribute Delete
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",
),
)