From 42b0a6a479575ae00672e5dfd7421b0eb74ad3dd Mon Sep 17 00:00:00 2001 From: liuyun Date: Sun, 11 Oct 2026 16:41:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E7=99=BB=E5=BD=95=E5=A4=9A?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E9=80=89=E6=8B=A9=E5=BC=B9=E7=AA=97=20+=20au?= =?UTF-8?q?th=20store=20=E7=99=BB=E5=BD=95=E7=AD=BE=E5=90=8D=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- src/api/auth.ts | 18 ++++++++++++ src/pages/login/index.vue | 56 ++++++++++++++++++++++++++++++++++++-- src/stores/modules/auth.ts | 18 ++++++------ 3 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/api/auth.ts b/src/api/auth.ts index 0443340..207365b 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,6 +1,24 @@ import { http } from '@/api/http' 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 { + return http.post('/auth/login', { username, password, ...(tenantCode ? { tenantCode } : {}) }) +} + /** 当前用户菜单树。 */ export function fetchMenus(): Promise { return http.get('/auth/menus') diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue index c237e97..666fefd 100644 --- a/src/pages/login/index.vue +++ b/src/pages/login/index.vue @@ -101,6 +101,20 @@ meta: + + + + @@ -136,6 +150,8 @@ const captcha = ref('') const captchaCode = ref('') const remember = ref(true) const error = ref('') +const tenantOptions = ref<{ tenantCode: string; tenantName: string }[]>([]) +const tenantDialogVisible = ref(false) const rootStyle = computed(() => (config.background ? { background: config.background } : {})) @@ -173,11 +189,16 @@ function refreshCaptcha(): void { refreshCaptcha() const { loading, run: doLogin } = useRequest( - () => auth.login(config.tenantCode, model.username, model.password), + () => auth.login(model.username, model.password), { manual: true, - onSuccess: () => { - router.push(config.homePath) + onSuccess: (data) => { + if (data && 'needSelectTenant' in data && data.needSelectTenant) { + tenantOptions.value = data.tenants + tenantDialogVisible.value = true + } else { + router.push(config.homePath) + } }, onError: (e) => { error.value = (e as Error).message @@ -185,6 +206,16 @@ const { loading, run: doLogin } = useRequest( }, ) +async function selectTenant(tenantCode: string): Promise { + 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 { if (loading.value) return error.value = '' @@ -355,6 +386,25 @@ function onRegister(): void { 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) ---------- */ .login__panel-head { @apply mb-lg text-center; diff --git a/src/stores/modules/auth.ts b/src/stores/modules/auth.ts index 1248423..0c5ee8f 100644 --- a/src/stores/modules/auth.ts +++ b/src/stores/modules/auth.ts @@ -1,6 +1,7 @@ import { computed, ref } from 'vue'; import { defineStore } from 'pinia'; import { http } from '@/api/http'; +import { login as loginApi, type LoginResult } from '@/api/auth'; import { clearTokens, getAccessToken, getRefreshToken, setTokens } from '@/utils/token'; export const useAuthStore = defineStore('authStore', () => { @@ -9,15 +10,14 @@ export const useAuthStore = defineStore('authStore', () => { const isAuthenticated = computed(() => !!accessToken.value); - async function login(tenantCode: string, username: string, password: string): Promise { - const data = await http.post<{ accessToken: string; refreshToken: string }>('/auth/login', { - tenantCode, - username, - password, - }); - setTokens(data.accessToken, data.refreshToken); - accessToken.value = data.accessToken; - refreshToken.value = data.refreshToken; + async function login(username: string, password: string, tenantCode?: string): Promise { + const data = await loginApi(username, password, tenantCode); + if ('accessToken' in data) { + setTokens(data.accessToken, data.refreshToken); + accessToken.value = data.accessToken; + refreshToken.value = data.refreshToken; + } + return data; } async function logout(): Promise {