v0.6.2: hero page site foot, dismiss detail on empty click, drop patches version footer.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -121,7 +121,7 @@ climperor/
|
|||||||
| `web/data/hero_abilities.json` | 英雄技能与机制汇总(由 `fetch_hero_abilities.py` 生成;含 ability `tags`) |
|
| `web/data/hero_abilities.json` | 英雄技能与机制汇总(由 `fetch_hero_abilities.py` 生成;含 ability `tags`) |
|
||||||
| `web/data/item_counter_stats.json` | OpenDota 对阵装备观测证据缓存(对阵购买率/条件胜率减同装备全局基线;可再生成;不进 recommend / relations) |
|
| `web/data/item_counter_stats.json` | OpenDota 对阵装备观测证据缓存(对阵购买率/条件胜率减同装备全局基线;可再生成;不进 recommend / relations) |
|
||||||
| `web/data/hero_item_fears.json` | 英雄怕的装备(规则推导;Web「怕」行) |
|
| `web/data/hero_item_fears.json` | 英雄怕的装备(规则推导;Web「怕」行) |
|
||||||
| `web/frontend/` | 上分帝 Web 前端静态资源(`index.html` / `config.js` / `app.js` / `style.css` / `router.js` / `mobile-gate.js` / `_redirects` / `robots.txt` / `functions/`);`config.js` 含 `SITE_VERSION`、`SITE_ORIGIN`、`ABILITY_VIDEO_BASE`、`STATIC_ASSET_BASE`;版本页底部显示 `v{SITE_VERSION}`;技能演示按官网 16:9(有空间加宽至约 720px,`contain` 不裁左右);移动端由 `mobile-gate.js` 拦截(搜索/AI 爬虫 UA 跳过) |
|
| `web/frontend/` | 上分帝 Web 前端静态资源(`index.html` / `config.js` / `app.js` / `style.css` / `router.js` / `mobile-gate.js` / `_redirects` / `robots.txt` / `functions/`);`config.js` 含 `SITE_VERSION`、`SITE_ORIGIN`、`ABILITY_VIDEO_BASE`、`STATIC_ASSET_BASE`;英雄页底部(无详情时)显示 `v{SITE_VERSION}` 与数据更新时间;技能演示按官网 16:9(有空间加宽至约 720px,`contain` 不裁左右);移动端由 `mobile-gate.js` 拦截(搜索/AI 爬虫 UA 跳过) |
|
||||||
| `web/frontend/router.js` | History 路径路由:`parseHash` / `serializeHash`(操作 pathname+search)/ `installRouter` / `syncStateToUrl`;状态↔URL 双向同步(顶层 `/heroes\|rankings\|streamers\|matches\|trends\|mechanics\|items\|patches` / 英雄 + 子标签 `skills\|core\|fears\|trends\|matchups\|matches\|streamers\|patches` / Immortal `/rankings[/region]` / 明星比赛 `/matches[/account_id][?origin=pro\|china][&page=N]` / 主播 `/streamers` / 走势 `/trends[/bracket][?sort=pr]` / 机制 `/mechanics[/{effect}]`(默认 `basic_dispel`) / 物品 / 版本 / 标签筛选 / 搜索;旧 `stats` / `/rankings/meta` 与 hash `#/...` 兼容) |
|
| `web/frontend/router.js` | History 路径路由:`parseHash` / `serializeHash`(操作 pathname+search)/ `installRouter` / `syncStateToUrl`;状态↔URL 双向同步(顶层 `/heroes\|rankings\|streamers\|matches\|trends\|mechanics\|items\|patches` / 英雄 + 子标签 `skills\|core\|fears\|trends\|matchups\|matches\|streamers\|patches` / Immortal `/rankings[/region]` / 明星比赛 `/matches[/account_id][?origin=pro\|china][&page=N]` / 主播 `/streamers` / 走势 `/trends[/bracket][?sort=pr]` / 机制 `/mechanics[/{effect}]`(默认 `basic_dispel`) / 物品 / 版本 / 标签筛选 / 搜索;旧 `stats` / `/rankings/meta` 与 hash `#/...` 兼容) |
|
||||||
| `web/assets/hero_portraits/` | 官网横版头像(上分帝 Web;默认 wide 面部构图,非匹配模板) |
|
| `web/assets/hero_portraits/` | 官网横版头像(上分帝 Web;默认 wide 面部构图,非匹配模板) |
|
||||||
| `web/assets/attr_icons/` | 官网主属性图标(力量/敏捷/智力/全才,上分帝 Web 用) |
|
| `web/assets/attr_icons/` | 官网主属性图标(力量/敏捷/智力/全才,上分帝 Web 用) |
|
||||||
|
|||||||
@@ -4,6 +4,17 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.6.2] - 2026-07-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 英雄页底部居中显示站点版本号与数据更新时间(取各数据集 `fetched_at` 最新值;打开详情时隐藏)。
|
||||||
|
- 点击定位标签上方空白(英雄网格空隙或工具栏空白)可关闭详情面板。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 版本页底部不再显示站点版本号(改由英雄页底部展示)。
|
||||||
|
|
||||||
## [0.6.1] - 2026-07-30
|
## [0.6.1] - 2026-07-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ from shared.paths import (
|
|||||||
from seo_prerender import DEFAULT_SITE_ORIGIN, write_seo_bundle
|
from seo_prerender import DEFAULT_SITE_ORIGIN, write_seo_bundle
|
||||||
from serve_relations import WEB_DIR, build_payload
|
from serve_relations import WEB_DIR, build_payload
|
||||||
|
|
||||||
SITE_VERSION = "0.6.1"
|
SITE_VERSION = "0.6.2"
|
||||||
DEFAULT_OSS_BASE = "https://climperor.oss-cn-shanghai.aliyuncs.com"
|
DEFAULT_OSS_BASE = "https://climperor.oss-cn-shanghai.aliyuncs.com"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+78
-17
@@ -76,6 +76,52 @@ function siteVersionLabel() {
|
|||||||
return v ? `v${v}` : "";
|
return v ? `v${v}` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Latest fetched_at among volatile site datasets (for hero detail footer). */
|
||||||
|
function siteDataFetchedAt() {
|
||||||
|
if (!state.data) return null;
|
||||||
|
const candidates = [];
|
||||||
|
const push = (v) => {
|
||||||
|
const ms = parseTimeMs(v);
|
||||||
|
if (Number.isFinite(ms)) candidates.push(ms);
|
||||||
|
};
|
||||||
|
push(state.data.hero_stats?.fetched_at);
|
||||||
|
push(state.data.leaderboards?.fetched_at);
|
||||||
|
push(state.data.hero_matches?.meta?.fetched_at);
|
||||||
|
push(state.data.pro_matches?.meta?.fetched_at);
|
||||||
|
push(state.data.streamers?.fetched_at);
|
||||||
|
push(state.data.stratz_hero_meta?.fetched_at);
|
||||||
|
push(state.data.stratz_matchup_tops?.fetched_at);
|
||||||
|
push(state.data.hero_items?.meta?.fetched_at);
|
||||||
|
if (!candidates.length) return null;
|
||||||
|
return new Date(Math.max(...candidates)).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHeroesSiteFoot() {
|
||||||
|
const el = $("#heroes-site-foot");
|
||||||
|
if (!el) return;
|
||||||
|
// Hide under the detail panel when a hero is selected.
|
||||||
|
const show = state.page === "heroes" && !state.selectedKey;
|
||||||
|
el.classList.toggle("hidden", !show);
|
||||||
|
if (!show) {
|
||||||
|
el.textContent = "";
|
||||||
|
el.removeAttribute("title");
|
||||||
|
el.setAttribute("aria-hidden", "true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parts = [];
|
||||||
|
const ver = siteVersionLabel();
|
||||||
|
if (ver) parts.push(ver);
|
||||||
|
const fetchedAt = siteDataFetchedAt();
|
||||||
|
const fetchedInfo = fetchedAt
|
||||||
|
? formatFriendlyTime(fetchedAt)
|
||||||
|
: { text: "", title: "" };
|
||||||
|
if (fetchedInfo.text) parts.push(`更新于 ${fetchedInfo.text}`);
|
||||||
|
el.textContent = parts.join(" · ");
|
||||||
|
if (fetchedInfo.title) el.title = fetchedInfo.title;
|
||||||
|
else el.removeAttribute("title");
|
||||||
|
el.setAttribute("aria-hidden", parts.length ? "false" : "true");
|
||||||
|
}
|
||||||
|
|
||||||
function siteOrigin() {
|
function siteOrigin() {
|
||||||
const configured = typeof SITE_ORIGIN === "string" ? SITE_ORIGIN.trim().replace(/\/+$/, "") : "";
|
const configured = typeof SITE_ORIGIN === "string" ? SITE_ORIGIN.trim().replace(/\/+$/, "") : "";
|
||||||
if (configured) return configured;
|
if (configured) return configured;
|
||||||
@@ -208,14 +254,6 @@ function clearSeoPrerender() {
|
|||||||
if (el) el.remove();
|
if (el) el.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPatchesSiteVersion() {
|
|
||||||
const el = $("#patches-site-version");
|
|
||||||
if (!el) return;
|
|
||||||
const label = siteVersionLabel();
|
|
||||||
el.textContent = label;
|
|
||||||
el.setAttribute("aria-hidden", label ? "false" : "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
data: null,
|
data: null,
|
||||||
page: "heroes", // heroes | rankings | matches | streamers | trends | mechanics | items | patches
|
page: "heroes", // heroes | rankings | matches | streamers | trends | mechanics | items | patches
|
||||||
@@ -385,6 +423,15 @@ function onHeroClick(key) {
|
|||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearHeroDetail() {
|
||||||
|
if (!state.selectedKey) return false;
|
||||||
|
state.selectedKey = null;
|
||||||
|
state.inspect = null;
|
||||||
|
syncStateToUrl();
|
||||||
|
render();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function renderTagbar() {
|
function renderTagbar() {
|
||||||
const bar = $("#tagbar");
|
const bar = $("#tagbar");
|
||||||
if (!bar) return;
|
if (!bar) return;
|
||||||
@@ -3471,7 +3518,6 @@ function renderPatches() {
|
|||||||
if (select) select.innerHTML = "";
|
if (select) select.innerHTML = "";
|
||||||
if (verEl) verEl.textContent = "";
|
if (verEl) verEl.textContent = "";
|
||||||
root.innerHTML = '<div class="patches-empty">暂无版本数据</div>';
|
root.innerHTML = '<div class="patches-empty">暂无版本数据</div>';
|
||||||
renderPatchesSiteVersion();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3506,7 +3552,6 @@ function renderPatches() {
|
|||||||
const det = details[state.selectedPatch];
|
const det = details[state.selectedPatch];
|
||||||
if (!det) {
|
if (!det) {
|
||||||
root.innerHTML = '<div class="patches-empty">该版本详情暂未收录</div>';
|
root.innerHTML = '<div class="patches-empty">该版本详情暂未收录</div>';
|
||||||
renderPatchesSiteVersion();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3539,7 +3584,6 @@ function renderPatches() {
|
|||||||
.map((h) => renderHeroEntry(h, lookup))
|
.map((h) => renderHeroEntry(h, lookup))
|
||||||
.join("");
|
.join("");
|
||||||
renderPatchSection(root, "英雄改动", heroesHtml);
|
renderPatchSection(root, "英雄改动", heroesHtml);
|
||||||
renderPatchesSiteVersion();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(s) {
|
function escapeHtml(s) {
|
||||||
@@ -5612,6 +5656,7 @@ function render() {
|
|||||||
} else {
|
} else {
|
||||||
renderPatches();
|
renderPatches();
|
||||||
}
|
}
|
||||||
|
renderHeroesSiteFoot();
|
||||||
updateDocumentMeta();
|
updateDocumentMeta();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5779,6 +5824,26 @@ function bindPageChrome() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Click empty area above role tags (board remainder / toolbar chrome) to dismiss detail. */
|
||||||
|
function bindHeroesDismiss() {
|
||||||
|
const view = $("#heroes-view");
|
||||||
|
if (view) {
|
||||||
|
view.addEventListener("click", (e) => {
|
||||||
|
if (state.page !== "heroes" || !state.selectedKey) return;
|
||||||
|
if (e.target.closest(".hero")) return;
|
||||||
|
clearHeroDetail();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const toolbar = $("#heroes-toolbar");
|
||||||
|
if (toolbar) {
|
||||||
|
toolbar.addEventListener("click", (e) => {
|
||||||
|
if (state.page !== "heroes" || !state.selectedKey) return;
|
||||||
|
if (e.target.closest("button, a, input, select, textarea, label")) return;
|
||||||
|
clearHeroDetail();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// mobile-gate.js sets this before paint; skip desktop data boot on phones/tablets.
|
// mobile-gate.js sets this before paint; skip desktop data boot on phones/tablets.
|
||||||
if (typeof window !== "undefined" && window.__CLIMPEROR_MOBILE__) return;
|
if (typeof window !== "undefined" && window.__CLIMPEROR_MOBILE__) return;
|
||||||
@@ -5796,12 +5861,7 @@ async function main() {
|
|||||||
renderBoard();
|
renderBoard();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.selectedKey) {
|
clearHeroDetail();
|
||||||
state.selectedKey = null;
|
|
||||||
state.inspect = null;
|
|
||||||
syncStateToUrl();
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.itemQuery && document.activeElement === $("#q-item")) {
|
if (state.itemQuery && document.activeElement === $("#q-item")) {
|
||||||
@@ -5839,6 +5899,7 @@ async function main() {
|
|||||||
);
|
);
|
||||||
bindSearch();
|
bindSearch();
|
||||||
bindPageChrome();
|
bindPageChrome();
|
||||||
|
bindHeroesDismiss();
|
||||||
const brandLogo = document.querySelector(".brand-logo");
|
const brandLogo = document.querySelector(".brand-logo");
|
||||||
if (brandLogo) brandLogo.src = assetUrl("/ui-icon/dota2_logo_wordmark.png");
|
if (brandLogo) brandLogo.src = assetUrl("/ui-icon/dota2_logo_wordmark.png");
|
||||||
const favicon = document.querySelector('link[rel="icon"]');
|
const favicon = document.querySelector('link[rel="icon"]');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* Local defaults; production export overwrites via export_relations_site.py. */
|
/* Local defaults; production export overwrites via export_relations_site.py. */
|
||||||
var SITE_VERSION = "0.6.1";
|
var SITE_VERSION = "0.6.2";
|
||||||
var SITE_ORIGIN = "";
|
var SITE_ORIGIN = "";
|
||||||
var ABILITY_VIDEO_BASE = "";
|
var ABILITY_VIDEO_BASE = "";
|
||||||
var STATIC_ASSET_BASE = "";
|
var STATIC_ASSET_BASE = "";
|
||||||
|
|||||||
@@ -43,8 +43,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<link rel="icon" href="/ui-icon/dota2_logo.png" type="image/png" />
|
<link rel="icon" href="/ui-icon/dota2_logo.png" type="image/png" />
|
||||||
<link rel="stylesheet" href="/style.css?v=0.6.1" />
|
<link rel="stylesheet" href="/style.css?v=0.6.2" />
|
||||||
<script src="/mobile-gate.js?v=0.6.1"></script>
|
<script src="/mobile-gate.js?v=0.6.2"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1 class="sr-only">DOTA2 上分帝</h1>
|
<h1 class="sr-only">DOTA2 上分帝</h1>
|
||||||
@@ -227,13 +227,13 @@
|
|||||||
<select id="patch-select" aria-label="选择版本"></select>
|
<select id="patch-select" aria-label="选择版本"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="patches-detail" id="patches-detail"></div>
|
<div class="patches-detail" id="patches-detail"></div>
|
||||||
<footer class="patches-site-version" id="patches-site-version" aria-hidden="true"></footer>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<section class="detail" id="detail" aria-live="polite"></section>
|
<section class="detail" id="detail" aria-live="polite"></section>
|
||||||
|
<footer class="heroes-site-foot" id="heroes-site-foot" aria-hidden="true"></footer>
|
||||||
|
|
||||||
<script src="/config.js?v=0.6.1"></script>
|
<script src="/config.js?v=0.6.2"></script>
|
||||||
<script src="/router.js?v=0.6.1"></script>
|
<script src="/router.js?v=0.6.2"></script>
|
||||||
<script src="/app.js?v=0.6.1"></script>
|
<script src="/app.js?v=0.6.2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+12
-12
@@ -493,6 +493,18 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
.heroes-site-foot {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 14px 10px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: var(--fw-medium);
|
||||||
|
color: var(--muted);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
opacity: 0.55;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
.detail:not(.empty) {
|
.detail:not(.empty) {
|
||||||
height: var(--detail-fixed-h);
|
height: var(--detail-fixed-h);
|
||||||
min-height: var(--detail-fixed-h);
|
min-height: var(--detail-fixed-h);
|
||||||
@@ -2808,18 +2820,6 @@ body:has(#items-view:not(.hidden)) {
|
|||||||
font-size: var(--fs-body);
|
font-size: var(--fs-body);
|
||||||
line-height: var(--lh-body);
|
line-height: var(--lh-body);
|
||||||
}
|
}
|
||||||
.patches-site-version {
|
|
||||||
width: min(var(--content-read), 92%);
|
|
||||||
padding: 4px 0 28px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: var(--fw-medium);
|
|
||||||
color: var(--muted);
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
opacity: 0.55;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rankings-board {
|
.rankings-board {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
Reference in New Issue
Block a user