committed by
GitHub
11 changed files with 375 additions and 43 deletions
@ -1,10 +1,24 @@ |
|||
<script lang="ts" setup> |
|||
import { AuthenticationQrCodeLogin } from '@vben/common-ui'; |
|||
import { LOGIN_PATH } from '@vben/constants'; |
|||
import { preferences } from '@vben/preferences'; |
|||
|
|||
import { QrCodeLogin } from '@abp/account'; |
|||
|
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
defineOptions({ name: 'QrCodeLogin' }); |
|||
|
|||
const authStore = useAuthStore(); |
|||
|
|||
async function onConfirm(key: string) { |
|||
await authStore.qrcodeLogin(key); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" /> |
|||
<QrCodeLogin |
|||
:login-path="LOGIN_PATH" |
|||
@confirm="onConfirm" |
|||
:default-avatar="preferences.app.defaultAvatar" |
|||
/> |
|||
</template> |
|||
|
|||
@ -1,5 +1,6 @@ |
|||
export { useAccountApi } from './useAccountApi'; |
|||
export { useMySessionApi } from './useMySessionApi'; |
|||
export { useProfileApi } from './useProfileApi'; |
|||
export { useQrCodeLoginApi } from './useQrCodeLoginApi'; |
|||
export { useTokenApi } from './useTokenApi'; |
|||
export { useUserInfoApi } from './useUserInfoApi'; |
|||
|
|||
@ -0,0 +1,72 @@ |
|||
import type { |
|||
GenerateQrCodeResult, |
|||
QrCodeUserInfoResult, |
|||
} from '../types/qrcode'; |
|||
import type { OAuthTokenResult } from '../types/token'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useQrCodeLoginApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 生成登录二维码 |
|||
* @returns 二维码信息 |
|||
*/ |
|||
function generateApi(): Promise<GenerateQrCodeResult> { |
|||
return request<GenerateQrCodeResult>('/api/account/qrcode/generate', { |
|||
method: 'POST', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 检查二维码状态 |
|||
* @param key 二维码Key |
|||
* @returns 二维码信息 |
|||
*/ |
|||
function checkCodeApi(key: string): Promise<QrCodeUserInfoResult> { |
|||
return request<QrCodeUserInfoResult>(`/api/account/qrcode/${key}/check`, { |
|||
method: 'GET', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 二维码登录 |
|||
* @param key 二维码Key |
|||
* @returns 用户token |
|||
*/ |
|||
async function loginApi(key: string) { |
|||
const { audience, clientId, clientSecret } = useAppConfig( |
|||
import.meta.env, |
|||
import.meta.env.PROD, |
|||
); |
|||
const result = await request<OAuthTokenResult>('/connect/token', { |
|||
data: { |
|||
client_id: clientId, |
|||
client_secret: clientSecret, |
|||
grant_type: 'qr_code', |
|||
qrcode_key: key, |
|||
scope: audience, |
|||
}, |
|||
headers: { |
|||
'Content-Type': 'application/x-www-form-urlencoded', |
|||
}, |
|||
method: 'POST', |
|||
}); |
|||
return { |
|||
accessToken: result.access_token, |
|||
expiresIn: result.expires_in, |
|||
refreshToken: result.refresh_token, |
|||
tokenType: result.token_type, |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
checkCodeApi, |
|||
generateApi, |
|||
loginApi, |
|||
}; |
|||
} |
|||
@ -0,0 +1,178 @@ |
|||
<script setup lang="ts"> |
|||
import type { QrCodeUserInfoResult } from '../types/qrcode'; |
|||
|
|||
import { computed, onMounted, onUnmounted, ref } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { VbenButton } from '@vben-core/shadcn-ui'; |
|||
|
|||
import { useQRCode } from '@vueuse/integrations/useQRCode'; |
|||
import { Spin } from 'ant-design-vue'; |
|||
|
|||
import { useQrCodeLoginApi } from '../api'; |
|||
import { QrCodeStatus } from '../types/qrcode'; |
|||
import Title from './components/LoginTitle.vue'; |
|||
|
|||
interface Props { |
|||
/** |
|||
* @zh_CN 默认头像 |
|||
*/ |
|||
defaultAvatar?: string; |
|||
/** |
|||
* @zh_CN 描述 |
|||
*/ |
|||
description?: string; |
|||
/** |
|||
* @zh_CN 是否处于加载处理状态 |
|||
*/ |
|||
loading?: boolean; |
|||
/** |
|||
* @zh_CN 登录路径 |
|||
*/ |
|||
loginPath?: string; |
|||
/** |
|||
* @zh_CN 按钮文本 |
|||
*/ |
|||
submitButtonText?: string; |
|||
/** |
|||
* @zh_CN 描述 |
|||
*/ |
|||
subTitle?: string; |
|||
/** |
|||
* @zh_CN 标题 |
|||
*/ |
|||
title?: string; |
|||
} |
|||
|
|||
defineOptions({ |
|||
name: 'AccountQrCodeLogin', |
|||
}); |
|||
|
|||
const props = withDefaults(defineProps<Props>(), { |
|||
defaultAvatar: '', |
|||
description: '', |
|||
loading: false, |
|||
loginPath: '/auth/login', |
|||
submitButtonText: '', |
|||
subTitle: '', |
|||
title: '', |
|||
}); |
|||
|
|||
const emits = defineEmits<{ |
|||
(event: 'confirm', key: string): void; |
|||
}>(); |
|||
|
|||
let interval: NodeJS.Timeout; |
|||
const router = useRouter(); |
|||
const { checkCodeApi, generateApi } = useQrCodeLoginApi(); |
|||
|
|||
const qrcodeInfo = ref<QrCodeUserInfoResult>({ |
|||
key: '', |
|||
status: QrCodeStatus.Invalid, |
|||
}); |
|||
|
|||
const getQrCodeUrl = computed(() => { |
|||
return `QRCODE_LOGIN:${qrcodeInfo.value.key}`; |
|||
}); |
|||
const getScanedQrCode = computed(() => { |
|||
return ( |
|||
qrcodeInfo.value.status === QrCodeStatus.Confirmed || |
|||
qrcodeInfo.value.status === QrCodeStatus.Scaned |
|||
); |
|||
}); |
|||
|
|||
const qrcode = useQRCode(getQrCodeUrl, { |
|||
errorCorrectionLevel: 'H', |
|||
margin: 4, |
|||
}); |
|||
|
|||
function goToLogin() { |
|||
router.push(props.loginPath); |
|||
} |
|||
|
|||
async function onInit() { |
|||
const loginCode = localStorage.getItem('login_qrocde'); |
|||
if (loginCode) { |
|||
qrcodeInfo.value = { |
|||
key: loginCode, |
|||
status: QrCodeStatus.Created, |
|||
}; |
|||
} else { |
|||
const result = await generateApi(); |
|||
qrcodeInfo.value = { |
|||
key: result.key, |
|||
status: QrCodeStatus.Invalid, |
|||
}; |
|||
localStorage.setItem('login_qrocde', result.key); |
|||
} |
|||
await onCheckCode(); |
|||
interval = setInterval(onCheckCode, 5000); |
|||
} |
|||
|
|||
async function onCheckCode() { |
|||
const result = await checkCodeApi(qrcodeInfo.value.key); |
|||
if (result.status === QrCodeStatus.Invalid) { |
|||
localStorage.removeItem('login_qrocde'); |
|||
interval && clearInterval(interval); |
|||
await onInit(); |
|||
return; |
|||
} |
|||
qrcodeInfo.value = result; |
|||
// 已确认登录 |
|||
if (result.status === QrCodeStatus.Confirmed) { |
|||
interval && clearInterval(interval); |
|||
localStorage.removeItem('login_qrocde'); |
|||
// 登录 |
|||
emits('confirm', result.key); |
|||
} |
|||
} |
|||
|
|||
onMounted(onInit); |
|||
|
|||
onUnmounted(() => { |
|||
interval && clearInterval(interval); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<div> |
|||
<Title> |
|||
<slot name="title"> |
|||
{{ title || $t('authentication.welcomeBack') }} 📱 |
|||
</slot> |
|||
<template #desc> |
|||
<span class="text-muted-foreground"> |
|||
<slot name="subTitle"> |
|||
{{ subTitle || $t('authentication.qrcodeSubtitle') }} |
|||
</slot> |
|||
</span> |
|||
</template> |
|||
</Title> |
|||
|
|||
<div class="flex-col-center mt-6"> |
|||
<template v-if="!getScanedQrCode"> |
|||
<img :src="qrcode" alt="qrcode" class="w-1/2" /> |
|||
<p class="text-muted-foreground mt-4 text-sm"> |
|||
<slot name="description"> |
|||
{{ description || $t('authentication.qrcodePrompt') }} |
|||
</slot> |
|||
</p> |
|||
</template> |
|||
<Spin v-else :tip="$t('abp.oauth.qrcodeLogin.scaned')"> |
|||
<div class="flex-row-center justify-items-center"> |
|||
<img |
|||
:src="qrcodeInfo.picture ?? defaultAvatar" |
|||
alt="qrcode" |
|||
class="w-1/2" |
|||
/> |
|||
</div> |
|||
</Spin> |
|||
</div> |
|||
|
|||
<VbenButton class="mt-4 w-full" variant="outline" @click="goToLogin()"> |
|||
{{ $t('common.back') }} |
|||
</VbenButton> |
|||
</div> |
|||
</template> |
|||
@ -0,0 +1,13 @@ |
|||
<template> |
|||
<div class="mb-7 sm:mx-auto sm:w-full sm:max-w-md"> |
|||
<h2 |
|||
class="text-foreground mb-3 text-3xl font-bold leading-9 tracking-tight lg:text-4xl" |
|||
> |
|||
<slot></slot> |
|||
</h2> |
|||
|
|||
<p class="text-muted-foreground lg:text-md text-sm"> |
|||
<slot name="desc"></slot> |
|||
</p> |
|||
</div> |
|||
</template> |
|||
@ -1,2 +1,3 @@ |
|||
export { default as MyProfile } from './MyProfile.vue'; |
|||
export { default as MySetting } from './MySetting.vue'; |
|||
export { default as QrCodeLogin } from './QrCodeLogin.vue'; |
|||
|
|||
@ -0,0 +1,27 @@ |
|||
export enum QrCodeStatus { |
|||
/** 已确认 */ |
|||
Confirmed = 10, |
|||
/** 创建 */ |
|||
Created = 0, |
|||
/** 无效 */ |
|||
Invalid = -1, |
|||
/** 已扫描 */ |
|||
Scaned = 5, |
|||
} |
|||
|
|||
interface GenerateQrCodeResult { |
|||
key: string; |
|||
} |
|||
|
|||
interface QrCodeInfoResult { |
|||
key: string; |
|||
status: QrCodeStatus; |
|||
} |
|||
|
|||
interface QrCodeUserInfoResult extends QrCodeInfoResult { |
|||
picture?: string; |
|||
userId?: string; |
|||
userName?: string; |
|||
} |
|||
|
|||
export type { GenerateQrCodeResult, QrCodeUserInfoResult }; |
|||
Loading…
Reference in new issue