feat(auth): 登录多租户选择弹窗 + auth store 登录签名调整
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b03e9ef62b
commit
42b0a6a479
@ -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<LoginResult> {
|
||||
return http.post<LoginResult>('/auth/login', { username, password, ...(tenantCode ? { tenantCode } : {}) })
|
||||
}
|
||||
|
||||
/** 当前用户菜单树。 */
|
||||
export function fetchMenus(): Promise<ResourceNode[]> {
|
||||
return http.get<ResourceNode[]>('/auth/menus')
|
||||
|
||||
@ -101,6 +101,20 @@ meta:
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@ -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<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> {
|
||||
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;
|
||||
|
||||
@ -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<void> {
|
||||
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<LoginResult> {
|
||||
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<void> {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user