diff --git a/AGENTS.md b/AGENTS.md index 65a5986..9c81b00 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -121,7 +121,7 @@ climperor/ | `web/data/hero_abilities.json` | 英雄技能与机制汇总(由 `fetch_hero_abilities.py` 生成;含 ability `tags`) | | `web/data/item_counter_stats.json` | OpenDota 对阵装备观测证据缓存(对阵购买率/条件胜率减同装备全局基线;可再生成;不进 recommend / relations) | | `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/assets/hero_portraits/` | 官网横版头像(上分帝 Web;默认 wide 面部构图,非匹配模板) | | `web/assets/attr_icons/` | 官网主属性图标(力量/敏捷/智力/全才,上分帝 Web 用) | diff --git a/CHANGELOG.md b/CHANGELOG.md index 4152591..75b3c7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ ## [Unreleased] +## [0.6.2] - 2026-07-30 + +### Added + +- 英雄页底部居中显示站点版本号与数据更新时间(取各数据集 `fetched_at` 最新值;打开详情时隐藏)。 +- 点击定位标签上方空白(英雄网格空隙或工具栏空白)可关闭详情面板。 + +### Changed + +- 版本页底部不再显示站点版本号(改由英雄页底部展示)。 + ## [0.6.1] - 2026-07-30 ### Added diff --git a/web/export_relations_site.py b/web/export_relations_site.py index 0beaadf..444273f 100644 --- a/web/export_relations_site.py +++ b/web/export_relations_site.py @@ -56,7 +56,7 @@ from shared.paths import ( from seo_prerender import DEFAULT_SITE_ORIGIN, write_seo_bundle 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" diff --git a/web/frontend/app.js b/web/frontend/app.js index ca2a1b8..5860c08 100644 --- a/web/frontend/app.js +++ b/web/frontend/app.js @@ -76,6 +76,52 @@ function siteVersionLabel() { 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() { const configured = typeof SITE_ORIGIN === "string" ? SITE_ORIGIN.trim().replace(/\/+$/, "") : ""; if (configured) return configured; @@ -208,14 +254,6 @@ function clearSeoPrerender() { 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 = { data: null, page: "heroes", // heroes | rankings | matches | streamers | trends | mechanics | items | patches @@ -385,6 +423,15 @@ function onHeroClick(key) { render(); } +function clearHeroDetail() { + if (!state.selectedKey) return false; + state.selectedKey = null; + state.inspect = null; + syncStateToUrl(); + render(); + return true; +} + function renderTagbar() { const bar = $("#tagbar"); if (!bar) return; @@ -3471,7 +3518,6 @@ function renderPatches() { if (select) select.innerHTML = ""; if (verEl) verEl.textContent = ""; root.innerHTML = '
暂无版本数据
'; - renderPatchesSiteVersion(); return; } @@ -3506,7 +3552,6 @@ function renderPatches() { const det = details[state.selectedPatch]; if (!det) { root.innerHTML = '
该版本详情暂未收录
'; - renderPatchesSiteVersion(); return; } @@ -3539,7 +3584,6 @@ function renderPatches() { .map((h) => renderHeroEntry(h, lookup)) .join(""); renderPatchSection(root, "英雄改动", heroesHtml); - renderPatchesSiteVersion(); } function escapeHtml(s) { @@ -5612,6 +5656,7 @@ function render() { } else { renderPatches(); } + renderHeroesSiteFoot(); 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() { // mobile-gate.js sets this before paint; skip desktop data boot on phones/tablets. if (typeof window !== "undefined" && window.__CLIMPEROR_MOBILE__) return; @@ -5796,12 +5861,7 @@ async function main() { renderBoard(); return; } - if (state.selectedKey) { - state.selectedKey = null; - state.inspect = null; - syncStateToUrl(); - render(); - } + clearHeroDetail(); return; } if (state.itemQuery && document.activeElement === $("#q-item")) { @@ -5839,6 +5899,7 @@ async function main() { ); bindSearch(); bindPageChrome(); + bindHeroesDismiss(); const brandLogo = document.querySelector(".brand-logo"); if (brandLogo) brandLogo.src = assetUrl("/ui-icon/dota2_logo_wordmark.png"); const favicon = document.querySelector('link[rel="icon"]'); diff --git a/web/frontend/config.js b/web/frontend/config.js index 5672df8..887a74c 100644 --- a/web/frontend/config.js +++ b/web/frontend/config.js @@ -1,5 +1,5 @@ /* 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 ABILITY_VIDEO_BASE = ""; var STATIC_ASSET_BASE = ""; diff --git a/web/frontend/index.html b/web/frontend/index.html index ff67023..5d8e24e 100644 --- a/web/frontend/index.html +++ b/web/frontend/index.html @@ -43,8 +43,8 @@ } - - + +

DOTA2 上分帝

@@ -227,13 +227,13 @@
-
+ - - - + + + diff --git a/web/frontend/style.css b/web/frontend/style.css index cdd941b..9de8a4b 100644 --- a/web/frontend/style.css +++ b/web/frontend/style.css @@ -493,6 +493,18 @@ body { overflow: hidden; 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) { 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); 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 { overflow-x: hidden; overflow-y: auto;