feat: 完善 ele 的请求、路由、百度统计、概览、登录
This commit is contained in:
@@ -2,24 +2,101 @@
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import type { AuthApi } from '#/api';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||
import { isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { sendSmsCode } from '#/api';
|
||||
import { getTenantByWebsite, getTenantSimpleList } from '#/api/core/auth';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const accessStore = useAccessStore();
|
||||
const tenantEnable = isTenantEnable();
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
const CODE_LENGTH = 4;
|
||||
|
||||
const loginRef = ref();
|
||||
|
||||
/** 获取租户列表,并默认选中 */
|
||||
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||
async function fetchTenantList() {
|
||||
if (!tenantEnable) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 获取租户列表、域名对应租户
|
||||
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||
tenantList.value = await getTenantSimpleList();
|
||||
|
||||
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||
let tenantId: null | number = null;
|
||||
const websiteTenant = await websiteTenantPromise;
|
||||
if (websiteTenant?.id) {
|
||||
tenantId = websiteTenant.id;
|
||||
}
|
||||
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||
if (!tenantId && accessStore.tenantId) {
|
||||
tenantId = accessStore.tenantId;
|
||||
}
|
||||
// 如果还是没有租户,使用列表中的第一个
|
||||
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||
tenantId = tenantList.value[0].id;
|
||||
}
|
||||
|
||||
// 设置选中的租户编号
|
||||
accessStore.setTenantId(tenantId);
|
||||
loginRef.value.getFormApi().setFieldValue('tenantId', tenantId?.toString());
|
||||
} catch (error) {
|
||||
console.error('获取租户列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(() => {
|
||||
fetchTenantList();
|
||||
});
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: tenantList.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id.toString(),
|
||||
})),
|
||||
placeholder: $t('authentication.tenantTip'),
|
||||
},
|
||||
fieldName: 'tenantId',
|
||||
label: $t('authentication.tenant'),
|
||||
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||
dependencies: {
|
||||
triggerFields: ['tenantId'],
|
||||
if: tenantEnable,
|
||||
trigger(values) {
|
||||
if (values.tenantId) {
|
||||
accessStore.setTenantId(Number(values.tenantId));
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.mobile'),
|
||||
},
|
||||
fieldName: 'phoneNumber',
|
||||
fieldName: 'mobile',
|
||||
label: $t('authentication.mobile'),
|
||||
rules: z
|
||||
.string()
|
||||
@@ -40,6 +117,29 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
return text;
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
handleSendCode: async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const formApi = loginRef.value?.getFormApi();
|
||||
if (!formApi) {
|
||||
throw new Error('表单未准备好');
|
||||
}
|
||||
// 验证手机号
|
||||
await formApi.validateField('mobile');
|
||||
const isMobileValid = await formApi.isFieldValid('mobile');
|
||||
if (!isMobileValid) {
|
||||
throw new Error('请输入有效的手机号码');
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
const { mobile } = await formApi.getValues();
|
||||
const scene = 21; // 场景:短信验证码登录
|
||||
await sendSmsCode({ mobile, scene });
|
||||
ElMessage.success('验证码发送成功');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
@@ -55,13 +155,17 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
* @param values 登录表单数据
|
||||
*/
|
||||
async function handleLogin(values: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(values);
|
||||
try {
|
||||
await authStore.authLogin('mobile', values);
|
||||
} catch (error) {
|
||||
console.error('Error in handleLogin:', error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationCodeLogin
|
||||
ref="loginRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleLogin"
|
||||
|
||||
@@ -2,40 +2,213 @@
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import type { AuthApi } from '#/api';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||
import { isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { sendSmsCode, smsResetPassword } from '#/api';
|
||||
import { getTenantByWebsite, getTenantSimpleList } from '#/api/core/auth';
|
||||
|
||||
defineOptions({ name: 'ForgetPassword' });
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const router = useRouter();
|
||||
const tenantEnable = isTenantEnable();
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 4;
|
||||
const forgetPasswordRef = ref();
|
||||
|
||||
/** 获取租户列表,并默认选中 */
|
||||
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||
async function fetchTenantList() {
|
||||
if (!tenantEnable) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 获取租户列表、域名对应租户
|
||||
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||
tenantList.value = await getTenantSimpleList();
|
||||
|
||||
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||
let tenantId: null | number = null;
|
||||
const websiteTenant = await websiteTenantPromise;
|
||||
if (websiteTenant?.id) {
|
||||
tenantId = websiteTenant.id;
|
||||
}
|
||||
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||
if (!tenantId && accessStore.tenantId) {
|
||||
tenantId = accessStore.tenantId;
|
||||
}
|
||||
// 如果还是没有租户,使用列表中的第一个
|
||||
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||
tenantId = tenantList.value[0].id;
|
||||
}
|
||||
|
||||
// 设置选中的租户编号
|
||||
accessStore.setTenantId(tenantId);
|
||||
forgetPasswordRef.value
|
||||
.getFormApi()
|
||||
.setFieldValue('tenantId', tenantId?.toString());
|
||||
} catch (error) {
|
||||
console.error('获取租户列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(() => {
|
||||
fetchTenantList();
|
||||
});
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: tenantList.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id.toString(),
|
||||
})),
|
||||
placeholder: $t('authentication.tenantTip'),
|
||||
},
|
||||
fieldName: 'tenantId',
|
||||
label: $t('authentication.tenant'),
|
||||
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||
dependencies: {
|
||||
triggerFields: ['tenantId'],
|
||||
if: tenantEnable,
|
||||
trigger(values) {
|
||||
if (values.tenantId) {
|
||||
accessStore.setTenantId(Number(values.tenantId));
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: 'example@example.com',
|
||||
placeholder: $t('authentication.mobile'),
|
||||
},
|
||||
fieldName: 'email',
|
||||
label: $t('authentication.email'),
|
||||
fieldName: 'mobile',
|
||||
label: $t('authentication.mobile'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.emailTip') })
|
||||
.email($t('authentication.emailValidErrorTip')),
|
||||
.min(1, { message: $t('authentication.mobileTip') })
|
||||
.refine((v) => /^\d{11}$/.test(v), {
|
||||
message: $t('authentication.mobileErrortip'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'VbenPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
? $t('authentication.sendText', [countdown])
|
||||
: $t('authentication.sendCode');
|
||||
return text;
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
handleSendCode: async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const formApi = forgetPasswordRef.value?.getFormApi();
|
||||
if (!formApi) {
|
||||
throw new Error('表单未准备好');
|
||||
}
|
||||
// 验证手机号
|
||||
await formApi.validateField('mobile');
|
||||
const isMobileValid = await formApi.isFieldValid('mobile');
|
||||
if (!isMobileValid) {
|
||||
throw new Error('请输入有效的手机号码');
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
const { mobile } = await formApi.getValues();
|
||||
const scene = 23; // 场景:重置密码
|
||||
await sendSmsCode({ mobile, scene });
|
||||
ElMessage.success('验证码发送成功');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
renderComponentContent() {
|
||||
return {
|
||||
strengthText: () => $t('authentication.passwordStrength'),
|
||||
};
|
||||
},
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.confirmPassword'),
|
||||
},
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
const { password } = values;
|
||||
return z
|
||||
.string({ required_error: $t('authentication.passwordTip') })
|
||||
.min(1, { message: $t('authentication.passwordTip') })
|
||||
.refine((value) => value === password, {
|
||||
message: $t('authentication.confirmPasswordTip'),
|
||||
});
|
||||
},
|
||||
triggerFields: ['password'],
|
||||
},
|
||||
fieldName: 'confirmPassword',
|
||||
label: $t('authentication.confirmPassword'),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit(value: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('reset email:', value);
|
||||
/**
|
||||
* 处理重置密码操作
|
||||
* @param values 表单数据
|
||||
*/
|
||||
async function handleSubmit(values: Recordable<any>) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { mobile, code, password } = values;
|
||||
await smsResetPassword({ mobile, code, password });
|
||||
ElMessage.success($t('authentication.resetPasswordSuccess'));
|
||||
// 重置成功后跳转到首页
|
||||
router.push('/');
|
||||
} catch (error) {
|
||||
console.error('重置密码失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationForgetPassword
|
||||
ref="forgetPasswordRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
|
||||
@@ -1,98 +1,192 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { BasicOption } from '@vben/types';
|
||||
|
||||
import { computed, markRaw } from 'vue';
|
||||
import type { AuthApi } from '#/api/core/auth';
|
||||
|
||||
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { AuthenticationLogin, Verification, z } from '@vben/common-ui';
|
||||
import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import {
|
||||
checkCaptcha,
|
||||
getCaptcha,
|
||||
getTenantByWebsite,
|
||||
getTenantSimpleList,
|
||||
socialAuthRedirect,
|
||||
} from '#/api/core/auth';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
|
||||
const { query } = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const accessStore = useAccessStore();
|
||||
const tenantEnable = isTenantEnable();
|
||||
const captchaEnable = isCaptchaEnable();
|
||||
|
||||
const MOCK_USER_OPTIONS: BasicOption[] = [
|
||||
{
|
||||
label: 'Super',
|
||||
value: 'vben',
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
value: 'jack',
|
||||
},
|
||||
];
|
||||
const loginRef = ref();
|
||||
const verifyRef = ref();
|
||||
|
||||
const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWord'
|
||||
|
||||
/** 获取租户列表,并默认选中 */
|
||||
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||
async function fetchTenantList() {
|
||||
if (!tenantEnable) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 获取租户列表、域名对应租户
|
||||
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||
tenantList.value = await getTenantSimpleList();
|
||||
|
||||
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||
let tenantId: null | number = null;
|
||||
const websiteTenant = await websiteTenantPromise;
|
||||
if (websiteTenant?.id) {
|
||||
tenantId = websiteTenant.id;
|
||||
}
|
||||
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||
if (!tenantId && accessStore.tenantId) {
|
||||
tenantId = accessStore.tenantId;
|
||||
}
|
||||
// 如果还是没有租户,使用列表中的第一个
|
||||
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||
tenantId = tenantList.value[0].id;
|
||||
}
|
||||
|
||||
// 设置选中的租户编号
|
||||
accessStore.setTenantId(tenantId);
|
||||
loginRef.value.getFormApi().setFieldValue('tenantId', tenantId?.toString());
|
||||
} catch (error) {
|
||||
console.error('获取租户列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理登录 */
|
||||
async function handleLogin(values: any) {
|
||||
// 如果开启验证码,则先验证验证码
|
||||
if (captchaEnable) {
|
||||
verifyRef.value.show();
|
||||
return;
|
||||
}
|
||||
// 无验证码,直接登录
|
||||
await authStore.authLogin('username', values);
|
||||
}
|
||||
|
||||
/** 验证码通过,执行登录 */
|
||||
async function handleVerifySuccess({ captchaVerification }: any) {
|
||||
try {
|
||||
await authStore.authLogin('username', {
|
||||
...(await loginRef.value.getFormApi().getValues()),
|
||||
captchaVerification,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleLogin:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理第三方登录 */
|
||||
const redirect = query?.redirect;
|
||||
async function handleThirdLogin(type: number) {
|
||||
if (type <= 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 计算 redirectUri
|
||||
// tricky: type、redirect 需要先 encode 一次,否则钉钉回调会丢失。配合 social-login.vue#getUrlValue() 使用
|
||||
const redirectUri = `${
|
||||
location.origin
|
||||
}/auth/social-login?${encodeURIComponent(
|
||||
`type=${type}&redirect=${redirect || '/'}`,
|
||||
)}`;
|
||||
|
||||
// 进行跳转
|
||||
window.location.href = await socialAuthRedirect(type, redirectUri);
|
||||
} catch (error) {
|
||||
console.error('第三方登录处理失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(() => {
|
||||
fetchTenantList();
|
||||
});
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: MOCK_USER_OPTIONS,
|
||||
placeholder: $t('authentication.selectAccount'),
|
||||
options: tenantList.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id.toString(),
|
||||
})),
|
||||
placeholder: $t('authentication.tenantTip'),
|
||||
},
|
||||
fieldName: 'tenantId',
|
||||
label: $t('authentication.tenant'),
|
||||
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||
dependencies: {
|
||||
triggerFields: ['tenantId'],
|
||||
if: tenantEnable,
|
||||
trigger(values) {
|
||||
if (values.tenantId) {
|
||||
accessStore.setTenantId(Number(values.tenantId));
|
||||
}
|
||||
},
|
||||
},
|
||||
fieldName: 'selectAccount',
|
||||
label: $t('authentication.selectAccount'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.selectAccount') })
|
||||
.optional()
|
||||
.default('vben'),
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
dependencies: {
|
||||
trigger(values, form) {
|
||||
if (values.selectAccount) {
|
||||
const findUser = MOCK_USER_OPTIONS.find(
|
||||
(item) => item.value === values.selectAccount,
|
||||
);
|
||||
if (findUser) {
|
||||
form.setValues({
|
||||
password: '123456',
|
||||
username: findUser.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
triggerFields: ['selectAccount'],
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.usernameTip') })
|
||||
.default(import.meta.env.VITE_APP_DEFAULT_USERNAME),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.password'),
|
||||
placeholder: $t('authentication.passwordTip'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
{
|
||||
component: markRaw(SliderCaptcha),
|
||||
fieldName: 'captcha',
|
||||
rules: z.boolean().refine((value) => value, {
|
||||
message: $t('authentication.verifyRequiredTip'),
|
||||
}),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.passwordTip') })
|
||||
.default(import.meta.env.VITE_APP_DEFAULT_PASSWORD),
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
@submit="authStore.authLogin"
|
||||
/>
|
||||
<div>
|
||||
<AuthenticationLogin
|
||||
ref="loginRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
@submit="handleLogin"
|
||||
@third-login="handleThirdLogin"
|
||||
/>
|
||||
<Verification
|
||||
ref="verifyRef"
|
||||
v-if="captchaEnable"
|
||||
:captcha-type="captchaType"
|
||||
:check-captcha-api="checkCaptcha"
|
||||
:get-captcha-api="getCaptcha"
|
||||
:img-size="{ width: '400px', height: '200px' }"
|
||||
mode="pop"
|
||||
@on-success="handleVerifySuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,18 +1,126 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, h, ref } from 'vue';
|
||||
import type { AuthApi } from '#/api/core/auth';
|
||||
|
||||
import { AuthenticationRegister, z } from '@vben/common-ui';
|
||||
import { computed, h, onMounted, ref } from 'vue';
|
||||
|
||||
import { AuthenticationRegister, Verification, z } from '@vben/common-ui';
|
||||
import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import {
|
||||
checkCaptcha,
|
||||
getCaptcha,
|
||||
getTenantByWebsite,
|
||||
getTenantSimpleList,
|
||||
} from '#/api/core/auth';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Register' });
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const authStore = useAuthStore();
|
||||
const tenantEnable = isTenantEnable();
|
||||
const captchaEnable = isCaptchaEnable();
|
||||
|
||||
const registerRef = ref();
|
||||
const verifyRef = ref();
|
||||
|
||||
const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWord'
|
||||
|
||||
/** 获取租户列表,并默认选中 */
|
||||
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||
async function fetchTenantList() {
|
||||
if (!tenantEnable) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 获取租户列表、域名对应租户
|
||||
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||
tenantList.value = await getTenantSimpleList();
|
||||
|
||||
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||
let tenantId: null | number = null;
|
||||
const websiteTenant = await websiteTenantPromise;
|
||||
if (websiteTenant?.id) {
|
||||
tenantId = websiteTenant.id;
|
||||
}
|
||||
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||
if (!tenantId && accessStore.tenantId) {
|
||||
tenantId = accessStore.tenantId;
|
||||
}
|
||||
// 如果还是没有租户,使用列表中的第一个
|
||||
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||
tenantId = tenantList.value[0].id;
|
||||
}
|
||||
|
||||
// 设置选中的租户编号
|
||||
accessStore.setTenantId(tenantId);
|
||||
registerRef.value
|
||||
.getFormApi()
|
||||
.setFieldValue('tenantId', tenantId?.toString());
|
||||
} catch (error) {
|
||||
console.error('获取租户列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 执行注册 */
|
||||
async function handleRegister(values: any) {
|
||||
// 如果开启验证码,则先验证验证码
|
||||
if (captchaEnable) {
|
||||
verifyRef.value.show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 无验证码,直接登录
|
||||
await authStore.authLogin('register', values);
|
||||
}
|
||||
|
||||
/** 验证码通过,执行注册 */
|
||||
const handleVerifySuccess = async ({ captchaVerification }: any) => {
|
||||
try {
|
||||
await authStore.authLogin('register', {
|
||||
...(await registerRef.value.getFormApi().getValues()),
|
||||
captchaVerification,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleRegister:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(() => {
|
||||
fetchTenantList();
|
||||
});
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: tenantList.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id.toString(),
|
||||
})),
|
||||
placeholder: $t('authentication.tenantTip'),
|
||||
},
|
||||
fieldName: 'tenantId',
|
||||
label: $t('authentication.tenant'),
|
||||
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||
dependencies: {
|
||||
triggerFields: ['tenantId'],
|
||||
if: tenantEnable,
|
||||
trigger(values) {
|
||||
if (values.tenantId) {
|
||||
accessStore.setTenantId(Number(values.tenantId));
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
@@ -22,6 +130,15 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.nicknameTip'),
|
||||
},
|
||||
fieldName: 'nickname',
|
||||
label: $t('authentication.nickname'),
|
||||
rules: z.string().min(1, { message: $t('authentication.nicknameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
@@ -80,17 +197,25 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit(value: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('register submit:', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationRegister
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
<div>
|
||||
<AuthenticationRegister
|
||||
ref="registerRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleRegister"
|
||||
/>
|
||||
<Verification
|
||||
ref="verifyRef"
|
||||
v-if="captchaEnable"
|
||||
:captcha-type="captchaType"
|
||||
:check-captcha-api="checkCaptcha"
|
||||
:get-captcha-api="getCaptcha"
|
||||
:img-size="{ width: '400px', height: '200px' }"
|
||||
mode="pop"
|
||||
@on-success="handleVerifySuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
215
apps/web-ele/src/views/_core/authentication/social-login.vue
Normal file
215
apps/web-ele/src/views/_core/authentication/social-login.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import type { AuthApi } from '#/api/core/auth';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { AuthenticationLogin, Verification, z } from '@vben/common-ui';
|
||||
import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import {
|
||||
checkCaptcha,
|
||||
getCaptcha,
|
||||
getTenantByWebsite,
|
||||
getTenantSimpleList,
|
||||
} from '#/api/core/auth';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'SocialLogin' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const accessStore = useAccessStore();
|
||||
const { query } = useRoute();
|
||||
const router = useRouter();
|
||||
const tenantEnable = isTenantEnable();
|
||||
const captchaEnable = isCaptchaEnable();
|
||||
|
||||
const loginRef = ref();
|
||||
const verifyRef = ref();
|
||||
|
||||
const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWord'
|
||||
|
||||
/** 获取租户列表,并默认选中 */
|
||||
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||
async function fetchTenantList() {
|
||||
if (!tenantEnable) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取租户列表、域名对应租户
|
||||
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||
tenantList.value = await getTenantSimpleList();
|
||||
|
||||
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||
let tenantId: null | number = null;
|
||||
const websiteTenant = await websiteTenantPromise;
|
||||
if (websiteTenant?.id) {
|
||||
tenantId = websiteTenant.id;
|
||||
}
|
||||
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||
if (!tenantId && accessStore.tenantId) {
|
||||
tenantId = accessStore.tenantId;
|
||||
}
|
||||
// 如果还是没有租户,使用列表中的第一个
|
||||
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||
tenantId = tenantList.value[0].id;
|
||||
}
|
||||
|
||||
// 设置选中的租户编号
|
||||
accessStore.setTenantId(tenantId);
|
||||
loginRef.value.getFormApi().setFieldValue('tenantId', tenantId);
|
||||
} catch (error) {
|
||||
console.error('获取租户列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 尝试登录:当账号已经绑定,socialLogin 会直接获得 token */
|
||||
const socialType = Number(getUrlValue('type'));
|
||||
const redirect = getUrlValue('redirect');
|
||||
const socialCode = query?.code as string;
|
||||
const socialState = query?.state as string;
|
||||
async function tryLogin() {
|
||||
// 用于登录后,基于 redirect 的重定向
|
||||
if (redirect) {
|
||||
await router.replace({
|
||||
query: {
|
||||
...query,
|
||||
redirect: encodeURIComponent(redirect),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 尝试登录
|
||||
await authStore.authLogin('social', {
|
||||
type: socialType,
|
||||
code: socialCode,
|
||||
state: socialState,
|
||||
});
|
||||
}
|
||||
|
||||
/** 处理登录 */
|
||||
async function handleLogin(values: any) {
|
||||
// 如果开启验证码,则先验证验证码
|
||||
if (captchaEnable) {
|
||||
verifyRef.value.show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 无验证码,直接登录
|
||||
await authStore.authLogin('username', {
|
||||
...values,
|
||||
socialType,
|
||||
socialCode,
|
||||
socialState,
|
||||
});
|
||||
}
|
||||
|
||||
/** 验证码通过,执行登录 */
|
||||
async function handleVerifySuccess({ captchaVerification }: any) {
|
||||
try {
|
||||
await authStore.authLogin('username', {
|
||||
...(await loginRef.value.getFormApi().getValues()),
|
||||
captchaVerification,
|
||||
socialType,
|
||||
socialCode,
|
||||
socialState,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleLogin:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** tricky: 配合 login.vue 中,redirectUri 需要对参数进行 encode,需要在回调后进行decode */
|
||||
function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href));
|
||||
return url.searchParams.get(key) ?? '';
|
||||
}
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(async () => {
|
||||
await fetchTenantList();
|
||||
|
||||
await tryLogin();
|
||||
});
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: tenantList.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id.toString(),
|
||||
})),
|
||||
placeholder: $t('authentication.tenantTip'),
|
||||
},
|
||||
fieldName: 'tenantId',
|
||||
label: $t('authentication.tenant'),
|
||||
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||
dependencies: {
|
||||
triggerFields: ['tenantId'],
|
||||
if: tenantEnable,
|
||||
trigger(values) {
|
||||
if (values.tenantId) {
|
||||
accessStore.setTenantId(Number(values.tenantId));
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.usernameTip') })
|
||||
.default(import.meta.env.VITE_APP_DEFAULT_USERNAME),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.passwordTip'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.passwordTip') })
|
||||
.default(import.meta.env.VITE_APP_DEFAULT_PASSWORD),
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AuthenticationLogin
|
||||
ref="loginRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
:show-code-login="false"
|
||||
:show-qrcode-login="false"
|
||||
:show-third-party-login="false"
|
||||
:show-register="false"
|
||||
@submit="handleLogin"
|
||||
/>
|
||||
<Verification
|
||||
ref="verifyRef"
|
||||
v-if="captchaEnable"
|
||||
:captcha-type="captchaType"
|
||||
:check-captcha-api="checkCaptcha"
|
||||
:get-captcha-api="getCaptcha"
|
||||
:img-size="{ width: '400px', height: '200px' }"
|
||||
mode="pop"
|
||||
@on-success="handleVerifySuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
221
apps/web-ele/src/views/_core/authentication/sso-login.vue
Normal file
221
apps/web-ele/src/views/_core/authentication/sso-login.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { AuthenticationAuthTitle, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { authorize, getAuthorize } from '#/api/system/oauth2/open';
|
||||
|
||||
defineOptions({ name: 'SSOLogin' });
|
||||
|
||||
const { query } = useRoute(); // 路由参数
|
||||
|
||||
const client = ref({
|
||||
name: '',
|
||||
logo: '',
|
||||
}); // 客户端信息
|
||||
|
||||
const queryParams = reactive({
|
||||
responseType: '',
|
||||
clientId: '',
|
||||
redirectUri: '',
|
||||
state: '',
|
||||
scopes: [] as string[], // 优先从 query 参数获取;如果未传递,从后端获取
|
||||
}); // URL 上的 client_id、scope 等参数
|
||||
|
||||
const loading = ref(false); // 表单是否提交中
|
||||
|
||||
/** 初始化授权信息 */
|
||||
async function init() {
|
||||
// 防止在没有登录的情况下循环弹窗
|
||||
if (query.client_id === undefined) {
|
||||
return;
|
||||
}
|
||||
// 解析参数
|
||||
// 例如说【自动授权不通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write
|
||||
// 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read
|
||||
queryParams.responseType = query.response_type as string;
|
||||
queryParams.clientId = query.client_id as string;
|
||||
queryParams.redirectUri = query.redirect_uri as string;
|
||||
queryParams.state = query.state as string;
|
||||
if (query.scope) {
|
||||
queryParams.scopes = (query.scope as string).split(' ');
|
||||
}
|
||||
|
||||
// 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。
|
||||
if (queryParams.scopes.length > 0) {
|
||||
const data = await doAuthorize(true, queryParams.scopes, []);
|
||||
if (data) {
|
||||
location.href = data;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 1.1 获取授权页的基本信息
|
||||
const data = await getAuthorize(queryParams.clientId);
|
||||
client.value = data.client;
|
||||
// 1.2 解析 scope
|
||||
let scopes;
|
||||
// 如果 params.scope 非空,则过滤下返回的 scopes
|
||||
if (queryParams.scopes.length > 0) {
|
||||
scopes = data.scopes.filter((scope) =>
|
||||
queryParams.scopes.includes(scope.key),
|
||||
);
|
||||
// 如果 params.scope 为空,则使用返回的 scopes 设置它
|
||||
} else {
|
||||
scopes = data.scopes;
|
||||
queryParams.scopes = scopes.map((scope) => scope.key);
|
||||
}
|
||||
|
||||
// 2.设置表单的初始值
|
||||
formApi.setFieldValue(
|
||||
'scopes',
|
||||
scopes.filter((scope) => scope.value).map((scope) => scope.key),
|
||||
);
|
||||
}
|
||||
|
||||
/** 处理授权的提交 */
|
||||
async function handleSubmit(approved: boolean) {
|
||||
// 计算 checkedScopes + uncheckedScopes
|
||||
let checkedScopes: string[];
|
||||
let uncheckedScopes: string[];
|
||||
if (approved) {
|
||||
// 同意授权,按照用户的选择
|
||||
const res = await formApi.getValues();
|
||||
checkedScopes = res.scopes;
|
||||
uncheckedScopes = queryParams.scopes.filter(
|
||||
(item) => !checkedScopes.includes(item),
|
||||
);
|
||||
} else {
|
||||
// 拒绝,则都是取消
|
||||
checkedScopes = [];
|
||||
uncheckedScopes = queryParams.scopes;
|
||||
}
|
||||
|
||||
// 提交授权的请求
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await doAuthorize(false, checkedScopes, uncheckedScopes);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
// 跳转授权成功后的回调地址
|
||||
location.href = data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 调用授权 API 接口 */
|
||||
const doAuthorize = (
|
||||
autoApprove: boolean,
|
||||
checkedScopes: string[],
|
||||
uncheckedScopes: string[],
|
||||
) => {
|
||||
return authorize(
|
||||
queryParams.responseType,
|
||||
queryParams.clientId,
|
||||
queryParams.redirectUri,
|
||||
queryParams.state,
|
||||
autoApprove,
|
||||
checkedScopes,
|
||||
uncheckedScopes,
|
||||
);
|
||||
};
|
||||
|
||||
/** 格式化 scope 文本 */
|
||||
function formatScope(scope: string) {
|
||||
// 格式化 scope 授权范围,方便用户理解。
|
||||
// 这里仅仅是一个 demo,可以考虑录入到字典数据中,例如说字典类型 "system_oauth2_scope",它的每个 scope 都是一条字典数据。
|
||||
switch (scope) {
|
||||
case 'user.read': {
|
||||
return '访问你的个人信息';
|
||||
}
|
||||
case 'user.write': {
|
||||
return '修改你的个人信息';
|
||||
}
|
||||
default: {
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
fieldName: 'scopes',
|
||||
label: '授权范围',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: queryParams.scopes.map((scope) => ({
|
||||
label: formatScope(scope),
|
||||
value: scope,
|
||||
})),
|
||||
class: 'flex flex-col gap-2',
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm(
|
||||
reactive({
|
||||
commonConfig: {
|
||||
hideLabel: true,
|
||||
hideRequiredMark: true,
|
||||
},
|
||||
schema: formSchema,
|
||||
showDefaultActions: false,
|
||||
}),
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @keydown.enter.prevent="handleSubmit(true)">
|
||||
<AuthenticationAuthTitle>
|
||||
<slot name="title">
|
||||
{{ `${client.name} 👋🏻` }}
|
||||
</slot>
|
||||
<template #desc>
|
||||
<span class="text-muted-foreground">
|
||||
此第三方应用请求获得以下权限:
|
||||
</span>
|
||||
</template>
|
||||
</AuthenticationAuthTitle>
|
||||
|
||||
<Form />
|
||||
|
||||
<div class="flex gap-2">
|
||||
<VbenButton
|
||||
:class="{
|
||||
'cursor-wait': loading,
|
||||
}"
|
||||
:loading="loading"
|
||||
aria-label="login"
|
||||
class="w-2/3"
|
||||
@click="handleSubmit(true)"
|
||||
>
|
||||
同意授权
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
:class="{
|
||||
'cursor-wait': loading,
|
||||
}"
|
||||
:loading="loading"
|
||||
aria-label="login"
|
||||
class="w-1/3"
|
||||
variant="outline"
|
||||
@click="handleSubmit(false)"
|
||||
>
|
||||
拒绝
|
||||
</VbenButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
7
apps/web-ele/src/views/_core/profile/index.vue
Normal file
7
apps/web-ele/src/views/_core/profile/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user