feat(auth): 登录多租户选择弹窗 + auth store 登录签名调整

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 16:41:26 +08:00
parent b03e9ef62b
commit 42b0a6a479
3 changed files with 80 additions and 12 deletions

View File

@ -1,6 +1,24 @@
import { http } from '@/api/http' import { http } from '@/api/http'
import type { ResourceNode } from '@/types/resource' import type { ResourceNode } from '@/types/resource'
export interface LoginSuccess {
accessToken: string
refreshToken: string
expiresIn: number
}
export interface LoginNeedTenant {
needSelectTenant: true
tenants: { tenantCode: string; tenantName: string }[]
}
export type LoginResult = LoginSuccess | LoginNeedTenant
/** 登录:缺省 tenantCode 时按用户名自动探测;多租户返回 needSelectTenant。 */
export function login(username: string, password: string, tenantCode?: string): Promise<LoginResult> {
return http.post<LoginResult>('/auth/login', { username, password, ...(tenantCode ? { tenantCode } : {}) })
}
/** 当前用户菜单树。 */ /** 当前用户菜单树。 */
export function fetchMenus(): Promise<ResourceNode[]> { export function fetchMenus(): Promise<ResourceNode[]> {
return http.get<ResourceNode[]>('/auth/menus') return http.get<ResourceNode[]>('/auth/menus')

View File

@ -101,6 +101,20 @@ meta:
</div> </div>
</div> </div>
</div> </div>
<el-dialog v-model="tenantDialogVisible" title="选择登录租户" width="360px" :close-on-click-modal="false">
<div class="login__tenants">
<div
v-for="t in tenantOptions"
:key="t.tenantCode"
class="login__tenant-item"
@click="selectTenant(t.tenantCode)"
>
<span>{{ t.tenantName }}</span>
<span class="login__tenant-code">{{ t.tenantCode }}</span>
</div>
</div>
</el-dialog>
</div> </div>
</template> </template>
@ -136,6 +150,8 @@ const captcha = ref('')
const captchaCode = ref('') const captchaCode = ref('')
const remember = ref(true) const remember = ref(true)
const error = ref('') const error = ref('')
const tenantOptions = ref<{ tenantCode: string; tenantName: string }[]>([])
const tenantDialogVisible = ref(false)
const rootStyle = computed(() => (config.background ? { background: config.background } : {})) const rootStyle = computed(() => (config.background ? { background: config.background } : {}))
@ -173,11 +189,16 @@ function refreshCaptcha(): void {
refreshCaptcha() refreshCaptcha()
const { loading, run: doLogin } = useRequest( const { loading, run: doLogin } = useRequest(
() => auth.login(config.tenantCode, model.username, model.password), () => auth.login(model.username, model.password),
{ {
manual: true, manual: true,
onSuccess: () => { onSuccess: (data) => {
router.push(config.homePath) if (data && 'needSelectTenant' in data && data.needSelectTenant) {
tenantOptions.value = data.tenants
tenantDialogVisible.value = true
} else {
router.push(config.homePath)
}
}, },
onError: (e) => { onError: (e) => {
error.value = (e as Error).message error.value = (e as Error).message
@ -185,6 +206,16 @@ const { loading, run: doLogin } = useRequest(
}, },
) )
async function selectTenant(tenantCode: string): Promise<void> {
tenantDialogVisible.value = false
try {
await auth.login(model.username, model.password, tenantCode)
router.push(config.homePath)
} catch (e) {
error.value = (e as Error).message
}
}
async function onSubmit(): Promise<void> { async function onSubmit(): Promise<void> {
if (loading.value) return if (loading.value) return
error.value = '' error.value = ''
@ -355,6 +386,25 @@ function onRegister(): void {
color: var(--el-color-danger); color: var(--el-color-danger);
} }
/* ---------- 租户选择 ---------- */
.login__tenants {
@apply flex flex-col gap-2;
}
.login__tenant-item {
@apply flex cursor-pointer items-center justify-between rounded-base border border-solid px-4 py-3 transition-colors;
border-color: var(--wm-border-color-light);
&:hover {
@apply border-primary;
background: var(--el-color-primary-light-9);
}
}
.login__tenant-code {
@apply text-xs text-text-secondary;
}
/* ---------- 卡片布局(card) ---------- */ /* ---------- 卡片布局(card) ---------- */
.login__panel-head { .login__panel-head {
@apply mb-lg text-center; @apply mb-lg text-center;

View File

@ -1,6 +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 { login as loginApi, type LoginResult } from '@/api/auth';
import { clearTokens, getAccessToken, getRefreshToken, setTokens } from '@/utils/token'; import { clearTokens, getAccessToken, getRefreshToken, setTokens } from '@/utils/token';
export const useAuthStore = defineStore('authStore', () => { export const useAuthStore = defineStore('authStore', () => {
@ -9,15 +10,14 @@ export const useAuthStore = defineStore('authStore', () => {
const isAuthenticated = computed(() => !!accessToken.value); const isAuthenticated = computed(() => !!accessToken.value);
async function login(tenantCode: string, username: string, password: string): Promise<void> { async function login(username: string, password: string, tenantCode?: string): Promise<LoginResult> {
const data = await http.post<{ accessToken: string; refreshToken: string }>('/auth/login', { const data = await loginApi(username, password, tenantCode);
tenantCode, if ('accessToken' in data) {
username, setTokens(data.accessToken, data.refreshToken);
password, accessToken.value = data.accessToken;
}); refreshToken.value = data.refreshToken;
setTokens(data.accessToken, data.refreshToken); }
accessToken.value = data.accessToken; return data;
refreshToken.value = data.refreshToken;
} }
async function logout(): Promise<void> { async function logout(): Promise<void> {