Download src/view_me.py from sidaier/fc: direct link, hf CLI and curl.
- Browser
- Download file 19.6 kB
-
https://huggingface.co/spaces/sidaier/fc/resolve/main/src/view_me.py
- Command line
-
hf download hf://spaces/sidaier/fc/src/view_me.py
-
curl -L -o view_me.py https://huggingface.co/spaces/sidaier/fc/resolve/main/src/view_me.py
19.6 kB
| """我的数据视图:总览、进球/助攻趋势图、逐场登记、MVP 投票、改密面板。 | |
| 约定:触发动作的按钮放在 _my_panel 静态区(每次登录构建一次),id 稳定; | |
| 仅「带初值的输入框」放进随数据刷新的 output_ui。 | |
| """ | |
| from __future__ import annotations | |
| from typing import Any, Optional | |
| from shiny import ui | |
| from . import config, theme | |
| from .view_common import _match_top, _name_with_jersey | |
| def _my_panel() -> ui.Tag: | |
| return ui.div( | |
| ui.output_ui("my_fifa_card"), | |
| ui.output_ui("my_totals"), | |
| ui.output_ui("my_trend"), | |
| theme.card( | |
| "逐场登记", | |
| ui.div( | |
| "默认每场都算你出勤。某场没上场就关掉「出勤」开关;" | |
| "进了球 / 有助攻就填上数字,然后点保存。", | |
| class_="sde-sub", | |
| ), | |
| ui.output_ui("my_form"), | |
| ui.input_action_button("btn_save_stats", "💾 保存我的数据", class_="btn-primary"), | |
| icon="📝", | |
| ), | |
| theme.card( | |
| "评选本场 MVP", | |
| ui.div("为每场比赛投出你心中的 MVP(可改、可不投;不能投自己)。", class_="sde-sub"), | |
| ui.output_ui("my_mvp_form"), | |
| ui.input_action_button("btn_save_mvp", "🏅 保存我的投票", class_="btn-primary"), | |
| icon="🗳️", | |
| ), | |
| theme.card( | |
| "修改密码", | |
| ui.div("首次登录请尽快修改初始密码。新密码至少 6 位。", class_="sde-sub"), | |
| ui.layout_columns( | |
| ui.input_password("cp_old", "当前密码"), | |
| ui.input_password("cp_new", "新密码"), | |
| ui.input_password("cp_new2", "确认新密码"), | |
| col_widths=[4, 4, 4], | |
| fill=False, | |
| ), | |
| ui.input_action_button("btn_change_pw", "🔑 修改密码", class_="btn-primary"), | |
| icon="🔒", | |
| ), | |
| ) | |
| def _earned_badges(rank: dict[str, Any]) -> list[ui.Tag]: | |
| """按数据自动点亮称号徽章:队内第一/荣誉/里程碑。""" | |
| b: list[ui.Tag] = [] | |
| if rank["goals"] > 0 and rank["goal_rank"] == 1: | |
| b.append(ui.span("⚽ 队内射手王", class_="sde-badge")) | |
| if rank["assists"] > 0 and rank["assist_rank"] == 1: | |
| b.append(ui.span("🎯 队内助攻王", class_="sde-badge b-blue")) | |
| if rank.get("mvps", 0) > 0: | |
| b.append(ui.span(f'🏅 最佳球员 ×{rank["mvps"]}', class_="sde-badge")) | |
| if rank.get("dmvps", 0) > 0: | |
| b.append(ui.span(f'🛡️ 铁血防守 ×{rank["dmvps"]}', class_="sde-badge b-red")) | |
| if rank["total"] > 0 and rank["attended"] == rank["total"]: | |
| b.append(ui.span("📋 全勤铁人", class_="sde-badge b-green")) | |
| if rank.get("is_goalkeeper") and rank.get("saves", 0) > 0: | |
| b.append(ui.span(f'🧤 扑救 ×{rank["saves"]}', class_="sde-badge b-blue")) | |
| if rank["ga"] >= 10: | |
| b.append(ui.span("🔥 进攻核心 · 参与10+球", class_="sde-badge b-red")) | |
| elif rank["ga"] >= 5: | |
| b.append(ui.span("⚡ 关键先生 · 参与5+球", class_="sde-badge")) | |
| return b | |
| def _motivation(rank: dict[str, Any]) -> tuple[str, str]: | |
| """按数据生成一句教练寄语(标题, 正文)。规则从上到下取第一条命中。""" | |
| n = rank["n_players"] | |
| if rank.get("mvps", 0) >= 2: | |
| return "球队的招牌 🌟", ( | |
| f'{rank["mvps"]} 座最佳球员奖杯说明了一切——你就是肆带贰的门面。' | |
| "保持状态,队史会记住你的名字。" | |
| ) | |
| if rank["goals"] > 0 and rank["goal_rank"] == 1: | |
| return "锋线杀神 🔥", ( | |
| f'{rank["goals"]} 粒进球领跑全队!对手教练布置战术时,' | |
| "白板上圈出来的那个号码就是你。继续轰门!" | |
| ) | |
| if rank["assists"] > 0 and rank["assist_rank"] == 1: | |
| return "中场大脑 🧠", ( | |
| f'{rank["assists"]} 次助攻全队第一。队友们的进球集锦里,' | |
| "一半镜头都该给传出致命一传的你。" | |
| ) | |
| if rank.get("dmvps", 0) > 0: | |
| return "后防铁闸 🛡️", ( | |
| f'{rank["dmvps"]} 次铁血防守认证——前锋们敢内切,是因为知道身后有你。' | |
| "防守赢得冠军!" | |
| ) | |
| if rank.get("is_goalkeeper") and rank.get("saves", 0) > 0: | |
| return "门线守护神 🧤", ( | |
| f'{rank["saves"]} 次扑救,每一次都是白捡的净胜球。' | |
| "手套已经热了,下一场继续封门!" | |
| ) | |
| if rank["total"] > 0 and rank["attended"] == rank["total"]: | |
| return "全勤铁人 💪", ( | |
| "一场不落!天赋会迟到,但出勤从不缺席——球队最稳的一块拼图就是你。" | |
| ) | |
| if rank["ga"] > 0: | |
| return "稳步上分 📈", ( | |
| f'已参与 {rank["ga"]} 粒进球,射手榜第 {rank["goal_rank"]}/{n}。' | |
| "距离前排只差几脚发力,下一场就是你的破纪录之夜。" | |
| ) | |
| return "登场即主角 🚀", ( | |
| "数据栏还空着?那是留给你书写的。下一场比赛,从第一粒进球、" | |
| "第一次助攻开始——全队都在等你的高光时刻。" | |
| ) | |
| _FIFA_LABELS = { | |
| "SHO": "射术", "PAS": "传球", "STA": "体能", | |
| "HON": "荣誉", "DEF": "防守", "IMP": "影响", | |
| } | |
| def _card_quote(card: dict[str, Any]) -> tuple[str, str]: | |
| """球星卡背面寄语(标题, 正文):按卡面数据画像,从上到下取第一条命中。""" | |
| if card.get("mvps", 0) >= 2: | |
| return "球队的招牌 🌟", ( | |
| f'{card["mvps"]} 座最佳球员奖杯说明了一切——你就是肆带贰的门面。' | |
| ) | |
| if card.get("mvps", 0) == 1: | |
| return "高光时刻 🏅", "最佳球员奖杯已入账,状态正盛——下一座已经在路上。" | |
| if card.get("dmvps", 0) > 0: | |
| return "后防铁闸 🛡️", ( | |
| f'{card["dmvps"]} 次铁血防守认证——前锋们敢内切,是因为身后有你。' | |
| ) | |
| if card.get("is_goalkeeper") and card.get("saves", 0) > 0: | |
| return "门线守护神 🧤", f'{card["saves"]} 次扑救,每一次都是白捡的净胜球。' | |
| if card.get("goals", 0) >= 3: | |
| return "锋线杀神 🔥", f'{card["goals"]} 粒进球在手,对手后卫看到你就头疼。' | |
| if card.get("assists", 0) >= 3: | |
| return "中场大脑 🧠", f'{card["assists"]} 次助攻,队友的进球集锦里全是你的传球。' | |
| if card.get("total", 0) > 0 and card.get("attended") == card.get("total"): | |
| return "全勤铁人 💪", "一场不落!球队最稳的一块拼图就是你。" | |
| if card.get("ga", 0) > 0: | |
| return "稳步上分 📈", f'已参与 {card["ga"]} 粒进球,下一场就是爆发之夜。' | |
| return "登场即主角 🚀", "数据栏还空着?那是留给你书写的。" | |
| def _fifa_back_tag(card: dict[str, Any], tier: str) -> ui.Tag: | |
| """球星卡背面:真实数据 2×2 + 出场 + 寄语(与正面同档配色)。""" | |
| qt, qb = _card_quote(card) | |
| cells = [ | |
| ("⚽", "进球", card.get("goals", 0)), | |
| ("🅰️", "助攻", card.get("assists", 0)), | |
| ("🏅", "最佳球员", card.get("mvps", 0)), | |
| ("🛡️", "防守MVP", card.get("dmvps", 0)), | |
| ] | |
| if card.get("is_goalkeeper"): | |
| cells.append(("🧤", "扑救", card.get("saves", 0))) | |
| grid = [ | |
| ui.div( | |
| ui.span(f"{ic} {lb}", class_="bk"), | |
| ui.span(str(v), class_="bn"), | |
| class_="bst", | |
| ) | |
| for ic, lb, v in cells | |
| ] | |
| return ui.div( | |
| ui.div(card["name"], class_="bname"), | |
| ui.div(f'出场 {card.get("attended", 0)}/{card.get("total", 0)}', class_="bapps"), | |
| ui.div(*grid, class_="bgrid"), | |
| ui.div(ui.div(qt, class_="qt"), ui.div(qb, class_="qb"), class_="bquote"), | |
| ui.div("点击翻回 ↩", class_="bflip"), | |
| class_=f"sde-fifa sde-fifa-back {tier}", | |
| ) | |
| def fifa_card_tag(card: dict[str, Any], is_me: bool = False, | |
| flip_prefix: str = "", diamond: bool = False) -> ui.Tag: | |
| """渲染一张 FIFA 卡(供个人页与主页卡墙复用)。 | |
| 档位:OVR≥92 传奇紫 / ≥80 金 / ≥68 银 / 其余铜。 | |
| flip_prefix 非空时启用点击 3D 翻面(checkbox+label,零 JS),背面为真实数据+寄语。 | |
| diamond=True 时套用「钻石」专属皮肤(纯外观,仅个人页本人触发)——OVR 数字与 | |
| 六维数据完全不变,只换配色/边框,不影响任何评分与榜单。 | |
| """ | |
| ovr = card["ovr"] | |
| tier = ("diamond" if diamond else | |
| "legend" if ovr >= 88 else "gold" if ovr >= 74 | |
| else "silver" if ovr >= 66 else "bronze") | |
| order = ["SHO", "PAS", "STA", "HON", "DEF", "IMP"] | |
| grid = [ | |
| ui.div( | |
| ui.span(str(card["stats"][k]), class_="n"), | |
| ui.span(_FIFA_LABELS[k] + " " + k, class_="k"), | |
| class_="st", | |
| ) | |
| for k in order | |
| ] | |
| front = ui.div( | |
| ui.div(class_="halo"), | |
| ui.div( | |
| ui.div(str(ovr), class_="ovr"), | |
| ui.div( | |
| ui.div(card["pos"], class_="pos"), | |
| (ui.div(f'NO.{card["jersey"]}', class_="jer") if card.get("jersey") else None), | |
| class_="meta", | |
| ), | |
| ui.div(ui.img(src=config.CREST_URL), class_="crest-s"), | |
| class_="top", | |
| ), | |
| ui.div(card["name"], class_="fname"), | |
| ui.div(*grid, class_="grid"), | |
| ui.div("SI DAI ER FC · 肆带贰", class_="club"), | |
| (ui.div("点击查看战绩 ↻", class_="fflip") if flip_prefix else None), | |
| class_=f"sde-fifa {tier}" + (" is-me-card" if is_me else ""), | |
| ) | |
| if not flip_prefix: | |
| return front | |
| cb_id = f"{flip_prefix}_{card['user_id']}" | |
| return ui.div( | |
| ui.tags.input(type="checkbox", id=cb_id, class_="sde-flip-cb"), | |
| ui.tags.label( | |
| ui.div( | |
| ui.div(front, class_="face front"), | |
| ui.div(_fifa_back_tag(card, tier), class_="face back"), | |
| class_="sde-flipper", | |
| ), | |
| **{"for": cb_id}, | |
| class_="sde-flip", | |
| ), | |
| class_="sde-flip-box", | |
| ) | |
| def my_fifa_card_view(card: Optional[dict[str, Any]], | |
| diamond: bool = False) -> ui.Tag: | |
| """个人页顶部的大尺寸 FIFA 卡(可翻面)。 | |
| diamond=True 时本人的卡显示为钻石皮肤(专属外观,不改数据)。 | |
| """ | |
| if not card: | |
| return ui.div() | |
| return ui.div( | |
| fifa_card_tag(card, flip_prefix="flipme", diamond=diamond), | |
| class_="sde-fifa-wrap", | |
| ) | |
| def my_totals_view(rank: Optional[dict[str, Any]]) -> ui.Tag: | |
| """我的主场:球员卡 Hero——背号水印 + 大名 + 称号徽章 + 教练寄语 + 荣誉瓦片。""" | |
| if not rank: | |
| return ui.div() | |
| n = rank["n_players"] | |
| qt, qb = _motivation(rank) | |
| tiles = [ | |
| ui.div(ui.div(str(rank["goals"]), class_="v"), ui.div("进球", class_="l"), | |
| ui.div(f'队内第 {rank["goal_rank"]}/{n}', class_="r"), class_="sde-ptile"), | |
| ui.div(ui.div(str(rank["assists"]), class_="v"), ui.div("助攻", class_="l"), | |
| ui.div(f'队内第 {rank["assist_rank"]}/{n}', class_="r"), class_="sde-ptile t-blue"), | |
| ui.div(ui.div(str(rank["ga"]), class_="v"), ui.div("参与进球", class_="l"), | |
| ui.div("进球+助攻", class_="r"), class_="sde-ptile t-cream"), | |
| ui.div(ui.div(f'{rank["attended"]}/{rank["total"]}', class_="v"), ui.div("出场", class_="l"), | |
| ui.div("出勤率 %d%%" % round(100 * rank["attended"] / rank["total"]) if rank["total"] else "—", | |
| class_="r"), class_="sde-ptile t-green"), | |
| ui.div(ui.div(str(rank.get("mvps", 0)), class_="v"), ui.div("最佳球员 MVP", class_="l"), | |
| ui.div("领队结算", class_="r"), class_="sde-ptile"), | |
| ui.div(ui.div(str(rank.get("dmvps", 0)), class_="v"), ui.div("铁血防守 MVP", class_="l"), | |
| ui.div("领队结算", class_="r"), class_="sde-ptile t-red"), | |
| ] | |
| if rank.get("is_goalkeeper"): | |
| tiles.append( | |
| ui.div(ui.div(str(rank.get("saves", 0)), class_="v"), ui.div("扑救", class_="l"), | |
| ui.div("门将", class_="r"), class_="sde-ptile t-blue") | |
| ) | |
| badges = _earned_badges(rank) | |
| return ui.div( | |
| ui.div(f'#{rank["jersey"]}' if rank.get("jersey") else "SDE", class_="wm"), | |
| ui.div("MY ARENA · 我的主场", class_="kicker"), | |
| ui.div(rank["name"], class_="nm"), | |
| ui.div( | |
| ("GOALKEEPER · 门将" if rank.get("is_goalkeeper") else "PLAYER · 球员") | |
| + (f' · NO.{rank["jersey"]}' if rank.get("jersey") else ""), | |
| class_="rl", | |
| ), | |
| (ui.div(*badges, class_="sde-badges") if badges else None), | |
| ui.div(ui.div(qt, class_="h"), ui.div(qb, class_="b"), class_="sde-quote"), | |
| ui.div(*tiles, class_="sde-ptiles"), | |
| class_="sde-pcard", | |
| ) | |
| def trend_svg(matchlog: list[dict[str, Any]]) -> ui.Tag: | |
| """手写 SVG 柱状趋势:每场两根柱(进球=金、助攻=蓝),按时间正序。无外部依赖。""" | |
| rows = list(reversed(matchlog)) # player_matchlog 是日期倒序,这里转正序 | |
| if not rows: | |
| return ui.div("还没有比赛数据,等领队录入比分后即可看趋势。", class_="sde-empty") | |
| maxv = max([1] + [r["goals"] for r in rows] + [r["assists"] for r in rows]) | |
| slot, barw, gap = 56, 15, 7 | |
| pad_l = pad_r = 10 | |
| top, bottom, h = 20, 28, 176 | |
| area = h - top - bottom | |
| y0 = top + area | |
| w = pad_l + pad_r + slot * len(rows) | |
| parts: list[str] = [ | |
| f'<line x1="{pad_l}" y1="{y0}" x2="{w - pad_r}" y2="{y0}" ' | |
| f'stroke="rgba(255,255,255,.18)" stroke-width="1"/>' | |
| ] | |
| for i, r in enumerate(rows): | |
| cx = pad_l + slot * i + slot / 2 | |
| gx, ax = cx - barw - gap / 2, cx + gap / 2 | |
| gh, ah = r["goals"] / maxv * area, r["assists"] / maxv * area | |
| parts.append( | |
| f'<rect x="{gx:.1f}" y="{y0 - gh:.1f}" width="{barw}" height="{gh:.1f}" rx="3" fill="#EBCB6B"/>' | |
| ) | |
| if r["goals"]: | |
| parts.append( | |
| f'<text x="{gx + barw / 2:.1f}" y="{y0 - gh - 4:.1f}" text-anchor="middle" ' | |
| f'font-size="10" fill="#EBCB6B">{r["goals"]}</text>' | |
| ) | |
| parts.append( | |
| f'<rect x="{ax:.1f}" y="{y0 - ah:.1f}" width="{barw}" height="{ah:.1f}" rx="3" fill="#3DA5D9"/>' | |
| ) | |
| if r["assists"]: | |
| parts.append( | |
| f'<text x="{ax + barw / 2:.1f}" y="{y0 - ah - 4:.1f}" text-anchor="middle" ' | |
| f'font-size="10" fill="#7fd0f0">{r["assists"]}</text>' | |
| ) | |
| label = r["date"][5:] if len(r["date"]) >= 10 else r["date"] | |
| parts.append( | |
| f'<text x="{cx:.1f}" y="{y0 + 16:.1f}" text-anchor="middle" ' | |
| f'font-size="10" fill="#9DB0D0">{label}</text>' | |
| ) | |
| svg = ( | |
| f'<svg viewBox="0 0 {w} {h}" preserveAspectRatio="xMinYMid meet" ' | |
| f'style="width:100%;max-width:{w}px;height:auto;overflow:visible">' | |
| + "".join(parts) | |
| + "</svg>" | |
| ) | |
| legend = ui.div( | |
| ui.span(ui.span(class_="sde-dot-g"), "进球"), | |
| ui.span(ui.span(class_="sde-dot-a"), "助攻"), | |
| class_="sde-legend", | |
| ) | |
| return ui.div(legend, ui.div(ui.HTML(svg), class_="sde-trend-wrap"), class_="sde-trend") | |
| def my_trend_view(matchlog: list[dict[str, Any]]) -> ui.Tag: | |
| return theme.card("我的进球/助攻趋势", trend_svg(matchlog), icon="📈", sub="按时间正序") | |
| def my_form_view(matchlog: list[dict[str, Any]], is_gk: bool = False) -> ui.Tag: | |
| """逐场登记:每场一张卡片,出勤/进球/助攻控件 flex 换行,手机端不溢出。 | |
| 输入框 id(att_/g_/a_)保持不变;门将额外多一个扑救栏(sv_)。 | |
| """ | |
| if not matchlog: | |
| return ui.div("还没有比赛,等领队录入比分后即可登记。", class_="sde-empty") | |
| cards = [] | |
| for m in matchlog: | |
| mid = m["id"] | |
| cards.append( | |
| ui.div( | |
| _match_top(m), | |
| ui.div( | |
| ui.div( | |
| ui.span("出勤", class_="sde-ctl-l"), | |
| ui.input_switch(f"att_{mid}", "", value=bool(m["attended"])), | |
| class_="sde-ctl", | |
| ), | |
| ui.div( | |
| ui.span("进球", class_="sde-ctl-l"), | |
| ui.input_numeric(f"g_{mid}", None, int(m["goals"]), min=0, max=99, width="80px"), | |
| class_="sde-ctl", | |
| ), | |
| ui.div( | |
| ui.span("助攻", class_="sde-ctl-l"), | |
| ui.input_numeric(f"a_{mid}", None, int(m["assists"]), min=0, max=99, width="80px"), | |
| class_="sde-ctl", | |
| ), | |
| ( | |
| ui.div( | |
| ui.span("扑救", class_="sde-ctl-l"), | |
| ui.input_numeric(f"sv_{mid}", None, int(m["saves"]), min=0, max=99, width="80px"), | |
| class_="sde-ctl", | |
| ) | |
| if is_gk | |
| else None | |
| ), | |
| class_="sde-ctl-row", | |
| ), | |
| class_="sde-mc sde-mc-form", | |
| ) | |
| ) | |
| return ui.div(*cards, class_="sde-mc-list") | |
| def my_mvp_form_view( | |
| matchlog: list[dict[str, Any]], | |
| players: list[dict[str, Any]], | |
| my_votes_map: dict[int, int], | |
| me_id: Optional[int], | |
| settled: Optional[dict[int, dict[str, Any]]] = None, | |
| ) -> ui.Tag: | |
| """每场投 MVP:下拉选队员(排除自己),预填已投票;id 为 mvp_{mid}。 | |
| 已结算的场次不再显示下拉,改显示「本场 MVP:XXX(领队已结算)」。 | |
| """ | |
| settled = settled or {} | |
| if not matchlog: | |
| return ui.div("还没有比赛,等领队录入比分后即可投票。", class_="sde-empty") | |
| choices: dict[str, str] = {"none": "— 未投 —"} | |
| for p in players: | |
| if p["id"] != me_id: | |
| choices[str(p["id"])] = _name_with_jersey(p) | |
| cards = [] | |
| for m in matchlog: | |
| mid = m["id"] | |
| if m.get("mvp_locked"): | |
| win = settled.get(mid) | |
| parts = [] | |
| if win and win.get("names"): | |
| parts.append(f'🏅 最佳球员:{"、".join(win["names"])}') | |
| if win and win.get("dmvp_name"): | |
| parts.append(f'🛡️ 铁血防守:{win["dmvp_name"]}') | |
| text = (" · ".join(parts) + "(领队已结算)") if parts else "本场 MVP 待定(投票已锁定)" | |
| ctl = ui.div(ui.span(text, class_="sde-mvp-settled"), class_="sde-ctl-row") | |
| else: | |
| sel = str(my_votes_map.get(mid, "none")) | |
| if sel not in choices: | |
| sel = "none" | |
| ctl = ui.div( | |
| ui.span("我投 MVP", class_="sde-ctl-l"), | |
| ui.input_select(f"mvp_{mid}", "", choices, selected=sel, width="190px"), | |
| class_="sde-ctl-row", | |
| ) | |
| cards.append(ui.div(_match_top(m), ctl, class_="sde-mc sde-mc-form")) | |
| return ui.div(*cards, class_="sde-mc-list") | |