Browse Source

feat: 实现扫码登录.

pull/1128/head
colin 1 year ago
parent
commit
ec502ee462
  1. 3
      apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json
  2. 3
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  3. 101
      apps/vben5/apps/app-antd/src/store/auth.ts
  4. 18
      apps/vben5/apps/app-antd/src/views/_core/authentication/qrcode-login.vue
  5. 1
      apps/vben5/packages/@abp/account/src/api/index.ts
  6. 72
      apps/vben5/packages/@abp/account/src/api/useQrCodeLoginApi.ts
  7. 178
      apps/vben5/packages/@abp/account/src/components/QrCodeLogin.vue
  8. 13
      apps/vben5/packages/@abp/account/src/components/components/LoginTitle.vue
  9. 1
      apps/vben5/packages/@abp/account/src/components/index.ts
  10. 27
      apps/vben5/packages/@abp/account/src/types/qrcode.ts
  11. 1
      apps/vben5/packages/@abp/core/src/types/global.ts

3
apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json

@ -8,6 +8,9 @@
"phoneNumber": "Phone Number",
"getCode": "Get Code",
"code": "Code"
},
"qrcodeLogin": {
"scaned": "Please confirm login on your phone."
}
},
"manage": {

3
apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json

@ -8,6 +8,9 @@
"phoneNumber": "手机号码",
"getCode": "获取验证码",
"code": "验证码"
},
"qrcodeLogin": {
"scaned": "请在手机上确认登录."
}
},
"manage": {

101
apps/vben5/apps/app-antd/src/store/auth.ts

@ -1,3 +1,5 @@
import type { TokenResult } from '@abp/account';
import type { Recordable, UserInfo } from '@vben/types';
import { ref } from 'vue';
@ -6,7 +8,7 @@ import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { useTokenApi, useUserInfoApi } from '@abp/account';
import { useQrCodeLoginApi, useTokenApi, useUserInfoApi } from '@abp/account';
import { Events, useAbpStore, useEventBus } from '@abp/core';
import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia';
@ -17,6 +19,7 @@ import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
const { publish } = useEventBus();
const { loginApi } = useTokenApi();
const { loginApi: qrcodeLoginApi } = useQrCodeLoginApi();
const { getUserInfoApi } = useUserInfoApi();
const { getConfigApi } = useAbpConfigApi();
const accessStore = useAccessStore();
@ -26,6 +29,14 @@ export const useAuthStore = defineStore('auth', () => {
const loginLoading = ref(false);
async function qrcodeLogin(
key: string,
onSuccess?: () => Promise<void> | void,
) {
const result = await qrcodeLoginApi(key);
return await _loginSuccess(result, onSuccess);
}
/**
*
* Asynchronously handle the login process
@ -35,46 +46,8 @@ export const useAuthStore = defineStore('auth', () => {
params: Recordable<any>,
onSuccess?: () => Promise<void> | void,
) {
// 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null;
try {
loginLoading.value = true;
const loginResult = await loginApi(params as any);
const { accessToken, tokenType, refreshToken } = loginResult;
// 如果成功获取到 accessToken
if (accessToken) {
accessStore.setAccessToken(`${tokenType} ${accessToken}`);
accessStore.setRefreshToken(refreshToken);
userInfo = await fetchUserInfo();
userStore.setUserInfo(userInfo);
publish(Events.UserLogin, userInfo);
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
onSuccess
? await onSuccess?.()
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
}
if (userInfo?.realName) {
notification.success({
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
duration: 3,
message: $t('authentication.loginSuccess'),
});
}
}
} finally {
loginLoading.value = false;
}
return {
userInfo,
};
const result = await loginApi(params as any);
return await _loginSuccess(result, onSuccess);
}
async function logout(redirect: boolean = true) {
@ -129,6 +102,51 @@ export const useAuthStore = defineStore('auth', () => {
return userInfo;
}
async function _loginSuccess(
loginResult: TokenResult,
onSuccess?: () => Promise<void> | void,
) {
// 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null;
try {
loginLoading.value = true;
const { accessToken, tokenType, refreshToken } = loginResult;
// 如果成功获取到 accessToken
if (accessToken) {
accessStore.setAccessToken(`${tokenType} ${accessToken}`);
accessStore.setRefreshToken(refreshToken);
userInfo = await fetchUserInfo();
userStore.setUserInfo(userInfo);
publish(Events.UserLogin, userInfo);
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
onSuccess
? await onSuccess?.()
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
}
if (userInfo?.realName) {
notification.success({
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
duration: 3,
message: $t('authentication.loginSuccess'),
});
}
}
} finally {
loginLoading.value = false;
}
return {
userInfo,
};
}
function $reset() {
loginLoading.value = false;
}
@ -136,6 +154,7 @@ export const useAuthStore = defineStore('auth', () => {
return {
$reset,
authLogin,
qrcodeLogin,
fetchUserInfo,
loginLoading,
logout,

18
apps/vben5/apps/app-antd/src/views/_core/authentication/qrcode-login.vue

@ -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
apps/vben5/packages/@abp/account/src/api/index.ts

@ -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';

72
apps/vben5/packages/@abp/account/src/api/useQrCodeLoginApi.ts

@ -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,
};
}

178
apps/vben5/packages/@abp/account/src/components/QrCodeLogin.vue

@ -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>

13
apps/vben5/packages/@abp/account/src/components/components/LoginTitle.vue

@ -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
apps/vben5/packages/@abp/account/src/components/index.ts

@ -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';

27
apps/vben5/packages/@abp/account/src/types/qrcode.ts

@ -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 };

1
apps/vben5/packages/@abp/core/src/types/global.ts

@ -43,6 +43,7 @@ interface IHasExtraProperties {
}
/** 选择项 */
interface ISelectionStringValueItem {
[key: string]: any;
/** 选择项显示文本多语言对象 */
displayText: LocalizableStringInfo;
/** 选择项值 */

Loading…
Cancel
Save