diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b3c7f..a84a614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ ## [Unreleased] +## [0.6.3] - 2026-07-30 + +### Changed + +- 英雄详情改为自底部上滑抽屉:高度展开顶起定位标签(标签不挂在面板上);点击空白或下滑可关闭;去掉标签行全宽底与打开时的网格遮罩条。 +- 比赛页去掉筛选侧栏重复的「共 X 场 · 第 X/X 页」(仅保留列表底部分页区统计)。 + ## [0.6.2] - 2026-07-30 ### Added diff --git a/web/export_relations_site.py b/web/export_relations_site.py index 444273f..ec59500 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.2" +SITE_VERSION = "0.6.3" DEFAULT_OSS_BASE = "https://climperor.oss-cn-shanghai.aliyuncs.com" diff --git a/web/frontend/app.js b/web/frontend/app.js index 5860c08..c6aa509 100644 --- a/web/frontend/app.js +++ b/web/frontend/app.js @@ -425,6 +425,15 @@ function onHeroClick(key) { function clearHeroDetail() { if (!state.selectedKey) return false; + const drawer = $("#detail-drawer"); + if ( + drawer && + !drawer.classList.contains("is-collapsed") && + !detailDrawerClosing + ) { + collapseDetailDrawerThenClear(); + return true; + } state.selectedKey = null; state.inspect = null; syncStateToUrl(); @@ -432,6 +441,188 @@ function clearHeroDetail() { return true; } +let detailDrawerDrag = null; +let detailDrawerSuppressClickUntil = 0; +let detailDrawerClosing = false; + +function detailDrawerFullHeight() { + const panel = $("#detail"); + if (panel && panel.offsetHeight) return panel.offsetHeight; + return Math.min(720, Math.round((window.innerHeight || 800) * 0.88)); +} + +function syncDetailDrawer() { + const drawer = $("#detail-drawer"); + if (!drawer) return; + const open = state.page === "heroes" && !!state.selectedKey && !detailDrawerClosing; + drawer.setAttribute("aria-hidden", open ? "false" : "true"); + document.body.classList.toggle("detail-drawer-open", open); + if (!open) { + drawer.classList.add("is-collapsed"); + drawer.style.maxHeight = ""; + resetDetailDrawerDrag(); + return; + } + drawer.classList.remove("is-collapsed"); + drawer.style.maxHeight = ""; +} + +function resetDetailDrawerDrag() { + const drawer = $("#detail-drawer"); + if (drawer) { + drawer.classList.remove("is-dragging"); + if (!detailDrawerClosing) drawer.style.maxHeight = ""; + } + detailDrawerDrag = null; +} + +function setDetailDrawerDragOffset(dy) { + const drawer = $("#detail-drawer"); + if (!drawer) return; + const full = detailDrawerDrag?.fullH || detailDrawerFullHeight() || 1; + const h = Math.max(0, full - Math.max(0, dy)); + drawer.style.maxHeight = `${h}px`; +} + +function endDetailDrawerDrag(dy, velocityY) { + const drawer = $("#detail-drawer"); + if (!drawer) { + resetDetailDrawerDrag(); + return; + } + const full = detailDrawerDrag?.fullH || detailDrawerFullHeight() || 1; + const shouldClose = dy > Math.min(140, full * 0.22) || velocityY > 0.85; + if (detailDrawerDrag?.moved) { + detailDrawerSuppressClickUntil = performance.now() + 280; + } + if (shouldClose) { + collapseDetailDrawerThenClear(); + return; + } + drawer.classList.remove("is-dragging"); + drawer.style.maxHeight = ""; + detailDrawerDrag = null; +} + +function collapseDetailDrawerThenClear() { + const drawer = $("#detail-drawer"); + if (!state.selectedKey) return; + if (!drawer || detailDrawerClosing) { + state.selectedKey = null; + state.inspect = null; + syncStateToUrl(); + render(); + return; + } + detailDrawerClosing = true; + const fromH = drawer.offsetHeight || detailDrawerFullHeight(); + drawer.classList.add("is-dragging"); + drawer.style.maxHeight = `${fromH}px`; + void drawer.offsetHeight; + drawer.classList.remove("is-dragging"); + drawer.classList.add("is-collapsed"); + drawer.style.maxHeight = "0px"; + document.body.classList.remove("detail-drawer-open"); + let done = false; + const finish = () => { + if (done) return; + done = true; + drawer.removeEventListener("transitionend", finish); + detailDrawerClosing = false; + drawer.style.maxHeight = ""; + resetDetailDrawerDrag(); + state.selectedKey = null; + state.inspect = null; + syncStateToUrl(); + render(); + }; + const reduce = + typeof matchMedia === "function" && + matchMedia("(prefers-reduced-motion: reduce)").matches; + if (reduce) { + finish(); + return; + } + drawer.addEventListener("transitionend", finish); + window.setTimeout(finish, 360); +} + +/** True when the event target is drawer chrome (handle / head / empty padding), not interactive content. */ +function isDetailDrawerDismissTarget(target) { + if (!(target instanceof Element)) return false; + if (target.closest(".detail-drawer-handle, .detail-head")) return true; + if (target.closest("button, a, input, select, textarea, label, video, .hero-inspect, .detail-tab-body, .detail-tabs, .detail-abilities, .detail-items, .detail-skills-layout, .detail-items-layout, .detail-stats-layout, .detail-matchups-layout, .detail-matches-layout, .detail-streamers-layout, .detail-patches-layout")) { + return false; + } + return target.id === "detail" || target.classList.contains("detail-drawer-panel"); +} + +function bindDetailDrawerGestures() { + const drawer = $("#detail-drawer"); + const panel = $("#detail"); + if (!drawer || !panel) return; + + panel.addEventListener("click", (e) => { + if (state.page !== "heroes" || !state.selectedKey) return; + if (performance.now() < detailDrawerSuppressClickUntil) return; + if (!isDetailDrawerDismissTarget(e.target)) return; + collapseDetailDrawerThenClear(); + }); + + const onPointerDown = (e) => { + if (state.page !== "heroes" || !state.selectedKey) return; + if (e.button != null && e.button !== 0) return; + if (!e.target.closest(".detail-drawer-handle, .detail-head")) return; + const fullH = detailDrawerFullHeight(); + detailDrawerDrag = { + pointerId: e.pointerId, + startY: e.clientY, + lastY: e.clientY, + lastT: performance.now(), + dy: 0, + velocityY: 0, + moved: false, + fullH, + }; + drawer.classList.add("is-dragging"); + drawer.style.maxHeight = `${fullH}px`; + try { + drawer.setPointerCapture(e.pointerId); + } catch (_) { + /* ignore */ + } + }; + + const onPointerMove = (e) => { + if (!detailDrawerDrag || e.pointerId !== detailDrawerDrag.pointerId) return; + const now = performance.now(); + const dy = e.clientY - detailDrawerDrag.startY; + const dt = Math.max(1, now - detailDrawerDrag.lastT); + detailDrawerDrag.velocityY = (e.clientY - detailDrawerDrag.lastY) / dt; + detailDrawerDrag.lastY = e.clientY; + detailDrawerDrag.lastT = now; + detailDrawerDrag.dy = dy; + if (Math.abs(dy) > 4) detailDrawerDrag.moved = true; + if (dy > 0) setDetailDrawerDragOffset(dy); + else setDetailDrawerDragOffset(0); + }; + + const onPointerUp = (e) => { + if (!detailDrawerDrag || e.pointerId !== detailDrawerDrag.pointerId) return; + const { dy, velocityY, moved } = detailDrawerDrag; + if (!moved && dy <= 0) { + resetDetailDrawerDrag(); + return; + } + endDetailDrawerDrag(Math.max(0, dy), velocityY); + }; + + drawer.addEventListener("pointerdown", onPointerDown); + drawer.addEventListener("pointermove", onPointerMove); + drawer.addEventListener("pointerup", onPointerUp); + drawer.addEventListener("pointercancel", onPointerUp); +} + function renderTagbar() { const bar = $("#tagbar"); if (!bar) return; @@ -2957,18 +3148,27 @@ function renderDetail() { root.innerHTML = ""; if (!state.selectedKey) { - root.classList.add("empty", "is-collapsed"); + root.classList.add("empty"); + root.innerHTML = ""; + syncDetailDrawer(); return; } - root.classList.remove("is-collapsed", "empty"); + root.classList.remove("empty"); const hero = heroByKey(state.selectedKey); if (!hero) { root.classList.add("empty"); root.textContent = "未找到英雄数据"; + syncDetailDrawer(); return; } + const handle = document.createElement("button"); + handle.type = "button"; + handle.className = "detail-drawer-handle"; + handle.setAttribute("aria-label", "下滑或点击关闭详情"); + root.appendChild(handle); + const head = document.createElement("div"); head.className = "detail-head"; const title = document.createElement("h2"); @@ -3112,6 +3312,7 @@ function renderDetail() { layout.appendChild(renderHeroInspect(state.selectedKey)); root.appendChild(layout); } + syncDetailDrawer(); } function shopCatalog() { @@ -3932,7 +4133,6 @@ function renderMatchesPage() { const body = $("#matches-body"); const originsEl = $("#matches-origins"); const playersEl = $("#matches-players"); - const sub = $("#matches-sub"); if (!body) return; const origin = MATCH_ORIGIN_FILTERS.some((o) => o.id === state.matchesOrigin) @@ -4039,7 +4239,6 @@ function renderMatchesPage() { const summary = total ? `共 ${total} 场 · 第 ${page}/${pageCount} 页` : ""; - if (sub) sub.textContent = summary; if (!rows.length) { body.innerHTML = state.matchesPlayerId @@ -5608,6 +5807,7 @@ function syncChrome() { document.querySelectorAll(".main-tab").forEach((btn) => { btn.classList.toggle("active", btn.dataset.page === state.page); }); + const detailDrawer = $("#detail-drawer"); const heroesTb = $("#heroes-toolbar"); const heroSearch = $("#q"); const itemSearch = $("#q-item"); @@ -5619,8 +5819,8 @@ function syncChrome() { const mechanicsView = $("#mechanics-view"); const itemsView = $("#items-view"); const patchesView = $("#patches-view"); - const detail = $("#detail"); if (heroesTb) heroesTb.classList.toggle("hidden", state.page !== "heroes"); + if (detailDrawer) detailDrawer.classList.toggle("hidden", state.page !== "heroes"); if (heroSearch) heroSearch.classList.toggle("hidden", state.page !== "heroes"); if (itemSearch) itemSearch.classList.toggle("hidden", state.page !== "items"); if (heroesView) heroesView.classList.toggle("hidden", state.page !== "heroes"); @@ -5631,7 +5831,8 @@ function syncChrome() { if (mechanicsView) mechanicsView.classList.toggle("hidden", state.page !== "mechanics"); if (itemsView) itemsView.classList.toggle("hidden", state.page !== "items"); if (patchesView) patchesView.classList.toggle("hidden", state.page !== "patches"); - if (detail) detail.classList.toggle("hidden", state.page !== "heroes"); + // Drawer open animation needs detail content laid out first; heroes path syncs in renderDetail. + if (state.page !== "heroes") syncDetailDrawer(); } function render() { @@ -5842,6 +6043,7 @@ function bindHeroesDismiss() { clearHeroDetail(); }); } + bindDetailDrawerGestures(); } async function main() { diff --git a/web/frontend/config.js b/web/frontend/config.js index 887a74c..1de8741 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.2"; +var SITE_VERSION = "0.6.3"; var SITE_ORIGIN = ""; var ABILITY_VIDEO_BASE = ""; var STATIC_ASSET_BASE = ""; diff --git a/web/frontend/index.html b/web/frontend/index.html index 5d8e24e..248ae31 100644 --- a/web/frontend/index.html +++ b/web/frontend/index.html @@ -43,8 +43,8 @@ } - - + +