feat(mvp): 新增铁血防守 MVP(领队结算/红色榜单/🛡️芯片),原 MVP 更名最佳球员
Browse files- app.py +12 -8
- src/db.py +1 -0
- src/models.py +4 -0
- src/repo.py +44 -10
- src/repo_base.py +1 -0
- src/theme.py +13 -0
- src/view_admin.py +20 -17
- src/view_home.py +20 -9
- src/view_me.py +8 -6
app.py
CHANGED
|
@@ -91,6 +91,7 @@ def server(input: Inputs, output: Outputs, session: Session) -> None:
|
|
| 91 |
repo.match_contributions(season),
|
| 92 |
repo.mvp_ranking(season),
|
| 93 |
repo.settled_mvps(),
|
|
|
|
| 94 |
)
|
| 95 |
|
| 96 |
# ------------------------------------------------------------------ 导出榜单图片
|
|
@@ -108,6 +109,7 @@ def server(input: Inputs, output: Outputs, session: Session) -> None:
|
|
| 108 |
key=lambda x: (-x["saves"], -x["attended"]),
|
| 109 |
)
|
| 110 |
mvp = repo.mvp_ranking(season)
|
|
|
|
| 111 |
|
| 112 |
def rowsify(items, valuefn):
|
| 113 |
return [(str(i), it["name"], valuefn(it)) for i, it in enumerate(items, 1)]
|
|
@@ -116,7 +118,8 @@ def server(input: Inputs, output: Outputs, session: Session) -> None:
|
|
| 116 |
("射手榜", rowsify(scorers, lambda x: f'{x["goals"]} 球')),
|
| 117 |
("助攻榜", rowsify(assisters, lambda x: f'{x["assists"]} 助')),
|
| 118 |
("出勤榜", rowsify(attend, lambda x: f'{x["attended"]}/{x["total"]}')),
|
| 119 |
-
("
|
|
|
|
| 120 |
]
|
| 121 |
if keepers: # 有门将才加扑救榜
|
| 122 |
boards.append(("扑救榜", rowsify(keepers, lambda x: f'{x["saves"]} 救')))
|
|
@@ -298,17 +301,18 @@ def server(input: Inputs, output: Outputs, session: Session) -> None:
|
|
| 298 |
def _save_mvp_lock():
|
| 299 |
if not input.btn_save_mvp_lock() or not is_admin():
|
| 300 |
return
|
| 301 |
-
def _sel(mid: int,
|
| 302 |
-
raw = input[f"
|
| 303 |
return int(raw) if (raw and raw != "none" and str(raw).isdigit()) else None
|
| 304 |
settled = opened = 0
|
| 305 |
for m in repo.list_matches():
|
| 306 |
mid = m["id"]
|
| 307 |
-
w1, w2 = _sel(mid), _sel(mid, "
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
|
|
|
| 312 |
bump()
|
| 313 |
ui.notification_show(
|
| 314 |
f"已结算 {settled} 场、重新开放 {opened} 场 🏅", type="message"
|
|
|
|
| 91 |
repo.match_contributions(season),
|
| 92 |
repo.mvp_ranking(season),
|
| 93 |
repo.settled_mvps(),
|
| 94 |
+
repo.dmvp_ranking(season),
|
| 95 |
)
|
| 96 |
|
| 97 |
# ------------------------------------------------------------------ 导出榜单图片
|
|
|
|
| 109 |
key=lambda x: (-x["saves"], -x["attended"]),
|
| 110 |
)
|
| 111 |
mvp = repo.mvp_ranking(season)
|
| 112 |
+
dmvp = repo.dmvp_ranking(season)
|
| 113 |
|
| 114 |
def rowsify(items, valuefn):
|
| 115 |
return [(str(i), it["name"], valuefn(it)) for i, it in enumerate(items, 1)]
|
|
|
|
| 118 |
("射手榜", rowsify(scorers, lambda x: f'{x["goals"]} 球')),
|
| 119 |
("助攻榜", rowsify(assisters, lambda x: f'{x["assists"]} 助')),
|
| 120 |
("出勤榜", rowsify(attend, lambda x: f'{x["attended"]}/{x["total"]}')),
|
| 121 |
+
("最佳球员 MVP", rowsify(mvp, lambda x: f'{x["mvps"]} 次')),
|
| 122 |
+
("铁血防守 MVP", rowsify(dmvp, lambda x: f'{x["dmvps"]} 次')),
|
| 123 |
]
|
| 124 |
if keepers: # 有门将才加扑救榜
|
| 125 |
boards.append(("扑救榜", rowsify(keepers, lambda x: f'{x["saves"]} 救')))
|
|
|
|
| 301 |
def _save_mvp_lock():
|
| 302 |
if not input.btn_save_mvp_lock() or not is_admin():
|
| 303 |
return
|
| 304 |
+
def _sel(mid: int, prefix: str = "mvpsel") -> int | None:
|
| 305 |
+
raw = input[f"{prefix}_{mid}"]()
|
| 306 |
return int(raw) if (raw and raw != "none" and str(raw).isdigit()) else None
|
| 307 |
settled = opened = 0
|
| 308 |
for m in repo.list_matches():
|
| 309 |
mid = m["id"]
|
| 310 |
+
w1, w2, wd = _sel(mid), _sel(mid, "mvpsel2"), _sel(mid, "dmvpsel")
|
| 311 |
+
cur = (m.get("mvp_user_id"), m.get("mvp2_user_id"), m.get("dmvp_user_id"))
|
| 312 |
+
if (w1, w2, wd) != cur:
|
| 313 |
+
repo.settle_mvp(mid, w1, w2, wd)
|
| 314 |
+
settled += 1 if (w1 or w2 or wd) else 0
|
| 315 |
+
opened += 1 if (w1 is None and w2 is None and wd is None) else 0
|
| 316 |
bump()
|
| 317 |
ui.notification_show(
|
| 318 |
f"已结算 {settled} 场、重新开放 {opened} 场 🏅", type="message"
|
src/db.py
CHANGED
|
@@ -59,6 +59,7 @@ def _run_light_migrations() -> None:
|
|
| 59 |
("matches", "mvp_locked", f"BOOLEAN NOT NULL DEFAULT {bool_false}"),
|
| 60 |
("matches", "mvp_user_id", "INTEGER"), # 可空:None=未结算
|
| 61 |
("matches", "mvp2_user_id", "INTEGER"), # 可空:双 MVP 第二人
|
|
|
|
| 62 |
]
|
| 63 |
for table, col, ddl in wanted:
|
| 64 |
if table not in existing:
|
|
|
|
| 59 |
("matches", "mvp_locked", f"BOOLEAN NOT NULL DEFAULT {bool_false}"),
|
| 60 |
("matches", "mvp_user_id", "INTEGER"), # 可空:None=未结算
|
| 61 |
("matches", "mvp2_user_id", "INTEGER"), # 可空:双 MVP 第二人
|
| 62 |
+
("matches", "dmvp_user_id", "INTEGER"), # 可空:铁血防守 MVP
|
| 63 |
]
|
| 64 |
for table, col, ddl in wanted:
|
| 65 |
if table not in existing:
|
src/models.py
CHANGED
|
@@ -64,6 +64,10 @@ class Match(Base):
|
|
| 64 |
mvp2_user_id: Mapped[int | None] = mapped_column(
|
| 65 |
ForeignKey("users.id"), nullable=True
|
| 66 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
| 68 |
|
| 69 |
|
|
|
|
| 64 |
mvp2_user_id: Mapped[int | None] = mapped_column(
|
| 65 |
ForeignKey("users.id"), nullable=True
|
| 66 |
)
|
| 67 |
+
# 铁血防守 MVP(同样由管理员结算指定;独立于最佳球员 MVP,可同场同人)
|
| 68 |
+
dmvp_user_id: Mapped[int | None] = mapped_column(
|
| 69 |
+
ForeignKey("users.id"), nullable=True
|
| 70 |
+
)
|
| 71 |
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
| 72 |
|
| 73 |
|
src/repo.py
CHANGED
|
@@ -344,12 +344,16 @@ def player_rank(user_id: int) -> Optional[dict[str, Any]]:
|
|
| 344 |
my_mvps = next(
|
| 345 |
(r["mvps"] for r in mvp_ranking() if r["user_id"] == user_id), 0
|
| 346 |
)
|
|
|
|
|
|
|
|
|
|
| 347 |
return {
|
| 348 |
**me,
|
| 349 |
"goal_rank": goal_rank,
|
| 350 |
"assist_rank": assist_rank,
|
| 351 |
"n_players": len(agg),
|
| 352 |
"mvps": my_mvps,
|
|
|
|
| 353 |
}
|
| 354 |
|
| 355 |
|
|
@@ -411,12 +415,37 @@ def mvp_ranking(season: Optional[int] = None) -> list[dict[str, Any]]:
|
|
| 411 |
return sorted(tally.values(), key=lambda x: (-x["mvps"], x["name"]))
|
| 412 |
|
| 413 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
def settle_mvp(
|
| 415 |
-
match_id: int,
|
|
|
|
|
|
|
|
|
|
| 416 |
) -> None:
|
| 417 |
-
"""管理员结算某场 MVP
|
| 418 |
|
| 419 |
-
数据不变式:
|
|
|
|
| 420 |
"""
|
| 421 |
if mvp_user_id is None and mvp2_user_id is not None:
|
| 422 |
mvp_user_id, mvp2_user_id = mvp2_user_id, None
|
|
@@ -428,25 +457,30 @@ def settle_mvp(
|
|
| 428 |
return
|
| 429 |
m.mvp_user_id = mvp_user_id
|
| 430 |
m.mvp2_user_id = mvp2_user_id
|
| 431 |
-
m.
|
|
|
|
| 432 |
|
| 433 |
|
| 434 |
def settled_mvps() -> dict[int, dict[str, Any]]:
|
| 435 |
-
"""已结算场次的
|
| 436 |
|
| 437 |
-
|
| 438 |
"""
|
| 439 |
with get_session() as s:
|
| 440 |
rows = (
|
| 441 |
-
s.query(Match.id, Match.mvp_user_id, Match.mvp2_user_id)
|
| 442 |
-
.filter(Match.mvp_user_id.isnot(None))
|
| 443 |
.all()
|
| 444 |
)
|
| 445 |
names = {u.id: u.display_name for u in s.query(User).all()}
|
| 446 |
out: dict[int, dict[str, Any]] = {}
|
| 447 |
-
for mid, u1, u2 in rows:
|
| 448 |
ids = [u for u in (u1, u2) if u is not None]
|
| 449 |
-
out[mid] = {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
return out
|
| 451 |
|
| 452 |
|
|
|
|
| 344 |
my_mvps = next(
|
| 345 |
(r["mvps"] for r in mvp_ranking() if r["user_id"] == user_id), 0
|
| 346 |
)
|
| 347 |
+
my_dmvps = next(
|
| 348 |
+
(r["dmvps"] for r in dmvp_ranking() if r["user_id"] == user_id), 0
|
| 349 |
+
)
|
| 350 |
return {
|
| 351 |
**me,
|
| 352 |
"goal_rank": goal_rank,
|
| 353 |
"assist_rank": assist_rank,
|
| 354 |
"n_players": len(agg),
|
| 355 |
"mvps": my_mvps,
|
| 356 |
+
"dmvps": my_dmvps,
|
| 357 |
}
|
| 358 |
|
| 359 |
|
|
|
|
| 415 |
return sorted(tally.values(), key=lambda x: (-x["mvps"], x["name"]))
|
| 416 |
|
| 417 |
|
| 418 |
+
def dmvp_ranking(season: Optional[int] = None) -> list[dict[str, Any]]:
|
| 419 |
+
"""当季铁血防守 MVP 榜:按获得次数降序(每场由领队结算指定一人)。"""
|
| 420 |
+
with get_session() as s:
|
| 421 |
+
match_ids = _season_match_ids(s, season)
|
| 422 |
+
if not match_ids:
|
| 423 |
+
return []
|
| 424 |
+
rows = (
|
| 425 |
+
s.query(Match.dmvp_user_id, User.display_name, User.jersey_number)
|
| 426 |
+
.join(User, Match.dmvp_user_id == User.id)
|
| 427 |
+
.filter(Match.id.in_(match_ids), Match.dmvp_user_id.isnot(None))
|
| 428 |
+
.all()
|
| 429 |
+
)
|
| 430 |
+
tally: dict[int, dict[str, Any]] = {}
|
| 431 |
+
for uid, name, jersey in rows:
|
| 432 |
+
t = tally.setdefault(
|
| 433 |
+
uid, {"user_id": uid, "name": name, "jersey": jersey, "dmvps": 0}
|
| 434 |
+
)
|
| 435 |
+
t["dmvps"] += 1
|
| 436 |
+
return sorted(tally.values(), key=lambda x: (-x["dmvps"], x["name"]))
|
| 437 |
+
|
| 438 |
+
|
| 439 |
def settle_mvp(
|
| 440 |
+
match_id: int,
|
| 441 |
+
mvp_user_id: Optional[int],
|
| 442 |
+
mvp2_user_id: Optional[int] = None,
|
| 443 |
+
dmvp_user_id: Optional[int] = None,
|
| 444 |
) -> None:
|
| 445 |
+
"""管理员结算某场:最佳球员 MVP(可双)+ 铁血防守 MVP;全传 None=取消结算重开。
|
| 446 |
|
| 447 |
+
数据不变式:任一奖项有人=已结算锁票;最佳球员两栏同一人自动去重、第一栏空则
|
| 448 |
+
第二栏顶上;防守 MVP 独立评选,可与最佳球员同场同人。
|
| 449 |
"""
|
| 450 |
if mvp_user_id is None and mvp2_user_id is not None:
|
| 451 |
mvp_user_id, mvp2_user_id = mvp2_user_id, None
|
|
|
|
| 457 |
return
|
| 458 |
m.mvp_user_id = mvp_user_id
|
| 459 |
m.mvp2_user_id = mvp2_user_id
|
| 460 |
+
m.dmvp_user_id = dmvp_user_id
|
| 461 |
+
m.mvp_locked = mvp_user_id is not None or dmvp_user_id is not None
|
| 462 |
|
| 463 |
|
| 464 |
def settled_mvps() -> dict[int, dict[str, Any]]:
|
| 465 |
+
"""已结算场次的奖项:{match_id: {"user_ids": [...], "names": [...], "dmvp_name": str|None}}。
|
| 466 |
|
| 467 |
+
names 为最佳球员 MVP(单/双);dmvp_name 为铁血防守 MVP(可空)。
|
| 468 |
"""
|
| 469 |
with get_session() as s:
|
| 470 |
rows = (
|
| 471 |
+
s.query(Match.id, Match.mvp_user_id, Match.mvp2_user_id, Match.dmvp_user_id)
|
| 472 |
+
.filter((Match.mvp_user_id.isnot(None)) | (Match.dmvp_user_id.isnot(None)))
|
| 473 |
.all()
|
| 474 |
)
|
| 475 |
names = {u.id: u.display_name for u in s.query(User).all()}
|
| 476 |
out: dict[int, dict[str, Any]] = {}
|
| 477 |
+
for mid, u1, u2, ud in rows:
|
| 478 |
ids = [u for u in (u1, u2) if u is not None]
|
| 479 |
+
out[mid] = {
|
| 480 |
+
"user_ids": ids,
|
| 481 |
+
"names": [names.get(u, "?") for u in ids],
|
| 482 |
+
"dmvp_name": names.get(ud) if ud is not None else None,
|
| 483 |
+
}
|
| 484 |
return out
|
| 485 |
|
| 486 |
|
src/repo_base.py
CHANGED
|
@@ -33,6 +33,7 @@ def _match_dict(m: Match) -> dict[str, Any]:
|
|
| 33 |
"mvp_locked": bool(getattr(m, "mvp_locked", False)),
|
| 34 |
"mvp_user_id": getattr(m, "mvp_user_id", None),
|
| 35 |
"mvp2_user_id": getattr(m, "mvp2_user_id", None),
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
|
|
|
|
| 33 |
"mvp_locked": bool(getattr(m, "mvp_locked", False)),
|
| 34 |
"mvp_user_id": getattr(m, "mvp_user_id", None),
|
| 35 |
"mvp2_user_id": getattr(m, "mvp2_user_id", None),
|
| 36 |
+
"dmvp_user_id": getattr(m, "dmvp_user_id", None),
|
| 37 |
}
|
| 38 |
|
| 39 |
|
src/theme.py
CHANGED
|
@@ -230,6 +230,17 @@ body{
|
|
| 230 |
box-shadow:0 14px 34px -18px rgba(31,169,113,.45), inset 0 1px 0 rgba(255,255,255,.08)}
|
| 231 |
.sde-hero.c-green .big .n{background:linear-gradient(180deg,#c9f5e2,var(--green));-webkit-background-clip:text;background-clip:text}
|
| 232 |
.sde-hero.c-green .title{color:var(--green)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
.sde-board .sde-empty{padding:18px}
|
| 234 |
|
| 235 |
/* ================= 每场比分 · 战报记分牌 ================= */
|
|
@@ -278,6 +289,8 @@ body{
|
|
| 278 |
.sde-chip.assist{border-color:rgba(61,165,217,.4);background:rgba(61,165,217,.10)}
|
| 279 |
.sde-chip.mvp{border-color:rgba(212,175,55,.65);background:linear-gradient(90deg,rgba(212,175,55,.22),rgba(212,175,55,.08));
|
| 280 |
color:#ffe9a8;font-weight:800;box-shadow:0 0 14px -4px rgba(212,175,55,.7)}
|
|
|
|
|
|
|
| 281 |
@media(max-width:560px){
|
| 282 |
.sde-sc-score .n{font-size:30px}
|
| 283 |
.sde-hero .big .n{font-size:34px}
|
|
|
|
| 230 |
box-shadow:0 14px 34px -18px rgba(31,169,113,.45), inset 0 1px 0 rgba(255,255,255,.08)}
|
| 231 |
.sde-hero.c-green .big .n{background:linear-gradient(180deg,#c9f5e2,var(--green));-webkit-background-clip:text;background-clip:text}
|
| 232 |
.sde-hero.c-green .title{color:var(--green)}
|
| 233 |
+
.sde-hero.c-red{border-color:rgba(226,56,75,.55);
|
| 234 |
+
background:radial-gradient(120% 150% at 100% 0%, rgba(200,16,46,.30), transparent 55%),
|
| 235 |
+
linear-gradient(135deg, rgba(200,16,46,.17), rgba(17,37,74,.9) 58%);
|
| 236 |
+
box-shadow:0 14px 34px -18px rgba(200,16,46,.5), inset 0 1px 0 rgba(255,255,255,.08)}
|
| 237 |
+
.sde-hero.c-red .big .n{background:linear-gradient(180deg,#ffd2d8,var(--red-2));-webkit-background-clip:text;background-clip:text;
|
| 238 |
+
filter:drop-shadow(0 4px 14px rgba(200,16,46,.5))}
|
| 239 |
+
.sde-hero.c-red .title{color:var(--red-2)}
|
| 240 |
+
.sde-hero.c-red .crown{filter:drop-shadow(0 3px 8px rgba(200,16,46,.6))}
|
| 241 |
+
.sde-brow.c-red .bar{background:linear-gradient(90deg,var(--red-2),rgba(200,16,46,.15))}
|
| 242 |
+
.sde-brow.c-red .val{color:#ff9fab}
|
| 243 |
+
.sde-brow.c-red.r2,.sde-brow.c-red.r3{border-color:rgba(226,56,75,.30)}
|
| 244 |
.sde-board .sde-empty{padding:18px}
|
| 245 |
|
| 246 |
/* ================= 每场比分 · 战报记分牌 ================= */
|
|
|
|
| 289 |
.sde-chip.assist{border-color:rgba(61,165,217,.4);background:rgba(61,165,217,.10)}
|
| 290 |
.sde-chip.mvp{border-color:rgba(212,175,55,.65);background:linear-gradient(90deg,rgba(212,175,55,.22),rgba(212,175,55,.08));
|
| 291 |
color:#ffe9a8;font-weight:800;box-shadow:0 0 14px -4px rgba(212,175,55,.7)}
|
| 292 |
+
.sde-chip.dmvp{border-color:rgba(226,56,75,.6);background:linear-gradient(90deg,rgba(200,16,46,.24),rgba(200,16,46,.08));
|
| 293 |
+
color:#ffc3cb;font-weight:800;box-shadow:0 0 14px -4px rgba(200,16,46,.7)}
|
| 294 |
@media(max-width:560px){
|
| 295 |
.sde-sc-score .n{font-size:30px}
|
| 296 |
.sde-hero .big .n{font-size:34px}
|
src/view_admin.py
CHANGED
|
@@ -39,8 +39,9 @@ def _admin_panel(user_choices: dict[str, str]) -> ui.Tag:
|
|
| 39 |
mvp_settle = theme.card(
|
| 40 |
"结算本场 MVP",
|
| 41 |
ui.div(
|
| 42 |
-
"由领队为每场选定 MVP(
|
| 43 |
-
"
|
|
|
|
| 44 |
class_="sde-sub",
|
| 45 |
),
|
| 46 |
ui.output_ui("admin_mvp_settle"),
|
|
@@ -143,38 +144,40 @@ def admin_mvp_settle_view(
|
|
| 143 |
vote_summary: dict[int, dict[str, Any]],
|
| 144 |
player_choices: dict[str, str],
|
| 145 |
) -> ui.Tag:
|
| 146 |
-
"""每场
|
| 147 |
|
| 148 |
-
|
| 149 |
"""
|
| 150 |
if not matches:
|
| 151 |
return ui.div("还没有比赛,请先在上方添加。", class_="sde-empty")
|
| 152 |
choices = {"none": "— 未结算 —", **player_choices}
|
| 153 |
head = ui.tags.tr(
|
| 154 |
-
*[ui.tags.th(h) for h in
|
|
|
|
| 155 |
)
|
| 156 |
body = []
|
| 157 |
for m in matches:
|
| 158 |
mid = m["id"]
|
| 159 |
win = vote_summary.get(mid)
|
| 160 |
leader = f'{"、".join(win["names"])}({win["votes"]} 票)' if win else "暂无投票"
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
body.append(
|
| 168 |
ui.tags.tr(
|
| 169 |
ui.tags.td(m["date"]),
|
| 170 |
ui.tags.td(m["opponent"]),
|
| 171 |
ui.tags.td(leader),
|
| 172 |
-
|
| 173 |
-
ui.input_select(f"mvpsel_{mid}", "", choices, selected=sel1, width="170px")
|
| 174 |
-
),
|
| 175 |
-
ui.tags.td(
|
| 176 |
-
ui.input_select(f"mvpsel2_{mid}", "", choices, selected=sel2, width="170px")
|
| 177 |
-
),
|
| 178 |
)
|
| 179 |
)
|
| 180 |
return ui.div(
|
|
|
|
| 39 |
mvp_settle = theme.card(
|
| 40 |
"结算本场 MVP",
|
| 41 |
ui.div(
|
| 42 |
+
"由领队为每场选定「最佳球员 MVP」(可双:两人各计一次)与「铁血防守 MVP」"
|
| 43 |
+
"(队员投票数仅作参考)。任一奖项有人即结算锁票;全部改回「— 未结算 —」"
|
| 44 |
+
"并保存可重新开放投票。",
|
| 45 |
class_="sde-sub",
|
| 46 |
),
|
| 47 |
ui.output_ui("admin_mvp_settle"),
|
|
|
|
| 144 |
vote_summary: dict[int, dict[str, Any]],
|
| 145 |
player_choices: dict[str, str],
|
| 146 |
) -> ui.Tag:
|
| 147 |
+
"""每场结算表:投票参考 + 最佳球员双下拉 + 铁血防守下拉。
|
| 148 |
|
| 149 |
+
id:mvpsel_{mid} / mvpsel2_{mid} / dmvpsel_{mid};任一有人=结算锁定,全 none=重开。
|
| 150 |
"""
|
| 151 |
if not matches:
|
| 152 |
return ui.div("还没有比赛,请先在上方添加。", class_="sde-empty")
|
| 153 |
choices = {"none": "— 未结算 —", **player_choices}
|
| 154 |
head = ui.tags.tr(
|
| 155 |
+
*[ui.tags.th(h) for h in
|
| 156 |
+
["日期", "对手", "投票参考", "最佳球员 ①", "最佳球员 ②(可选)", "🛡️ 铁血防守"]]
|
| 157 |
)
|
| 158 |
body = []
|
| 159 |
for m in matches:
|
| 160 |
mid = m["id"]
|
| 161 |
win = vote_summary.get(mid)
|
| 162 |
leader = f'{"、".join(win["names"])}({win["votes"]} 票)' if win else "暂无投票"
|
| 163 |
+
sels = []
|
| 164 |
+
for key, field in (
|
| 165 |
+
(f"mvpsel_{mid}", "mvp_user_id"),
|
| 166 |
+
(f"mvpsel2_{mid}", "mvp2_user_id"),
|
| 167 |
+
(f"dmvpsel_{mid}", "dmvp_user_id"),
|
| 168 |
+
):
|
| 169 |
+
sel = str(m.get(field) or "none")
|
| 170 |
+
if sel not in choices:
|
| 171 |
+
sel = "none"
|
| 172 |
+
sels.append(
|
| 173 |
+
ui.tags.td(ui.input_select(key, "", choices, selected=sel, width="150px"))
|
| 174 |
+
)
|
| 175 |
body.append(
|
| 176 |
ui.tags.tr(
|
| 177 |
ui.tags.td(m["date"]),
|
| 178 |
ui.tags.td(m["opponent"]),
|
| 179 |
ui.tags.td(leader),
|
| 180 |
+
*sels,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
)
|
| 182 |
)
|
| 183 |
return ui.div(
|
src/view_home.py
CHANGED
|
@@ -17,6 +17,7 @@ def home_view(
|
|
| 17 |
contributions: dict[int, dict[str, list[str]]],
|
| 18 |
mvp: list[dict[str, Any]],
|
| 19 |
mvp_summary: Optional[dict[int, dict[str, Any]]] = None,
|
|
|
|
| 20 |
) -> ui.Tag:
|
| 21 |
stats = ui.div(
|
| 22 |
theme.stat(summary["played"], "场次", accent="cream"),
|
|
@@ -51,12 +52,16 @@ def home_view(
|
|
| 51 |
attend, "全勤铁人", lambda r: r["attended"], "场", "c-green",
|
| 52 |
lambda r: f'共 {r["total"]} 场 · 进球+助攻 {r["ga"]}', me_id,
|
| 53 |
), icon="📋"),
|
| 54 |
-
theme.card("
|
| 55 |
-
mvp, "
|
| 56 |
None, me_id,
|
| 57 |
), icon="🏅", sub="每场由领队结算 · 计获得次数"),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
]
|
| 59 |
-
widths = [6, 6, 6, 6]
|
| 60 |
if keepers: # 有门将才显示扑救榜
|
| 61 |
board_cards.append(
|
| 62 |
theme.card("扑救榜", _impact_board(
|
|
@@ -64,7 +69,7 @@ def home_view(
|
|
| 64 |
lambda r: f'出场 {r["attended"]}/{r["total"]}', me_id,
|
| 65 |
), icon="🧤", sub="仅门将")
|
| 66 |
)
|
| 67 |
-
widths = [6, 6, 6, 6, 6]
|
| 68 |
boards = ui.layout_columns(*board_cards, col_widths=widths, fill=False)
|
| 69 |
return ui.div(
|
| 70 |
theme.card("球队战绩", stats, icon="🏆"),
|
|
@@ -97,11 +102,17 @@ def _scorecard(
|
|
| 97 |
"""一场比赛的战报卡:结果色边+光晕、居中大比分、结果大徽章、明细芯片。"""
|
| 98 |
res = m["result"]
|
| 99 |
chips: list[Any] = []
|
| 100 |
-
if m.get("mvp_locked") and mvp
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
for who in c.get("goals", []):
|
| 106 |
chips.append(ui.span(ui.span("⚽", class_="ic"), who, class_="sde-chip goal"))
|
| 107 |
for who in c.get("assists", []):
|
|
|
|
| 17 |
contributions: dict[int, dict[str, list[str]]],
|
| 18 |
mvp: list[dict[str, Any]],
|
| 19 |
mvp_summary: Optional[dict[int, dict[str, Any]]] = None,
|
| 20 |
+
dmvp: Optional[list[dict[str, Any]]] = None,
|
| 21 |
) -> ui.Tag:
|
| 22 |
stats = ui.div(
|
| 23 |
theme.stat(summary["played"], "场次", accent="cream"),
|
|
|
|
| 52 |
attend, "全勤铁人", lambda r: r["attended"], "场", "c-green",
|
| 53 |
lambda r: f'共 {r["total"]} 场 · 进球+助攻 {r["ga"]}', me_id,
|
| 54 |
), icon="📋"),
|
| 55 |
+
theme.card("最佳球员 MVP", _impact_board(
|
| 56 |
+
mvp, "最佳球员", lambda r: r["mvps"], "次", "",
|
| 57 |
None, me_id,
|
| 58 |
), icon="🏅", sub="每场由领队结算 · 计获得次数"),
|
| 59 |
+
theme.card("铁血防守 MVP", _impact_board(
|
| 60 |
+
dmvp or [], "铁血防守", lambda r: r["dmvps"], "次", "c-red",
|
| 61 |
+
None, me_id,
|
| 62 |
+
), icon="🛡️", sub="每场由领队结算 · 计获得次数"),
|
| 63 |
]
|
| 64 |
+
widths = [6, 6, 6, 6, 6]
|
| 65 |
if keepers: # 有门将才显示扑救榜
|
| 66 |
board_cards.append(
|
| 67 |
theme.card("扑救榜", _impact_board(
|
|
|
|
| 69 |
lambda r: f'出场 {r["attended"]}/{r["total"]}', me_id,
|
| 70 |
), icon="🧤", sub="仅门将")
|
| 71 |
)
|
| 72 |
+
widths = [6, 6, 6, 6, 6, 6]
|
| 73 |
boards = ui.layout_columns(*board_cards, col_widths=widths, fill=False)
|
| 74 |
return ui.div(
|
| 75 |
theme.card("球队战绩", stats, icon="🏆"),
|
|
|
|
| 102 |
"""一场比赛的战报卡:结果色边+光晕、居中大比分、结果大徽章、明细芯片。"""
|
| 103 |
res = m["result"]
|
| 104 |
chips: list[Any] = []
|
| 105 |
+
if m.get("mvp_locked") and mvp:
|
| 106 |
+
if mvp.get("names"):
|
| 107 |
+
chips.append(
|
| 108 |
+
ui.span(ui.span("🏅", class_="ic"), "最佳球员 " + "、".join(mvp["names"]),
|
| 109 |
+
class_="sde-chip mvp")
|
| 110 |
+
)
|
| 111 |
+
if mvp.get("dmvp_name"):
|
| 112 |
+
chips.append(
|
| 113 |
+
ui.span(ui.span("🛡️", class_="ic"), f'铁血防守 {mvp["dmvp_name"]}',
|
| 114 |
+
class_="sde-chip dmvp")
|
| 115 |
+
)
|
| 116 |
for who in c.get("goals", []):
|
| 117 |
chips.append(ui.span(ui.span("⚽", class_="ic"), who, class_="sde-chip goal"))
|
| 118 |
for who in c.get("assists", []):
|
src/view_me.py
CHANGED
|
@@ -61,7 +61,8 @@ def my_totals_view(rank: Optional[dict[str, Any]]) -> ui.Tag:
|
|
| 61 |
),
|
| 62 |
theme.stat(rank["ga"], "进球+助攻", accent="cream"),
|
| 63 |
theme.stat(f'{rank["attended"]}/{rank["total"]}', "出场", accent="green"),
|
| 64 |
-
theme.stat(rank.get("mvps", 0), "MVP", "领队结算", accent="gold"),
|
|
|
|
| 65 |
]
|
| 66 |
if rank.get("is_goalkeeper"):
|
| 67 |
items.append(theme.stat(rank.get("saves", 0), "我的扑救", "门将", accent="blue"))
|
|
@@ -197,11 +198,12 @@ def my_mvp_form_view(
|
|
| 197 |
mid = m["id"]
|
| 198 |
if m.get("mvp_locked"):
|
| 199 |
win = settled.get(mid)
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
| 205 |
ctl = ui.div(ui.span(text, class_="sde-mvp-settled"), class_="sde-ctl-row")
|
| 206 |
else:
|
| 207 |
sel = str(my_votes_map.get(mid, "none"))
|
|
|
|
| 61 |
),
|
| 62 |
theme.stat(rank["ga"], "进球+助攻", accent="cream"),
|
| 63 |
theme.stat(f'{rank["attended"]}/{rank["total"]}', "出场", accent="green"),
|
| 64 |
+
theme.stat(rank.get("mvps", 0), "最佳球员 MVP", "领队结算", accent="gold"),
|
| 65 |
+
theme.stat(rank.get("dmvps", 0), "铁血防守 MVP", "领队结算", accent="red"),
|
| 66 |
]
|
| 67 |
if rank.get("is_goalkeeper"):
|
| 68 |
items.append(theme.stat(rank.get("saves", 0), "我的扑救", "门将", accent="blue"))
|
|
|
|
| 198 |
mid = m["id"]
|
| 199 |
if m.get("mvp_locked"):
|
| 200 |
win = settled.get(mid)
|
| 201 |
+
parts = []
|
| 202 |
+
if win and win.get("names"):
|
| 203 |
+
parts.append(f'🏅 最佳球员:{"、".join(win["names"])}')
|
| 204 |
+
if win and win.get("dmvp_name"):
|
| 205 |
+
parts.append(f'🛡️ 铁血防守:{win["dmvp_name"]}')
|
| 206 |
+
text = (" · ".join(parts) + "(领队已结算)") if parts else "本场 MVP 待定(投票已锁定)"
|
| 207 |
ctl = ui.div(ui.span(text, class_="sde-mvp-settled"), class_="sde-ctl-row")
|
| 208 |
else:
|
| 209 |
sel = str(my_votes_map.get(mid, "none"))
|