mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-06-04 13:30:32 +08:00
feat(tenant): show rich workspace-switch confirmation that survives reload
Replace the lightweight "switch success" message with a richer NotifyPlugin notification that reports the new workspace name and the user's role in it. Because the post-switch flow does a hard reload, the notification is stashed in sessionStorage before navigation and consumed by App.vue on the next mount, so the toast actually appears on the destination page with its full 6s duration instead of being torn down a frame after it is shown. For the cross-tenant superuser path (TenantSelector), the role line is omitted when the user has no membership row in the target tenant, avoiding a misleading empty/raw role value. Role labels are formatted through the shared useRoleLabel composable so any future role text change still lives in one place. i18n keys updated across zh-CN, en-US, ko-KR, ru-RU.
This commit is contained in:
@@ -2,11 +2,12 @@
|
||||
import { computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import { MessagePlugin, NotifyPlugin } from 'tdesign-vue-next'
|
||||
import ManualKnowledgeEditor from '@/components/manual-knowledge-editor.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { getCurrentUser } from '@/api/auth'
|
||||
import { consumePendingTenantSwitchToast } from '@/utils/tenantSwitch'
|
||||
|
||||
// TDesign locale configs
|
||||
import enUSConfig from 'tdesign-vue-next/esm/locale/en_US'
|
||||
@@ -14,7 +15,7 @@ import zhCNConfig from 'tdesign-vue-next/esm/locale/zh_CN'
|
||||
import koKRConfig from 'tdesign-vue-next/esm/locale/ko_KR'
|
||||
import ruRUConfig from 'tdesign-vue-next/esm/locale/ru_RU'
|
||||
|
||||
const { locale } = useI18n()
|
||||
const { locale, t } = useI18n()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
@@ -183,8 +184,24 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 切换租户后会 hard reload;切换前 stash 的 toast 这里 consume 并弹出,
|
||||
// 这样 toast 显示在新页面上,duration 才真正生效。
|
||||
const showPendingTenantSwitchToast = () => {
|
||||
const pending = consumePendingTenantSwitchToast()
|
||||
if (!pending) return
|
||||
NotifyPlugin.success({
|
||||
title: t('tenant.switchSuccessTitle'),
|
||||
content: pending.role
|
||||
? t('tenant.switchSuccessContentWithRole', { name: pending.name, role: pending.role })
|
||||
: t('tenant.switchSuccessContent', { name: pending.name }),
|
||||
duration: 6000,
|
||||
closeBtn: true,
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleGlobalOIDCCallback()
|
||||
showPendingTenantSwitchToast()
|
||||
|
||||
// Auto check for updates on startup
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -76,11 +76,13 @@ 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'
|
||||
import { navigateAfterTenantSwitch, stashTenantSwitchToast } from '@/utils/tenantSwitch'
|
||||
import CreateTenantDialog from '@/components/CreateTenantDialog.vue'
|
||||
import { useRoleLabel } from '@/composables/useRoleLabel'
|
||||
|
||||
const { t } = useI18n()
|
||||
const authStore = useAuthStore()
|
||||
const { formatRole } = useRoleLabel()
|
||||
|
||||
const showDropdown = ref(false)
|
||||
const searchQuery = ref('')
|
||||
@@ -164,7 +166,16 @@ const selectTenant = (tenantId: number) => {
|
||||
authStore.setSelectedTenant(tenantId, selectedTenant?.name || null)
|
||||
}
|
||||
closeDropdown()
|
||||
MessagePlugin.success(t('tenant.switchSuccess'))
|
||||
const displayName = selectedTenant?.name
|
||||
|| (tenantId === defaultTenantId.value ? authStore.tenant?.name : null)
|
||||
|| `#${tenantId}`
|
||||
// Cross-tenant superusers may not have a membership row in the target
|
||||
// tenant; in that case skip the role line rather than show a misleading
|
||||
// empty/raw value.
|
||||
const membership = (authStore.memberships ?? []).find((m) => Number(m.tenant_id) === tenantId)
|
||||
const roleLabel = membership ? formatRole(membership.role) : ''
|
||||
// Toast 在 reload 后由 App.vue 弹出(直接在这里弹会被 hard reload 干掉)。
|
||||
stashTenantSwitchToast({ name: displayName, role: roleLabel || undefined })
|
||||
// 切换租户后跳转到新租户下安全的入口(详见 tenantSwitch.ts 注释)。
|
||||
setTimeout(() => {
|
||||
navigateAfterTenantSwitch()
|
||||
|
||||
@@ -236,7 +236,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import IMChannelsOverviewPanel from '@/components/IMChannelsOverviewPanel.vue'
|
||||
import CreateTenantDialog from '@/components/CreateTenantDialog.vue'
|
||||
import { listAllIMChannels, type IMChannelOverview } from '@/api/agent'
|
||||
import { navigateAfterTenantSwitch } from '@/utils/tenantSwitch'
|
||||
import { navigateAfterTenantSwitch, stashTenantSwitchToast } from '@/utils/tenantSwitch'
|
||||
import type { TenantInfo } from '@/api/tenant'
|
||||
import { useRoleLabel, useHomeTenant } from '@/composables/useRoleLabel'
|
||||
|
||||
@@ -446,7 +446,11 @@ const switchToTenant = (m: Membership) => {
|
||||
authStore.setSelectedTenant(m.tenant_id, tenantDisplayName(m))
|
||||
}
|
||||
closeAll()
|
||||
MessagePlugin.success(t('tenant.switchSuccess'))
|
||||
// Toast 在 reload 后由 App.vue 弹出(直接在这里弹会被 hard reload 干掉)。
|
||||
stashTenantSwitchToast({
|
||||
name: tenantDisplayName(m),
|
||||
role: formatRole(m.role) || undefined,
|
||||
})
|
||||
// Hard reload so every cached store / open SSE stream / in-flight
|
||||
// request gets re-keyed under the new tenant. If the current path
|
||||
// embeds a tenant-scoped resource id, reload would white-screen the
|
||||
|
||||
@@ -2389,7 +2389,9 @@ export default {
|
||||
searchPlaceholder: 'Search by name or enter tenant ID...',
|
||||
searchHint: 'Search by name or enter tenant ID directly',
|
||||
noMatch: 'No matching tenants found',
|
||||
switchSuccess: 'Tenant switched successfully',
|
||||
switchSuccessTitle: 'Workspace switched',
|
||||
switchSuccessContent: 'You are now in "{name}"',
|
||||
switchSuccessContentWithRole: 'You are now in "{name}" as {role}',
|
||||
loadTenantsFailed: 'Failed to load tenant list',
|
||||
loading: 'Loading...',
|
||||
loadMore: 'Load more',
|
||||
|
||||
@@ -1637,7 +1637,9 @@ export default {
|
||||
searchPlaceholder: "테넌트 이름 검색 또는 테넌트 ID 입력...",
|
||||
searchHint: "이름으로 검색하거나 테넌트 ID를 직접 입력할 수 있습니다",
|
||||
noMatch: "일치하는 테넌트를 찾을 수 없습니다",
|
||||
switchSuccess: "테넌트 전환 성공",
|
||||
switchSuccessTitle: "공간이 전환되었습니다",
|
||||
switchSuccessContent: "「{name}」에 입장했습니다",
|
||||
switchSuccessContentWithRole: "「{name}」에 입장했습니다 · 역할: {role}",
|
||||
loadTenantsFailed: "테넌트 목록 로드 실패",
|
||||
loading: "로딩 중...",
|
||||
loadMore: "더 보기",
|
||||
|
||||
@@ -1435,7 +1435,9 @@ export default {
|
||||
searchPlaceholder: 'Поиск по имени или введите ID арендатора...',
|
||||
searchHint: 'Поиск по имени или введите ID арендатора напрямую',
|
||||
noMatch: 'Не найдено подходящих арендаторов',
|
||||
switchSuccess: 'Арендатор успешно переключен',
|
||||
switchSuccessTitle: 'Пространство переключено',
|
||||
switchSuccessContent: 'Вы вошли в «{name}»',
|
||||
switchSuccessContentWithRole: 'Вы вошли в «{name}» · роль: {role}',
|
||||
loadTenantsFailed: 'Не удалось загрузить список арендаторов',
|
||||
loading: 'Загрузка...',
|
||||
loadMore: 'Загрузить еще',
|
||||
|
||||
@@ -1617,7 +1617,9 @@ export default {
|
||||
searchPlaceholder: "搜索空间名称或输入空间 ID...",
|
||||
searchHint: "支持按名称搜索或直接输入空间 ID",
|
||||
noMatch: "未找到匹配的空间",
|
||||
switchSuccess: "空间切换成功",
|
||||
switchSuccessTitle: "已切换空间",
|
||||
switchSuccessContent: "你已进入「{name}」",
|
||||
switchSuccessContentWithRole: "你已进入「{name}」,身份:{role}",
|
||||
loadTenantsFailed: "加载空间列表失败",
|
||||
loading: "加载中...",
|
||||
loadMore: "加载更多",
|
||||
|
||||
@@ -22,3 +22,36 @@ export function tenantSwitchTargetPath(_currentPath: string): string {
|
||||
export function navigateAfterTenantSwitch(): void {
|
||||
window.location.href = tenantSwitchTargetPath(window.location.pathname)
|
||||
}
|
||||
|
||||
// 切换成功后的 toast 跨 hard reload 传递:调用方在 reload 前把信息塞进
|
||||
// sessionStorage,App.vue 启动时 consume 一次再弹出。直接在 reload 前调
|
||||
// NotifyPlugin 会被刷掉,根本来不及看清。
|
||||
const PENDING_TOAST_KEY = 'weknora_pending_tenant_switch_toast'
|
||||
|
||||
export interface PendingTenantSwitchToast {
|
||||
name: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
export function stashTenantSwitchToast(payload: PendingTenantSwitchToast): void {
|
||||
try {
|
||||
sessionStorage.setItem(PENDING_TOAST_KEY, JSON.stringify(payload))
|
||||
} catch {
|
||||
// sessionStorage 写失败(隐私模式等)就静默放弃,toast 是锦上添花
|
||||
}
|
||||
}
|
||||
|
||||
export function consumePendingTenantSwitchToast(): PendingTenantSwitchToast | null {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(PENDING_TOAST_KEY)
|
||||
if (!raw) return null
|
||||
sessionStorage.removeItem(PENDING_TOAST_KEY)
|
||||
const parsed = JSON.parse(raw)
|
||||
if (parsed && typeof parsed.name === 'string') {
|
||||
return { name: parsed.name, role: typeof parsed.role === 'string' ? parsed.role : undefined }
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user