committed by
GitHub
11 changed files with 375 additions and 43 deletions
@ -1,10 +1,24 @@ |
|||||
<script lang="ts" setup> |
<script lang="ts" setup> |
||||
import { AuthenticationQrCodeLogin } from '@vben/common-ui'; |
|
||||
import { LOGIN_PATH } from '@vben/constants'; |
import { LOGIN_PATH } from '@vben/constants'; |
||||
|
import { preferences } from '@vben/preferences'; |
||||
|
|
||||
|
import { QrCodeLogin } from '@abp/account'; |
||||
|
|
||||
|
import { useAuthStore } from '#/store'; |
||||
|
|
||||
defineOptions({ name: 'QrCodeLogin' }); |
defineOptions({ name: 'QrCodeLogin' }); |
||||
|
|
||||
|
const authStore = useAuthStore(); |
||||
|
|
||||
|
async function onConfirm(key: string) { |
||||
|
await authStore.qrcodeLogin(key); |
||||
|
} |
||||
</script> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" /> |
<QrCodeLogin |
||||
|
:login-path="LOGIN_PATH" |
||||
|
@confirm="onConfirm" |
||||
|
:default-avatar="preferences.app.defaultAvatar" |
||||
|
/> |
||||
</template> |
</template> |
||||
|
|||||
@ -1,5 +1,6 @@ |
|||||
export { useAccountApi } from './useAccountApi'; |
export { useAccountApi } from './useAccountApi'; |
||||
export { useMySessionApi } from './useMySessionApi'; |
export { useMySessionApi } from './useMySessionApi'; |
||||
export { useProfileApi } from './useProfileApi'; |
export { useProfileApi } from './useProfileApi'; |
||||
|
export { useQrCodeLoginApi } from './useQrCodeLoginApi'; |
||||
export { useTokenApi } from './useTokenApi'; |
export { useTokenApi } from './useTokenApi'; |
||||
export { useUserInfoApi } from './useUserInfoApi'; |
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 MyProfile } from './MyProfile.vue'; |
||||
export { default as MySetting } from './MySetting.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