mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 04:18:17 +08:00
fix(wiki): keep the open knowledge base and page in the URL
This commit is contained in:
parent
936c8621ed
commit
4b6e0b2e0e
@ -46,7 +46,10 @@ const router = createRouter({
|
|||||||
redirect: { path: '/agents', query: { view: 'live' } },
|
redirect: { path: '/agents', query: { view: 'live' } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'wiki',
|
// Optional :kbId path param so an open knowledge base survives a
|
||||||
|
// manual page refresh (without it, reload drops back to the library
|
||||||
|
// list). The legacy ?kbId=&slug= query form still resolves here too.
|
||||||
|
path: 'wiki/:kbId?',
|
||||||
name: 'Wiki',
|
name: 'Wiki',
|
||||||
component: () => import('@/views/Wiki/index.vue'),
|
component: () => import('@/views/Wiki/index.vue'),
|
||||||
meta: { title: 'Wiki', requiredCapability: 'view:wiki' },
|
meta: { title: 'Wiki', requiredCapability: 'view:wiki' },
|
||||||
|
|||||||
@ -117,47 +117,90 @@ async function handleDeleteKB(kb: WikiKB) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function consumeQueryNavigation() {
|
// ---- URL ↔ store sync --------------------------------------------------
|
||||||
// Global wikilink click delegator (see App.vue) pushes us with
|
// The open KB (and the open page within it) live in the route so a manual
|
||||||
// ?kbId=X&slug=Y on click. Honour both: enter the KB then surface
|
// refresh restores exactly where you were instead of dropping back to the
|
||||||
// the page directly. Strips the query immediately so a manual reload
|
// library list. Canonical shape: /wiki/:kbId?view=manage&slug=<page>.
|
||||||
// doesn't keep re-opening the same page.
|
//
|
||||||
//
|
// The store stays the source of truth for the rendered view; we mirror it
|
||||||
// **Snowflake precision** (per CLAUDE.md): the kbId is a 19-digit
|
// into the URL (syncUrlFromStore) and seed it from the URL on first load and
|
||||||
// Snowflake that exceeds Number.MAX_SAFE_INTEGER. NEVER coerce via
|
// on browser back/forward (syncStoreFromUrl). A single `syncing` latch keeps
|
||||||
// Number()/parseInt() — that truncates the last 2-3 digits and turns
|
// the two watchers from ping-ponging.
|
||||||
// a real lookup into a silent "KB not found". Keep the string and
|
//
|
||||||
// pass it through to the store; the store / api layer treats kbId as
|
// **Snowflake precision** (per CLAUDE.md): kbId is a 19-digit Snowflake that
|
||||||
// an opaque token interpolated into the request URL.
|
// exceeds Number.MAX_SAFE_INTEGER. NEVER Number()/parseInt() it — that
|
||||||
const kbIdRaw = route.query.kbId
|
// truncates the id into a silent "KB not found". It stays a string and is
|
||||||
const slugRaw = route.query.slug
|
// passed opaquely to the store / API; the `as unknown as number` casts only
|
||||||
if (typeof kbIdRaw !== 'string' || !kbIdRaw) return
|
// satisfy the store's numeric type signature — the runtime value is the string.
|
||||||
if (typeof slugRaw !== 'string' || !slugRaw) return
|
let syncing = false
|
||||||
// Cast to number ONLY to satisfy the store's type signature — the
|
|
||||||
// runtime value stays a string under the hood. TypeScript can't
|
function routeKbId(): string | null {
|
||||||
// express "number-or-Snowflake-string" without widening every signature,
|
// Accept the canonical path param and the legacy ?kbId= query that the
|
||||||
// so the cast is the localised, documented escape hatch.
|
// global wikilink delegator still pushes (App.vue → useGlobalWikilinkClick).
|
||||||
// snowflake-precision-ok: kbIdRaw is the URL-encoded string from the
|
const fromParam = route.params.kbId
|
||||||
// global click delegator; never passed through Number()/parseInt().
|
if (typeof fromParam === 'string' && fromParam) return fromParam
|
||||||
const kbId = kbIdRaw as unknown as number
|
const fromQuery = route.query.kbId
|
||||||
await store.selectKB(kbId)
|
if (typeof fromQuery === 'string' && fromQuery) return fromQuery
|
||||||
try {
|
return null
|
||||||
await store.loadPage(kbId, slugRaw)
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[Wiki] auto-open page failed', e)
|
|
||||||
}
|
|
||||||
// Drop the query so back-button + reload behave sanely.
|
|
||||||
router.replace({ name: 'Wiki' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncStoreFromUrl() {
|
||||||
|
if (syncing) return
|
||||||
|
syncing = true
|
||||||
|
try {
|
||||||
|
const kbId = routeKbId()
|
||||||
|
const mode = route.query.view === 'manage' ? 'manage' : 'browse'
|
||||||
|
const slug = typeof route.query.slug === 'string' ? route.query.slug : ''
|
||||||
|
if (kbId) {
|
||||||
|
if (String(store.currentKB?.id ?? '') !== kbId) {
|
||||||
|
// snowflake-precision-ok: kbId is the raw route string, never coerced.
|
||||||
|
await store.selectKB(kbId as unknown as number, mode)
|
||||||
|
} else if (store.workspaceMode !== mode) {
|
||||||
|
store.setWorkspaceMode(mode)
|
||||||
|
}
|
||||||
|
if (slug && store.currentPage?.slug !== slug) {
|
||||||
|
// snowflake-precision-ok: kbId stays a string end to end.
|
||||||
|
try { await store.loadPage(kbId as unknown as number, slug) }
|
||||||
|
catch (e) { console.warn('[Wiki] open page from url failed', e) }
|
||||||
|
}
|
||||||
|
} else if (store.currentKB) {
|
||||||
|
store.backToLibrary()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
syncing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncUrlFromStore() {
|
||||||
|
if (syncing) return
|
||||||
|
const kb = store.currentKB
|
||||||
|
const params: Record<string, string> = kb ? { kbId: String(kb.id) } : {}
|
||||||
|
const query: Record<string, string> = {}
|
||||||
|
if (kb) {
|
||||||
|
if (store.workspaceMode === 'manage') query.view = 'manage'
|
||||||
|
if (store.currentPage?.slug) query.slug = store.currentPage.slug
|
||||||
|
}
|
||||||
|
const sameKb = String(route.params.kbId ?? '') === (params.kbId ?? '')
|
||||||
|
const sameView = (route.query.view ?? '') === (query.view ?? '')
|
||||||
|
const sameSlug = (route.query.slug ?? '') === (query.slug ?? '')
|
||||||
|
// Collapse the legacy ?kbId= query into the canonical path-param form.
|
||||||
|
const legacyQuery = route.query.kbId != null
|
||||||
|
if (sameKb && sameView && sameSlug && !legacyQuery) return
|
||||||
|
router.replace({ name: 'Wiki', params, query })
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [store.currentKB?.id, store.workspaceMode, store.currentPage?.slug],
|
||||||
|
syncUrlFromStore,
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(() => route.fullPath, () => { syncStoreFromUrl() })
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await store.fetchKnowledgeBases()
|
await store.fetchKnowledgeBases()
|
||||||
await consumeQueryNavigation()
|
await syncStoreFromUrl()
|
||||||
|
syncUrlFromStore()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Re-consume the query when a click delegator navigates while we're
|
|
||||||
// already on /wiki (route.path unchanged, query changed).
|
|
||||||
watch(() => route.query, () => { consumeQueryNavigation() })
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user