feat(auth): 顶栏租户切换入口(仅超管显示)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-12 00:02:39 +08:00
parent 348221d198
commit da7a5b703e
5 changed files with 92 additions and 2 deletions

2
components.d.ts vendored
View File

@ -68,6 +68,7 @@ declare module 'vue' {
TySidebar: typeof import('./src/components/ty-sidebar.vue')['default']
TyTable: typeof import('./src/components/ty-table.vue')['default']
TyTabs: typeof import('./src/components/ty-tabs.vue')['default']
TyTenantSwitch: typeof import('./src/components/ty-tenant-switch.vue')['default']
}
export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
@ -132,4 +133,5 @@ declare global {
const TySidebar: typeof import('./src/components/ty-sidebar.vue')['default']
const TyTable: typeof import('./src/components/ty-table.vue')['default']
const TyTabs: typeof import('./src/components/ty-tabs.vue')['default']
const TyTenantSwitch: typeof import('./src/components/ty-tenant-switch.vue')['default']
}

View File

@ -58,3 +58,16 @@ export interface TenantBrand {
export function fetchBrand(signal?: AbortSignal): Promise<TenantBrand> {
return http.get<TenantBrand>('/auth/brand', { signal })
}
export interface SwitchTenantResult {
accessToken: string
refreshToken: string
expiresIn: number
tenantCode: string
tenantName: string
}
/** 平台超管切换租户。 */
export function switchTenant(tenantCode: string): Promise<SwitchTenantResult> {
return http.post<SwitchTenantResult>('/auth/switch-tenant', { tenantCode })
}

View File

@ -50,6 +50,7 @@
<icon type="iconify" icon="mdi:cog-outline" />
</button>
<ty-notice />
<ty-tenant-switch />
<el-dropdown trigger="click" @command="onUserCommand">
<span class="ty-header__user">
<icon type="iconify" icon="mdi:account-circle" />

View File

@ -0,0 +1,64 @@
<template>
<el-dropdown v-if="permissionStore.isSuper" trigger="click" @command="onCommand">
<button class="ty-tenant-switch" type="button" title="切换租户">
<icon type="iconify" icon="mdi:domain" />
</button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="t in tenants" :key="t.code" :command="t.code" :disabled="t.code === currentTenantCode">
{{ t.name }}({{ t.code }})
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useAuthStore } from '@/stores/modules/auth'
import { usePermissionStore } from '@/stores/modules/permission'
import { listTenants, type Tenant } from '@/api/tenant'
defineOptions({ name: 'TyTenantSwitch' })
const auth = useAuthStore()
const permissionStore = usePermissionStore()
const tenants = ref<Tenant[]>([])
const currentTenantCode = computed(() => decodeTenantCode(auth.accessToken))
function decodeTenantCode(token: string | null): string {
if (!token) return ''
try {
const payload = token.split('.')[1]
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(payload.length / 4) * 4, '=')
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0))
const json = JSON.parse(new TextDecoder().decode(bytes))
return (json.tenantCode as string) ?? ''
} catch {
return ''
}
}
async function onCommand(tenantCode: string): Promise<void> {
if (!tenantCode || tenantCode === currentTenantCode.value) return
await auth.switchTenant(tenantCode)
window.location.reload()
}
onMounted(async () => {
const res = await listTenants({ page: 1, pageSize: 200 })
tenants.value = res.list
})
</script>
<style scoped lang="scss">
.ty-tenant-switch {
@apply inline-flex items-center justify-center w-[34px] h-[34px] border-none rounded-base bg-transparent text-[18px] text-text-regular cursor-pointer transition-colors duration-200;
&:hover {
@apply bg-bg-page;
color: var(--el-color-primary);
}
}
</style>

View File

@ -1,7 +1,7 @@
import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
import { http } from '@/api/http';
import { fetchBrand, login as loginApi, type LoginResult, type TenantBrand } from '@/api/auth';
import { fetchBrand, login as loginApi, switchTenant as switchTenantApi, type LoginResult, type TenantBrand } from '@/api/auth';
import { clearTokens, getAccessToken, getMustChangePassword, getRefreshToken, setMustChangePassword, setTokens } from '@/utils/token';
export const useAuthStore = defineStore('authStore', () => {
@ -16,6 +16,16 @@ export const useAuthStore = defineStore('authStore', () => {
brand.value = await fetchBrand();
}
/** 平台超管切换租户:换 token 后由调用方刷新页面重建上下文。 */
async function switchTenant(tenantCode: string): Promise<void> {
const data = await switchTenantApi(tenantCode);
setTokens(data.accessToken, data.refreshToken);
accessToken.value = data.accessToken;
refreshToken.value = data.refreshToken;
mustChangePassword.value = false;
setMustChangePassword(false);
}
async function login(username: string, password: string, tenantCode?: string): Promise<LoginResult> {
const data = await loginApi(username, password, tenantCode);
if ('accessToken' in data) {
@ -49,5 +59,5 @@ export const useAuthStore = defineStore('authStore', () => {
setMustChangePassword(false);
}
return { accessToken, refreshToken, isAuthenticated, mustChangePassword, brand, login, logout, reset, fetchBrandState };
return { accessToken, refreshToken, isAuthenticated, mustChangePassword, brand, login, logout, reset, fetchBrandState, switchTenant };
});