From 854cb0c2a1407fdef0203b0a2fa83a0d6dc2d2cd Mon Sep 17 00:00:00 2001 From: wizardchen Date: Fri, 15 May 2026 12:16:49 +0800 Subject: [PATCH] feat(ui): redirect tenant switch away from tenant-scoped routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switching the active tenant triggers window.location.reload() to re-key every cached store / open SSE stream / in-flight request. The catch: when the user is currently sitting on a route whose URL embeds a tenant-scoped resource id, the new tenant lands on a 404/403 and the user sees a blank screen. Three risky route shapes today: /platform/knowledge-bases/:kbId /platform/knowledge-bases/:kbId/creatChat /platform/chat/:chatid Add a tiny tenantSwitch utility (navigateAfterTenantSwitch) that matches the current path against this list and redirects to /platform/knowledge-bases — the platform default landing, always renderable regardless of role. Anything else (settings, agent list, KB list, etc.) continues to reload its current path because those views handle empty/forbidden data gracefully. Both tenant-switch call sites (TenantSelector and UserMenu) use the helper so the matrix lives in one place; future tenant-scoped routes only need to add a regex to the list. --- frontend/src/components/TenantSelector.vue | 6 ++- frontend/src/components/UserMenu.vue | 9 +++-- frontend/src/utils/tenantSwitch.ts | 44 ++++++++++++++++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 frontend/src/utils/tenantSwitch.ts diff --git a/frontend/src/components/TenantSelector.vue b/frontend/src/components/TenantSelector.vue index 399adb4d..512bf961 100644 --- a/frontend/src/components/TenantSelector.vue +++ b/frontend/src/components/TenantSelector.vue @@ -82,6 +82,7 @@ import { useAuthStore } from '@/stores/auth' import { searchTenants, type TenantInfo } from '@/api/tenant' import { useI18n } from 'vue-i18n' import { MessagePlugin } from 'tdesign-vue-next' +import { navigateAfterTenantSwitch } from '@/utils/tenantSwitch' const { t } = useI18n() const authStore = useAuthStore() @@ -161,7 +162,7 @@ const clearSearch = () => { const selectTenant = (tenantId: number) => { // 找到选中的租户信息 const selectedTenant = tenants.value.find(t => t.id === tenantId) - + if (tenantId === defaultTenantId.value) { authStore.setSelectedTenant(null, null) } else { @@ -169,8 +170,9 @@ const selectTenant = (tenantId: number) => { } closeDropdown() MessagePlugin.success(t('tenant.switchSuccess')) + // 切换租户后跳转到新租户下安全的入口(详见 tenantSwitch.ts 注释)。 setTimeout(() => { - window.location.reload() + navigateAfterTenantSwitch() }, 500) } diff --git a/frontend/src/components/UserMenu.vue b/frontend/src/components/UserMenu.vue index c8e1cf2a..02578a20 100644 --- a/frontend/src/components/UserMenu.vue +++ b/frontend/src/components/UserMenu.vue @@ -214,6 +214,7 @@ import { getCurrentUser, logout as logoutApi } from '@/api/auth' import { useI18n } from 'vue-i18n' import IMChannelsOverviewPanel from '@/components/IMChannelsOverviewPanel.vue' import { listAllIMChannels, type IMChannelOverview } from '@/api/agent' +import { navigateAfterTenantSwitch } from '@/utils/tenantSwitch' const { t } = useI18n() @@ -372,10 +373,12 @@ const switchToTenant = (m: Membership) => { closeAll() MessagePlugin.success(t('tenant.switchSuccess')) // Hard reload so every cached store / open SSE stream / in-flight - // request gets re-keyed under the new tenant. Same approach as - // TenantSelector.vue. + // request gets re-keyed under the new tenant. If the current path + // embeds a tenant-scoped resource id, reload would white-screen the + // user; navigateAfterTenantSwitch redirects to the platform home in + // that case. Same helper as TenantSelector.vue. setTimeout(() => { - window.location.reload() + navigateAfterTenantSwitch() }, 400) } diff --git a/frontend/src/utils/tenantSwitch.ts b/frontend/src/utils/tenantSwitch.ts new file mode 100644 index 00000000..ec3374fa --- /dev/null +++ b/frontend/src/utils/tenantSwitch.ts @@ -0,0 +1,44 @@ +// Tenant-switch navigation helper. +// +// Switching the active tenant triggers a full page reload so every cached +// store / open SSE stream / in-flight request gets re-keyed under the new +// tenant. The catch: if the user is currently sitting on a route whose URL +// embeds a tenant-scoped resource id (KB id, session id), reloading lands +// them on a 403/empty page in the new tenant — visible to the user as a +// blank screen. For those routes we redirect to the platform default +// landing page instead, which is always safe regardless of role. +// +// Anything not on this list keeps its current path on reload (settings, +// agent list, KB list, etc. — they re-fetch under the new tenant +// naturally). + +const TENANT_SCOPED_ROUTE_PATTERNS: RegExp[] = [ + /^\/platform\/knowledge-bases\/[^/]+$/, // /platform/knowledge-bases/:kbId + /^\/platform\/knowledge-bases\/[^/]+\/creatChat$/, // /platform/knowledge-bases/:kbId/creatChat + /^\/platform\/chat\/[^/]+$/, // /platform/chat/:chatid +] + +const SAFE_FALLBACK_PATH = '/platform/knowledge-bases' + +/** + * Return the URL to navigate to after a tenant switch. If the current path + * embeds a tenant-scoped id, returns the safe fallback; otherwise returns + * null to signal "reload current path". + */ +export function tenantSwitchTargetPath(currentPath: string): string | null { + const isScoped = TENANT_SCOPED_ROUTE_PATTERNS.some(re => re.test(currentPath)) + return isScoped ? SAFE_FALLBACK_PATH : null +} + +/** + * Perform the post-switch navigation. Either redirect to the safe fallback + * (if the current page is tenant-scoped) or hard-reload the current page. + */ +export function navigateAfterTenantSwitch(): void { + const target = tenantSwitchTargetPath(window.location.pathname) + if (target !== null) { + window.location.href = target + } else { + window.location.reload() + } +}