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']
|
TySidebar: typeof import('./src/components/ty-sidebar.vue')['default']
|
||||||
TyTable: typeof import('./src/components/ty-table.vue')['default']
|
TyTable: typeof import('./src/components/ty-table.vue')['default']
|
||||||
TyTabs: typeof import('./src/components/ty-tabs.vue')['default']
|
TyTabs: typeof import('./src/components/ty-tabs.vue')['default']
|
||||||
|
TyTenantSwitch: typeof import('./src/components/ty-tenant-switch.vue')['default']
|
||||||
}
|
}
|
||||||
export interface GlobalDirectives {
|
export interface GlobalDirectives {
|
||||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
@ -132,4 +133,5 @@ declare global {
|
|||||||
const TySidebar: typeof import('./src/components/ty-sidebar.vue')['default']
|
const TySidebar: typeof import('./src/components/ty-sidebar.vue')['default']
|
||||||
const TyTable: typeof import('./src/components/ty-table.vue')['default']
|
const TyTable: typeof import('./src/components/ty-table.vue')['default']
|
||||||
const TyTabs: typeof import('./src/components/ty-tabs.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> {
|
export function fetchBrand(signal?: AbortSignal): Promise<TenantBrand> {
|
||||||
return http.get<TenantBrand>('/auth/brand', { signal })
|
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" />
|
<icon type="iconify" icon="mdi:cog-outline" />
|
||||||
</button>
|
</button>
|
||||||
<ty-notice />
|
<ty-notice />
|
||||||
|
<ty-tenant-switch />
|
||||||
<el-dropdown trigger="click" @command="onUserCommand">
|
<el-dropdown trigger="click" @command="onUserCommand">
|
||||||
<span class="ty-header__user">
|
<span class="ty-header__user">
|
||||||
<icon type="iconify" icon="mdi:account-circle" />
|
<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 { computed, ref } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { http } from '@/api/http';
|
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';
|
import { clearTokens, getAccessToken, getMustChangePassword, getRefreshToken, setMustChangePassword, setTokens } from '@/utils/token';
|
||||||
|
|
||||||
export const useAuthStore = defineStore('authStore', () => {
|
export const useAuthStore = defineStore('authStore', () => {
|
||||||
@ -16,6 +16,16 @@ export const useAuthStore = defineStore('authStore', () => {
|
|||||||
brand.value = await fetchBrand();
|
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> {
|
async function login(username: string, password: string, tenantCode?: string): Promise<LoginResult> {
|
||||||
const data = await loginApi(username, password, tenantCode);
|
const data = await loginApi(username, password, tenantCode);
|
||||||
if ('accessToken' in data) {
|
if ('accessToken' in data) {
|
||||||
@ -49,5 +59,5 @@ export const useAuthStore = defineStore('authStore', () => {
|
|||||||
setMustChangePassword(false);
|
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