v0.6.3: hero detail bottom drawer that pushes role tags, matches page summary dedupe.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+208
-6
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user