"""主页视图:球队战绩卡、每场战报记分牌、冲击力榜单(Hero 聚光台 + 奖牌行 + 能量条)。""" from __future__ import annotations from typing import Any, Callable, Optional from shiny import ui from . import config, theme from .view_common import _medal from .view_me import fifa_card_tag def home_view( summary: dict[str, Any], matches: list[dict[str, Any]], agg: list[dict[str, Any]], me_id: Optional[int], contributions: dict[int, dict[str, list[str]]], mvp: list[dict[str, Any]], mvp_summary: Optional[dict[int, dict[str, Any]]] = None, dmvp: Optional[list[dict[str, Any]]] = None, fifa_cards: Optional[list[dict[str, Any]]] = None, ) -> ui.Tag: stats = ui.div( theme.stat(summary["played"], "场次", accent="cream"), theme.stat( f'{summary["wins"]}-{summary["draws"]}-{summary["losses"]}', "胜-平-负" ), theme.stat( f'{summary["gf"]}-{summary["ga"]}', "进球-失球", f'净胜 {summary["gd"]:+d}', "blue" ), theme.stat(summary["points"], "积分"), theme.stat(f'{summary["win_rate"]}%', "胜率", accent="green"), class_="sde-stats", ) scorers = sorted(agg, key=lambda x: (-x["goals"], -x["assists"], -x["attended"])) assisters = sorted(agg, key=lambda x: (-x["assists"], -x["goals"])) attend = sorted(agg, key=lambda x: (-x["attended"], -x["goals"])) keepers = sorted( [r for r in agg if r.get("is_goalkeeper")], key=lambda x: (-x["saves"], -x["attended"]), ) board_cards = [ theme.card("射手榜", _impact_board( scorers, "射手王", lambda r: r["goals"], "球", "", lambda r: f'助攻 {r["assists"]} · 出场 {r["attended"]}/{r["total"]}', me_id, ), icon="⚽"), theme.card("助攻榜", _impact_board( assisters, "助攻王", lambda r: r["assists"], "助攻", "c-blue", lambda r: f'进球 {r["goals"]} · 出场 {r["attended"]}/{r["total"]}', me_id, ), icon="🎯"), theme.card("出勤榜", _impact_board( attend, "全勤铁人", lambda r: r["attended"], "场", "c-green", lambda r: f'共 {r["total"]} 场 · 进球+助攻 {r["ga"]}', me_id, ), icon="📋"), theme.card("最佳球员 MVP", _impact_board( mvp, "最佳球员", lambda r: r["mvps"], "次", "", None, me_id, ), icon="🏅", sub="每场由领队结算 · 计获得次数"), theme.card("铁血防守 MVP", _impact_board( dmvp or [], "铁血防守", lambda r: r["dmvps"], "次", "c-red", None, me_id, ), icon="🛡️", sub="每场由领队结算 · 计获得次数"), ] widths = [6, 6, 6, 6, 6] if keepers: # 有门将才显示扑救榜 board_cards.append( theme.card("扑救榜", _impact_board( keepers, "扑救之神", lambda r: r["saves"], "扑救", "c-blue", lambda r: f'出场 {r["attended"]}/{r["total"]}', me_id, ), icon="🧤", sub="仅门将") ) widths = [6, 6, 6, 6, 6, 6] boards = ui.layout_columns(*board_cards, col_widths=widths, fill=False) card_wall = None if fifa_cards: top8 = fifa_cards[:8] # all_fifa_cards 已按 OVR 降序;每场数据更新后席位自动洗牌 card_wall = theme.card( "本队 TOP 8 球星卡", ui.div( *[fifa_card_tag(c, is_me=(c["user_id"] == me_id), flip_prefix="flipw") for c in top8], class_="sde-fifa-wall", ), icon="🃏", sub="OVR 前 8 上榜 · 每场数据更新后席位洗牌 · 紫≥88 金≥74 银≥66 · ★=我 · " "点卡片翻面看战绩 · 自己的卡在「我的数据」页", ) return ui.div( theme.card("球队战绩", stats, icon="🏆"), theme.card( "每场比分", _matches_list(matches, contributions, mvp_summary or {}), icon="📅", sub="比分由领队录入;进球/助攻为队员自填汇总", ), boards, card_wall, ) # --------------------------------------------------------------- 战报记分牌 _RECENT_N = 3 # 默认展示最近场次数,其余折叠 def _matches_list( matches: list[dict[str, Any]], contributions: dict[int, dict[str, list[str]]], mvp_summary: dict[int, dict[str, Any]], ) -> ui.Tag: if not matches: return ui.div("还没有比赛记录。", class_="sde-empty") def card(m): return _scorecard(m, contributions.get(m["id"], {}), mvp_summary.get(m["id"])) recent, older = matches[:_RECENT_N], matches[_RECENT_N:] children: list[Any] = [card(m) for m in recent] if older: #
原生折叠:摘要行即按钮,展开显示其余场次(零 JS) children.append( ui.tags.details( ui.tags.summary( ui.span(f"展开更早的 {len(older)} 场", class_="txt-open"), ui.span("收起早期场次", class_="txt-close"), ui.span("▾", class_="arr"), class_="sde-more-btn", ), ui.div(*[card(m) for m in older], class_="sde-mc-list"), class_="sde-mc-more", ) ) return ui.div(*children, class_="sde-mc-list") def _scorecard( m: dict[str, Any], c: dict[str, list[str]], mvp: Optional[dict[str, Any]] ) -> ui.Tag: """一场比赛的战报卡:结果色边+光晕、居中大比分、结果大徽章、分行明细。 明细区每类一行:行首固定标签(最佳球员/铁血防守/进球/助攻)+ 该类芯片。 """ res = m["result"] def detail_row(icon: str, label: str, names: list[str], chip_cls: str): return ui.div( ui.span(ui.span(icon, class_="ic"), label, class_=f"sde-dl {chip_cls}"), ui.div( *[ui.span(n, class_=f"sde-chip {chip_cls}") for n in names], class_="sde-dr", ), class_="sde-detail-row", ) rows: list[Any] = [] if m.get("mvp_locked") and mvp: if mvp.get("names"): rows.append(detail_row("🏅", "最佳球员", mvp["names"], "mvp")) if mvp.get("dmvp_name"): rows.append(detail_row("🛡️", "铁血防守", [mvp["dmvp_name"]], "dmvp")) if c.get("goals"): rows.append(detail_row("⚽", "进球", c["goals"], "goal")) if c.get("assists"): rows.append(detail_row("🅰️", "助攻", c["assists"], "assist")) return ui.div( ui.span(res, class_=f"sde-sc-badge {res}"), ui.div( ui.span(m["date"], class_="d"), theme.ha_pill(m["home_away"]), (ui.span(m["note"], class_="note") if m["note"] else None), class_="sde-sc-meta", ), ui.div( ui.div(ui.div(config.TEAM_NAME, class_="tn"), class_="sde-sc-side us"), ui.div( ui.span(str(m["our"]), class_="n us"), ui.span(":", class_="sep"), ui.span(str(m["opp"]), class_="n them"), class_="sde-sc-score", ), ui.div(ui.div(m["opponent"], class_="tn"), class_="sde-sc-side them"), class_="sde-sc-main", ), (ui.div(*rows, class_="sde-sc-detail") if rows else None), class_=f"sde-scorecard res-{res}", ) # --------------------------------------------------------------- 冲击力榜单 def _impact_board( rows: list[dict[str, Any]], champ: str, val: Callable[[dict], int], unit: str, color: str, sub: Optional[Callable[[dict], str]], me_id: Optional[int], ) -> ui.Tag: """榜首 Hero 聚光台 + 2/3 名奖牌行 + 能量条;空数据显示占位。 只列「该项有记录(值>0)」的球员:榜首 Hero + 2/3 名常驻,第 4 名起折叠, 展开即显示全部有记录者(不设上限),无记录的不上榜。 """ rows = [r for r in rows if val(r) > 0] or rows[:0] if not rows: return ui.div(ui.div("暂无数据 · 虚位以待", class_="sde-empty"), class_="sde-board") top, rest = rows[0], rows[1:] maxv = max(1, val(top)) def jersey(r): return ui.span(f'#{r["jersey"]}', class_="sde-jersey") if r.get("jersey") else None def me_tag(r): return ui.span("我", class_="sde-me-tag") if r["user_id"] == me_id else None hero = ui.div( ui.span("👑", class_="crown"), ui.div(champ, class_="title"), ui.div( jersey(top), ui.span(top["name"], class_="nm"), me_tag(top), ui.span( ui.span(str(val(top)), class_="n"), ui.span(unit, class_="u"), class_="big", ), class_="who", ), (ui.div(sub(top), class_="sub2") if sub else None), class_=f"sde-hero {color}", ) def brow(i, r): cls = "sde-brow" + (" r2" if i == 2 else " r3" if i == 3 else "") if color: cls += f" {color}" if r["user_id"] == me_id: cls += " is-me" w = max(5, round(val(r) / maxv * 100)) return ui.div( ui.span(_medal(i), class_="rk"), jersey(r), ui.span(r["name"], class_="nm"), me_tag(r), ui.span(str(val(r)), ui.span(unit, class_="u"), class_="val"), ui.span(class_="bar", style=f"--w:{w}%"), class_=cls, ) # 榜首 Hero + 2/3 名奖牌行常驻;第 4 名起折叠(
,零 JS) shown = [brow(i, r) for i, r in enumerate(rest[:2], 2)] hidden = [brow(i, r) for i, r in enumerate(rest[2:], 4)] tail: list[Any] = [] if hidden: tail.append( ui.tags.details( ui.tags.summary( ui.span(f"展开完整榜单(还有 {len(hidden)} 人)", class_="txt-open"), ui.span("收起榜单", class_="txt-close"), ui.span("▾", class_="arr"), class_="sde-more-btn", ), ui.div(*hidden, class_="sde-board-more-list"), class_="sde-board-more", ) ) return ui.div(hero, *shown, *tail, class_="sde-board")