From 685cc9e0d8834fb6670c87b454b9477b317b64a7 Mon Sep 17 00:00:00 2001 From: LiHaohua Date: Wed, 22 Jul 2026 13:40:07 +0800 Subject: [PATCH] feat(portal): per-tab URL routes + loading states - Upload and My Packages are now distinct URL hashes (#/upload, #/mine), so refreshing (or bookmarking / back-forward) keeps the current tab instead of always snapping back to Upload. - Add a boot spinner while /api/me is fetched, so the page no longer looks blank/broken on first paint. - Show a spinner in the My Packages table while the APT index loads. Co-authored-by: Cursor --- site/app.js | 35 ++++++++++++++++++++++++++++++++--- site/index.html | 13 ++++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/site/app.js b/site/app.js index ee070c2..7f5c556 100644 --- a/site/app.js +++ b/site/app.js @@ -109,10 +109,13 @@ async function init() { const r = await fetch("/api/me"); if (r.ok) me = await r.json(); } catch { /* not logged in */ } + $("boot-view").classList.add("hidden"); $(me ? "app-view" : "login-view").classList.remove("hidden"); if (me) { $("who-box").classList.remove("hidden"); $("who").textContent = me.login + (me.is_admin ? "(管理员)" : ""); + // Honor the tab encoded in the URL hash so a refresh stays put. + applyRoute(); } } @@ -136,17 +139,39 @@ async function loadIndex() { } /* --------------------------------- tabs ---------------------------------- */ +// Each tab is a distinct URL hash (#/upload, #/mine) so a page refresh keeps +// the user on the same tab and the browser back/forward buttons work. -$("tab-upload").addEventListener("click", () => switchTab("upload")); -$("tab-mine").addEventListener("click", () => { switchTab("mine"); renderMine(); }); +function tabFromHash() { + return /^#\/?mine\b/.test(location.hash || "") ? "mine" : "upload"; +} -function switchTab(which) { +// Reflect the current URL hash into the visible tab (no history change). +function applyRoute() { + showTab(tabFromHash()); +} + +// Update the visible panels/buttons for a tab, without touching the URL. +function showTab(which) { $("tab-upload").classList.toggle("active", which === "upload"); $("tab-mine").classList.toggle("active", which === "mine"); $("upload-panel").classList.toggle("hidden", which !== "upload"); $("mine-panel").classList.toggle("hidden", which !== "mine"); + if (which === "mine") renderMine(); } +// Clicking a tab navigates by hash; the hashchange handler does the rendering. +// (If the hash is already correct, hashchange won't fire, so render directly.) +function switchTab(which) { + const hash = which === "mine" ? "#/mine" : "#/upload"; + if (location.hash === hash) showTab(which); + else location.hash = hash; +} + +$("tab-upload").addEventListener("click", () => switchTab("upload")); +$("tab-mine").addEventListener("click", () => switchTab("mine")); +window.addEventListener("hashchange", applyRoute); + /* ------------------------------ file picking ----------------------------- */ const drop = $("drop"); @@ -391,7 +416,11 @@ $("submit-btn").addEventListener("click", async () => { /* ------------------------------ my packages ------------------------------ */ async function renderMine() { + if (!me) return; const rows = $("mine-rows"); + // Show a spinner while the (possibly slow) APT index fetch is in flight, so + // an empty table never looks like a broken page. + rows.innerHTML = `
正在读取你的软件包…
`; const idx = await loadIndex(); const mine = []; for (const [name, entries] of idx) { diff --git a/site/index.html b/site/index.html index 97b99be..dcdb0b6 100644 --- a/site/index.html +++ b/site/index.html @@ -101,6 +101,12 @@ .login-box { text-align: center; padding: 48px 20px; } .login-box button { width: auto; padding: 12px 40px; } .muted { color: var(--dim); font-size: 13px; } + .loading { display: flex; align-items: center; justify-content: center; gap: 10px; + color: var(--dim); font-size: 13px; padding: 30px 10px; } + .spinner { width: 16px; height: 16px; border: 2px solid var(--line); + border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; + display: inline-block; flex: none; } + @keyframes spin { to { transform: rotate(360deg); } } @@ -110,6 +116,11 @@

CardputerZero 开发者中心

+ +
+
加载中…
+
+