v0.5.109: hero role toolbar, skill side-by-side layout, taller detail panel.

Move role filters between grid and detail, fix skill video/text layout and detail height jump, unify item dividers, drop OpenDota matchup cross, sync docs and assets.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-30 00:48:07 +08:00
co-authored by Cursor
parent 09f0a0b4a0
commit d6212169af
38 changed files with 7225 additions and 6924 deletions
+301 -229
View File
@@ -1,269 +1,341 @@
# 上分帝(Climperor)—— 方案与实施细节
`README.md` 是操作手册;本文记录背景、选型与关键决策。协作约束见 `AGENTS.md`
---
version: alpha
name: Climperor Web
description: Dark Dota-inspired information panels for the Climperor Web desktop site.
colors:
primary: "#5EC8FF"
secondary: "#8FA3BC"
tertiary: "#3DCE7A"
neutral: "#0B1018"
surface: "#0E1622"
surface-raised: "#101824"
on-surface: "#E8EEF7"
on-surface-muted: "#8FA3BC"
border: "rgba(140, 170, 210, 0.18)"
accent: "#5EC8FF"
good: "#3DCE7A"
bad: "#E07A2F"
syn: "#7EC8FF"
danger: "#E86A6A"
gold: "#E8C878"
live: "#FE2C55"
panel: "rgba(14, 22, 34, 0.96)"
panel-soft: "rgba(8, 12, 20, 0.55)"
on-accent: "#0B0D10"
typography:
display:
fontFamily: "Segoe UI, Microsoft YaHei UI, PingFang SC, sans-serif"
fontSize: 32px
fontWeight: 800
lineHeight: 1.1
letterSpacing: 0.02em
page-title:
fontFamily: "Segoe UI, Microsoft YaHei UI, PingFang SC, sans-serif"
fontSize: 22px
fontWeight: 800
lineHeight: 1.2
letterSpacing: 0.02em
title-lg:
fontFamily: "Segoe UI, Microsoft YaHei UI, PingFang SC, sans-serif"
fontSize: 18px
fontWeight: 700
lineHeight: 1.3
letterSpacing: 0.04em
title-md:
fontFamily: "Segoe UI, Microsoft YaHei UI, PingFang SC, sans-serif"
fontSize: 16px
fontWeight: 700
lineHeight: 1.35
letterSpacing: 0.02em
body-lg:
fontFamily: "Segoe UI, Microsoft YaHei UI, PingFang SC, sans-serif"
fontSize: 15px
fontWeight: 400
lineHeight: 1.5
body-md:
fontFamily: "Segoe UI, Microsoft YaHei UI, PingFang SC, sans-serif"
fontSize: 14px
fontWeight: 400
lineHeight: 1.5
body-sm:
fontFamily: "Segoe UI, Microsoft YaHei UI, PingFang SC, sans-serif"
fontSize: 13px
fontWeight: 500
lineHeight: 1.45
label-md:
fontFamily: "Segoe UI, Microsoft YaHei UI, PingFang SC, sans-serif"
fontSize: 13px
fontWeight: 600
lineHeight: 1.3
letterSpacing: 0.06em
label-sm:
fontFamily: "Segoe UI, Microsoft YaHei UI, PingFang SC, sans-serif"
fontSize: 12px
fontWeight: 600
lineHeight: 1.3
letterSpacing: 0.04em
numeric-lg:
fontFamily: "Segoe UI, Microsoft YaHei UI, PingFang SC, sans-serif"
fontSize: 28px
fontWeight: 700
lineHeight: 1.1
letterSpacing: 0.02em
numeric-md:
fontFamily: "Segoe UI, Microsoft YaHei UI, PingFang SC, sans-serif"
fontSize: 14px
fontWeight: 700
lineHeight: 1.2
rounded:
sm: 4px
md: 8px
lg: 12px
xl: 16px
full: 9999px
spacing:
xs: 4px
sm: 8px
md: 12px
lg: 16px
xl: 24px
2xl: 32px
3xl: 48px
gutter: 16px
page-y: 28px
content-read: 820px
content-standard: 880px
content-wide: 1080px
content-data: 1680px
components:
button-primary:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-accent}"
rounded: "{rounded.md}"
padding: 12px
height: 40px
button-primary-hover:
backgroundColor: "#7AD4FF"
button-secondary:
backgroundColor: "{colors.surface}"
textColor: "{colors.on-surface}"
rounded: "{rounded.md}"
padding: 12px
height: 40px
button-secondary-active:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-accent}"
tab-main:
backgroundColor: "{colors.surface}"
textColor: "{colors.secondary}"
typography: "{typography.body-lg}"
height: 42px
padding: 10px
tab-main-active:
backgroundColor: "{colors.surface-raised}"
textColor: "{colors.on-surface}"
input-search:
backgroundColor: "{colors.neutral}"
textColor: "{colors.on-surface}"
rounded: "{rounded.md}"
height: 40px
padding: 12px
panel-card:
backgroundColor: "{colors.surface}"
textColor: "{colors.on-surface}"
rounded: "{rounded.lg}"
padding: 16px
chip-filter:
backgroundColor: "{colors.surface}"
textColor: "{colors.secondary}"
rounded: "{rounded.full}"
padding: 8px
chip-filter-active:
backgroundColor: "{colors.surface-raised}"
textColor: "{colors.on-surface}"
mark-good:
backgroundColor: "{colors.neutral}"
textColor: "{colors.good}"
mark-bad:
backgroundColor: "{colors.neutral}"
textColor: "{colors.bad}"
mark-syn:
backgroundColor: "{colors.neutral}"
textColor: "{colors.syn}"
badge-gold:
backgroundColor: "{colors.neutral}"
textColor: "{colors.gold}"
badge-live:
backgroundColor: "{colors.live}"
textColor: "#FFFFFF"
badge-danger:
backgroundColor: "{colors.neutral}"
textColor: "{colors.danger}"
panel-raised:
backgroundColor: "{colors.panel}"
textColor: "{colors.on-surface}"
panel-soft:
backgroundColor: "{colors.panel-soft}"
textColor: "{colors.on-surface-muted}"
divider:
backgroundColor: "{colors.surface}"
textColor: "{colors.border}"
---
## 1. 背景与目标
# Climperor Web Design System
### 要解决的问题
## Overview
在 Dota 2 选将阶段(以及进入游戏后)**几秒内自动获取双方 10 个英雄**,服务天梯选将辅助或赛后分析
上分帝 Web 是面向桌面浏览器的 Dota 2 信息站:深色蓝黑底、高信息密度、冷静可读。气质接近官方客户端与选人界面——偏工具面板,不走社交 feed 或明亮营销站
### 为什么 GSI 做不到
目标感受:
| 场景 | GSI 可获得的阵容数据 |
|------|---------------------|
| 排位 / 普通 All Pick | 仅自己的 `hero.id``draft` 通常为空 |
| Captains Mode | 历史上有部分 pick/ban,不稳定 |
| 观战 / 裁判视角 | 阵容字段较全 |
| 赛后 | 需依赖 OpenDota 等外部 API |
- **Dense but calm**:列表、表格、网格可挤满视口,但字号、行高、间距仍保持清晰节奏。
- **Semantic color first**:青色强调当前选择与导航;绿 / 橙 / 青蓝分别表示克制 / 被克 / 搭档;粉红仅用于直播态。
- **Desktop shell**`100dvh` 外框 + 页内滚动;手机 / 平板由 `mobile-gate` 拦截,不为真移动端另做一套 IA。
Valve 已明确关闭普通玩家视角的实时 draft
[#9562](https://github.com/ValveSoftware/Dota2-Gameplay/issues/9562)、
[#7193](https://github.com/ValveSoftware/Dota2-Gameplay/issues/7193)not planned)。
实现源:`web/frontend/style.css` 的 CSS 变量必须与本文件令牌一一对应。工程决策见 `ARCHITECTURE.md`
### 候选方案
## Colors
| 方案 | 结论 |
|------|------|
| 官方 GSI | 拿不到双方 pick |
| Overwolf GEP | 备选,偏重 |
| 读游戏内存 | **排除**(合规) |
| 截屏 + 模板匹配 | **选定** |
| 整图问多模态大模型 | 排除为主路径(实测整图几乎全错;裁顶栏后仍不如白名单匹配) |
调色盘以近黑海军为底,浅冷灰为正文,单一青色作交互主色。
社区同类工具共同点:**裁固定 ROI + 模板匹配 / 小模型**,输出约束在英雄白名单内
- **Primary (`#5EC8FF`)**:主交互与选中态(主 Tab 底线、侧栏选中填充、焦点环、选中英雄描边)
- **Secondary (`#8FA3BC`)**:次要文案、标签、时间戳、未选中控件。
- **Tertiary (`#3DCE7A`)**:正向语义(克制、胜率上升、胜场)。
- **Neutral (`#0B1018`)**:页面底色;表面层用更深的半透明面板叠在渐变背景上。
- **Bad / Danger / Gold / Live / Syn**:业务辨识色,不得拿来当通用强调色。
---
对比度要求:正文与底至少 WCAG AA;muted 文案仅用于辅助信息,不用在关键操作标签上。
## 2. 技术方案
## Typography
### 处理流程
系统栈:`Segoe UI` + `Microsoft YaHei UI` + `PingFang SC`。中文优先清晰,不引入展示性装饰字体。
```
截图 → 相对坐标裁 10 格 →(天梯则遮罩段位条)→ CDN NCC 匹配
→ score + margin 双门控 → hero_key | null
```
| Token | 用途 |
|-------|------|
| `display` | 版本号等少数展示数字(约 32px,勿再放大到 40px) |
| `page-title` | 各顶层页标题(排行 / 走势 / 机制 / 主播 / 物品等) |
| `title-lg` / `title-md` | 区段标题、英雄名、物品名 |
| `body-lg` / `body-md` / `body-sm` | 导航、正文、说明 |
| `label-md` / `label-sm` | 表头、分区标签、徽章 |
| `numeric-*` | 表格数字与指标卡,配合 `tabular-nums` |
### 关键决策
字重只用 400 / 500 / 600 / 700 / 800。禁止 `650``13.5px` 等孤立值。默认行高正文 1.5,标题 1.1–1.3;勿留 `line-height: normal` 在可读正文上。
**① 相对坐标**
## Layout
横向 `(cx - W/2) / H`,纵向与宽高 `/ H`。同宽高比下分辨率无关;21:9 等需锚点或重标定。
桌面壳:顶栏固定高度区 + 可滚动 `main.board`;英雄页的英雄网格与固定高 `#detail`
之间放置一行定位筛选。选中英雄后 `#detail` 使用共享固定高度(约 `min(640px, …)`),
Tab 切换不跳动;技能演示与文案左右排布,长 Tab(走势/对位等)在详情区内滚动。
**② 单一 CDN 模板库**
### 顶部壳(紧凑单层)
`templates/cdn/{hero}.png` 来自 Steam 官方头像,按顶栏实际窗口裁切后写入。
顶栏只保留主行,禁止品牌 / 主导航 / 搜索三者各自「漂浮居中」造成大块空洞:
曾尝试「CDN 兜底 + real 实拍」双层库。实测修正 CDN 裁切后 **CDN-only 即可全对**
继续攒 real 库性价比低,已放弃(见 §3 发现二)。皮肤靠会话策略,不堆变体模板。
1. **首行 `PrimaryRow`**`display: grid; grid-template-columns: 1fr auto 1fr`(或等价 flex)。
- 左:品牌(logo + 标题)
- 中:主导航分段控件(真正几何居中)
- 右:工具区(英雄/物品搜索 + 邮件入口),`justify-content: end`
视觉语言轻度借鉴 Dota 2 国服官网导航,但不复刻门户式大顶栏:
**③ 宁可不认,不可乱认**
- 顶栏使用近黑金属渐变与上下 1px 冷色高光,形成连续横梁;总高控制在约 60px。
- 品牌区仅用克制的暗红内光形成视觉锚点,不在 Logo 右侧增加分隔线;不使用视频 Logo、金色奖项条或厚重贴图。
- 主导航保持现代无边框文字 Tab;相邻 Tab 仅用低对比斜切细线分隔,不做梯形按钮。
- 主交互色仍为 `primary` 青色;Dota 红只用于品牌区,不替代全站选中态。
- 宽屏主行采用 44px 控件高:主导航 16px / 最小 76px 宽,品牌 Logo 32px。`lg` 断点恢复 40px、15px 的紧凑规格。
- `score >= min_score`(默认 0.45
- `margin = Top1 Top2 >= min_margin`(默认 0.04
断点降级:
**④ 会话层处理皮肤与淡入**
- `lg` ≤1200px:Tab 水平内边距收紧;搜索宽约 160–180px
- `md` ≤900px:隐藏品牌标题,仅留 logo;搜索再缩短
- `sm` ≤640px:极窄桌面可隐藏整块品牌
- 选人阶段:多帧确认、允许更高分改判(`revise_gain`
- 决策阶段:只补空槽、禁止改判(避免皮肤顶栏覆盖默认脸结论)
- best 帧优先选人阶段默认脸(`recognized` 相同时)
- 本人英雄以 GSI `hero` 覆盖视觉结果(只覆盖自己的槽)
内容宽度档位(命名,勿再散落魔法数):
**⑤ 截图按对局归档**
GSI 会话写入 `samples/raw/{matchid}/`;手动 `capture.py` 仍写 `samples/raw/` 根目录。
结果 JSON 仍为 `results/draft_<时间戳>.json`(内含 `match_id`)。
**⑥ GSI 全量落盘**
`gsi.dump_payloads`(默认开)把每包 POST body 追加到 `samples/raw/{matchid}/gsi.jsonl`
行格式 `{"t": <unix>, "payload": <原文>}`。内容上限仍是 cfg 订阅字段 + 普通玩家视角;
不能靠落盘补出双方 pick。CLI`--dump-gsi` / `--no-dump-gsi`
---
## 3. 关键实测(2026-07-25
环境:2560×1440 无边框;人机 + 天梯。
### 发现一:CDN 裁切窗口决定上限
初版按正方形取中心,比例与顶栏 111×88 不符,均分约 0.65。
网格搜索得到正确窗口 **x0=38, w=182, 全高**(源 256×144),均分约 0.94。
写入 `fetch_cdn_templates.py``CROP_X0/CROP_X1` 后,**CDN-only 30/30 / 40/40**。
> 兜底层差时先查素材处理,再考虑堆数据。
### 发现二:不必维护 real 库
CDN-only 开箱即用后,「打几十局攒默认脸」不再是主路径。项目改为只维护 CDN。
后续审计(迁出前原型库)进一步确认:
- 标注集上大量 `score=1.000` 往往是「从同帧裁进 real 再评同帧」的自证,夸大了 real 贡献。
- 剩余 real 与 CDN(天梯遮罩)多数 ≥0.85,近乎 CDN 副本;明显偏离的极少(如军团指挥官一类边缘格)。
- **近期问题局**(宙斯缺槽、斯拉达 CDN 分低、敌法末位、皮肤帧风行者 0.415)主要靠
**多帧会话 + 决策禁改判 + GSI 补自己** 凑齐,不是靠 real。
- 皮肤变体极多,入库皮肤模板不合理;段位条/徽章每人不同,匹配时已用遮罩忽略,
也不该为不同段位重复存图。
### 发现三:自动标定靠玩家颜色条
10 条固定玩家色定位槽位;中心用中位数槽距拟合(剔除被身后头像污染的宽条);
头像下沿用「格内列 vs 格间空隙」亮度差,勿用逐行差分(易误判到名字行)。
### 发现四:天梯段位条用遮罩
GSI 不含可靠 lobby 类型时,用右下金色勋章检测(`has_ranked_overlay`),
匹配时屏蔽底部 32% + 右侧 22%。人机无勋章走全图匹配。
### 发现五:禁用名单不靠模板
网格按主属性分块 + 客户端本地化名行优先排布;禁用/已选卡片对比度塌陷
std 约 821 vs 正常 ≥33)。`data/heroes.json` 需含 `attr` / `name_loc`
另含手工维护的中文口语 `aliases`(重拉 CDN 表时合并保留,暂未接入推荐展示)。
格数对不上时 `ok=False`,不返回残缺名单。
### 发现六:位置与「我」
定位局位置字用二值 IoU(非 OCR)。「我」优先 GSI `team_slot`
否则姓名亮度相对差(最亮比次亮 ≥ 25)兜底。
### 发现七:客户端顶栏时机(会话策略前提)
这些是实战纠正后的事实,改 `DraftSession` 时不要违背:
| 现象 | 含义 |
|------|------|
| 顶栏默认脸 → 皮肤 | **全员选完后**才换皮肤立绘,不是一进决策就换 |
| 本机已进 `STRATEGY_TIME` | 别人可能还在选;**不能**一进决策就永久停视觉 |
| 空槽红旗倒计时 | 只出现在**未选**槽;揭晓后消失,不是挡脸主因 |
| GSI | **没有**「皮肤已加载」字段;无法精确卡「人选完、皮肤前」单帧 |
因此正确节奏是:选人 + 决策前期持续读,直到确认 10/10 或 `strategy_tail_polls` 用尽;
凑齐后停视觉,再短等 GSI 补本人英雄。单帧皮肤弱识别(如决策 3D 模型挡顶栏)属预期,
靠会话回填,不靠降阈值或堆皮肤模板。
### 发现八:揭晓淡入需要可改判
首帧揭晓常半透明 + 段位条,易误认(例:剑圣揭晓瞬间被认成幽鬼)。
`confirm_polls` 后若仍禁止改判会锁死错误。选人阶段允许
`score ≥ was_score + revise_gain` 的稳定新结果覆盖;决策阶段则关闭改判。
---
## 4. 代码结构
```
climperor/
├── pc/ # 局内选将识别(GSI + 截屏 + OpenCV
│ ├── common.py / recognize.py / draft_session.py / gsi_watch.py
│ ├── recommend.py / draft_archetypes.py / roles.py / modes.py / overlay.py
│ ├── config.json / templates/ / assets/role_icons/ / samples/
│ └── requirements.txt
├── web/ # 上分帝 Web(前端、数据流水线、部署)
│ ├── frontend/ # 原 web/relations/
│ ├── data/ / assets/ / dist/
│ ├── fetch_*.py / item_fears.py / mechanic_tags.py / loc_format.py
│ ├── serve_relations.py / export_relations_site.py / deploy_relations.py
│ ├── refresh_web.py / notify_site_traffic.py / _cf_* / _oss_* / _gitea_*
│ └── requirements.txt
├── shared/ # 只被 pc/web 依赖,绝不反向依赖
│ ├── paths.py / grid.py / relations.py / hero_tags.py / http_utils.py
│ ├── import_relations_xlsx.py / audit_relations.py / fetch_matchups.py / fetch_stratz.py
│ └── data/heroes.json / data/relations.json
├── .gitea/workflows/ # site-traffic-notify + web-daily/weekly/patch
└── README.md / AGENTS.md / DESIGN.md / CHANGELOG.md
```
运行时目录 `pc/preview/``pc/results/``pc/failures/``pc/samples/raw/`(含按对局子目录)不入库。上分帝 Web 易变数据(STRATZ meta、hero_stats 等)可由 Gitea Actions 定时拉取后直部 Pages,不必入库。
### 主要配置
| 字段 | 含义 | 默认 |
| 档位 | 宽度 | 页面 |
|------|------|------|
| `match.min_score` / `min_margin` | 识别门控 | 0.45 / 0.04 |
| `match.ranked_mask` | 天梯遮罩比例 | bottom 0.32, right 0.22 |
| `gsi.port` | 监听端口 | 3223 |
| `gsi.confirm_polls` | 连续同结果帧数 | 2 |
| `gsi.revise_gain` | 选人阶段改判所需分数增益 | 0.15 |
| `gsi.strategy_tail_polls` | 决策阶段继续视觉轮询 | 8 |
| `gsi.strategy_gsi_wait` | 视觉结束后等待本人 GSI 英雄 | 3.0 |
| `gsi.require_foreground` | 仅当前台为 `dota2.exe` 时截屏识别 | true |
| `gsi.dump_payloads` | 全量 GSI JSONL 落盘 | true |
| `recommend.enabled` | 选将全网格克/搭/补推荐 | true |
| `recommend.top_n` | 标记数量上限;`≤0` 不截断 | 0 |
| `recommend.min_enemies` | 开始推荐所需敌方锁人数 | 1 |
| `recommend.min_heroes_for_gaps` | 缺口/补位注入所需锁人数 | 2 |
| `recommend.archetypes` | 推进/全球流/缺口规则画像 | true |
| `recommend.relations_path` | 定性关系文件 | `shared/data/relations.json` |
| `recommend.role_tags` | 分路→角色标签过滤(1–5 号位;非定位局不过滤) | 见 config.json |
| `overlay.enabled` | 选将顶栏角色标签悬浮层 | true |
| `overlay.y_gap_rel` / `icon_h_rel` / `icon_gap_rel` | 标签相对屏幕高的间距与尺寸 | 0.008 / 0.016 / 0.002 |
| `overlay.mark_size_rel` / `mark_pad_rel` / `mark_gap_rel` | 网格克/搭/补方标尺寸、内边距、间距 | 0.018 / 0.004 / 0.002 |
| `overlay.counter_color` / `synergy_color` / `fill_color` / `mark_text_color` | 克 / 搭 / 补 / 文字色 | `#2ec4b6` / `#e9a825` / `#9b7ebd` / `#0b1220` |
| `overlay.analysis_*` | 阵容分析横条位置/高度/字号/底色字色 | 见 config.json |
| full | 100% | 英雄网格 |
| read | `min(820px, 92%)` | 版本 |
| standard | `min(880px, calc(100% - 48px))` | 排行、主播、机制 |
| wide | `min(1080px, …)` | 走势 |
| data | `min(1680px, …)` | 比赛 |
---
间距尺度:4 / 8 / 12 / 16 / 24 / 32 / 48px。页内 gutter 默认 16px;列表页上下 `page-y` 28px。侧栏与主列间距 20px。
## 5. GSI 自动化
断点收敛为三档:
```
Dota 2 (-gamestateintegration)
→ POST → pc/gsi_watch.py :3223
→ pc/samples/raw/{matchid}/gsi.jsonl(可选)
→ DraftSession 轮询 recognize + roles/grid/modes
→ pc/results/draft_*.json
```
- `sm` ≤640px:极窄桌面(列堆叠、双列网格)
- `md` ≤900px:aside 改上下堆叠、英雄两列
- `lg` ≤1200px:物品详情上移、列间距收紧
要点:按 `matchid` 去重;截图进 `pc/samples/raw/{matchid}/`;识别在工作线程;
未标定时降级为只截图。
手机不走上述断点,由 UA 门禁接管。
天梯 AP 选人按 2/2/1 成批揭晓,本轮结束前互不可见——因此必须在
**HERO_SELECTION** 跟踪,不能只在决策时间截一张终局图;决策前期仍读几帧
以吃到最后一人揭晓(见发现七)。
## Elevation & Depth
---
深度靠色调分层与细边框,不用厚重投影:
## 6. 选将全网格「克 / 搭 / 补」推荐
1. 页面渐变底
2. 半透明顶栏 / 面板(`panel-soft`
3. 实心一点的浮层(`panel`、物品 sticky 详情、天赋 popover
- **触发**:敌方至少锁定 1 人(`min_enemies`)后开始;本人锁人后清空标记
- **分路**:定位匹配画面字(`roles.py`+ GSI `team_slot` 定位本人;不做截屏认自己。有分路则按 `role_tags` 过滤候选;非定位局(读不到分路字)则全英雄表。
- **关系模型**:机制克制/搭档(`shared/data/relations.json`),不用胜率/场次——版本会变,机制边相对稳。Web 英雄详情「走势」Tab 优先 STRATZ(`web/data/stratz_hero_meta.json`):勋章条切换 8 档 → **最近 1 周**三卡(胜率/上场率/场次)→ 1~5 号位分路胜率 → **近 8 周**逐周列表(日期、胜率、周环比、区间缩放横条、场次;新→旧);OpenDota(`web/data/hero_stats.json`,近约 7 天)作兜底。「对位」Tab 展示 STRATZ 数值 Top`web/data/stratz_matchup_tops.json`),与网格定性「克/怕/搭」分开展示;均不进局内推荐。
- **阵容画像(规则,不接 AI)**:[`pc/draft_archetypes.py`](pc/draft_archetypes.py) 识别敌方推进/全球流、敌我 tag 缺口(缺控制/爆发/输出/先手);输出一句 `analysis`(如 `敌:缺控制·偏推进 | 我:缺爆发`)与每英雄短 `reasons`
- **纳入**
- **克**:关系克制边;推进/全球流应对表(如美杜莎对推进);惩戒敌方缺口
- **搭**:与已锁己有搭档边
- **补**:填我方缺口(`min_heroes_for_gaps` 默认 2 才注入缺口类标记)
- **打分**:关系边为主;阵容应对 / 缺口小幅加分;敌方 tags 软加分仅排序。
- **展示**:网格左上角全量相关格标「克」(青)/「搭」(琥珀)/「补」(紫灰);叠加层横条显示阵容分析;原因进日志与 `results` JSON,不写在每个格子旁。
- **上分帝 Web**`web/serve_relations.py` 本地开发服务;仿选将网格查看克制/被克制/搭档;数据手改 `shared/data/relations.json` 或从 xlsx 导入。
另有顶级 **排行**、**走势**`#/trends[/bracket]`,近 8 周高胜率/上场率榜)、**物品**、**版本** 页;目录见 `web/data/item_shop.json` / `web/data/patches.json` / `web/data/leaderboards.json`
英雄详情子标签含 **走势**(见上;`#/heroes/<key>/trends`)、**对位**(克制/被克/搭档三列 STRATZ Top`#/heroes/<key>/matchups`)、**近期比赛**`#/heroes/<key>/matches`)。排行页为 Immortal 四区选手榜。
- **Web Hash 路由**:状态(顶层标签 / 选中英雄 + 详情子标签 / 选中物品 / 选中版本 /
标签筛选 / 搜索)双向同步进 URL(`#/heroes/axe/core?tags=核心&q=axe``#/heroes/axe/trends` 等)。选 Hash
而非 Path 路由:静态导出(GitHub Pages)无需 rewrite、`web/serve_relations.py` 零改动、
相对路径子站亦可用。写 URL 用 `history.pushState`/`replaceState`(静默,不触发
`hashchange`,无回环);前进/后退靠 `hashchange` 监听重 parse + 校验 + render。
搜索 debounce 300ms + `replaceState` 防刷历史栈;坏链接丢弃该项不崩。路由逻辑集中在
`web/frontend/router.js``app.js` 只在 `main()``installRouter` + 各 state 变更点
`syncStateToUrl`
- **不做**:避用标、Steam 登录页、爬 Dotabuff;关系**不**写入 `shared/data/heroes.json`;不接实时 AI。
选中态用 inset 底线、填充主色或 1–2px `accent` 描边;阴影仅用于浮层(popover / 气泡),模糊约 2432px、黑色 45% 透明度
---
## Shapes
## 7. 与 dota2-hex
圆角语言偏克制:
独立验证项目。可选:Rust 重写并入,或本地旁路 HTTP 回传。
合规同 `dota2-hex`**屏幕可见信息 only**。
- `sm` 4px:表格内图标、小徽章
- `md` 8px:输入框、多数按钮、指标卡
- `lg` 12px:详情面板、机制卡、主内容卡
- `xl` 16px:仅大卡片(主播卡)需要时使用,勿扩散到所有面板
- `full`:筛选 pill、关注按钮
---
禁止同一视图混用锐角与大圆角而无语义理由。
## 8. 参考
## Components
- [Valve #9562](https://github.com/ValveSoftware/Dota2-Gameplay/issues/9562)
- [Valve #14915](https://github.com/ValveSoftware/Dota2-Gameplay/issues/14915)(开发者 OCR ≈85%
- [Overwolf Dota 2 GEP](https://dev.overwolf.com/ow-native/live-game-data-gep/supported-games/dota-2/)
- Steam CDN`https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/heroes/{key}.png`
### 导航
- **Main tabs**:无边框文字 Tab,透明背景;相邻项以低对比斜切细线分隔。未选 muted,hover 仅提亮文字,禁止施加矩形 / 渐变底以免与斜切线冲突;选中使用高对比文字 + 居中 44px、2px 高的 `primary` 下划线。宽屏点击区高 44px、字号 16px、最小宽 76px,与搜索 / 邮件对齐;`lg` 断点收敛至 40px / 15px。禁止重新包回连续按钮外框或梯形按钮底。
- **Detail tabs**:下划线式,字号 `body-md`
- **Aside filters**(地区 / 机制效果):竖向 filled;选中 `primary` 底 + `on-accent` 字。
- **顶栏邮件**40×40 图标按钮,muted;hover 提亮。放在右侧工具区,不绝对悬浮到视口角。
### 英雄定位筛选
位于英雄网格和详情面板之间,使用 Valve 选人筛选符号与中文标签的紧凑斜角按钮。
「全部」用中性网格符号;核心 / 辅助 / 推进 / 逃生 / 爆发 / 先手 / 控制 / 耐久使用对应
Valve 图标。支持多选筛选(`active`);选中英雄时,其定位以同色选中态(`matched`
高亮,不单独在详情区再放 Chip。当前不展示「幻象」筛选,勿与主导航混排。
### 搜索
高 40px,圆角 `md`,左侧放大镜;focus 时边框与光晕用 `primary`。默认宽约 200px,落在首行右侧工具区(非 absolute 贴边);物品页换物品搜索,其他页隐藏搜索。
### 面板 / 卡片
共享 `.ui-panel`:边框 `border`、背景 `panel-soft`、圆角 `lg`、内边距 16px。空态统一 `.ui-empty`:居中 muted、`body-md`、上下 48px。
### 表格
表头 `label-sm` muted;单元格 `body-md`;数字 `tabular-nums`。行 hover 轻微提亮,不加斑马纹。
### 焦点
所有可交互控件:`outline: 2px solid primary; outline-offset: 2px``.ui-focus`)。最小点击高度 3640px。
### 业务标记(不可通用化)
- 克 / 怕 / 搭 → `good` / `bad` / `syn`
- 直播环与关注按钮 → `live`
- 装备金价 → `gold`
## Do's and Don'ts
- Do 改视觉前先更新本文件令牌,再改 `style.css` 变量与引用。
- Do 用命名宽度档位与三档断点;Don't 再加孤立 `max-width: Npx`
- Do 每个顶层页提供清晰的页标题语义(可见 `h1`/`h2` 或等价 `.page-title`)。
- Don't 用 `font-weight: 650` 或非尺度字号(如 `13.5px`)。
- Don't 把直播粉、胜负红、金价黄当作通用 CTA。
- Don't 为真移动端在现有碎片 `@media` 上硬撑;门禁解除前保持桌面壳。
- Do 保持英雄底栏固定高与列表页整板滚动两种壳模型,勿发明第三种。
- Don't 在 data-only 刷新里 bump `SITE_VERSION`;视觉发版才同步 config / export / `?v=` 缓存戳。