fc / src /view_home.py
Chuiguohuang's picture
fix(ui): 榜单展开显示全部有记录者(去掉第8名截断)
4d1c4f4 verified
Raw History Blame Contribute Delete
10.4 kB
"""主页视图:球队战绩卡、每场战报记分牌、冲击力榜单(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:
# <details> 原生折叠:摘要行即按钮,展开显示其余场次(零 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 名起折叠(<details>,零 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")