feat(auth): 顶栏租户切换入口(仅超管显示)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
348221d198
commit
da7a5b703e
2
components.d.ts
vendored
2
components.d.ts
vendored
@ -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']
|
||||
}
|
||||
@ -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 })
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
|
||||
64
src/components/ty-tenant-switch.vue
Normal file
64
src/components/ty-tenant-switch.vue
Normal 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>
|
||||
@ -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 };
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user