diff --git a/apps/web-naive/src/layouts/basic.vue b/apps/web-naive/src/layouts/basic.vue
index 0e9747bef..256868f4a 100644
--- a/apps/web-naive/src/layouts/basic.vue
+++ b/apps/web-naive/src/layouts/basic.vue
@@ -1,66 +1,68 @@
([]); // 租户列表
+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 });
+ message.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) {
- // eslint-disable-next-line no-console
- console.log('reset email:', value);
+/**
+ * 处理重置密码操作
+ * @param values 表单数据
+ */
+async function handleSubmit(values: Recordable) {
+ loading.value = true;
+ try {
+ const { mobile, code, password } = values;
+ await smsResetPassword({ mobile, code, password });
+ message.success($t('authentication.resetPasswordSuccess'));
+ // 重置成功后跳转到首页
+ router.push('/');
+ } catch (error) {
+ console.error('重置密码失败:', error);
+ } finally {
+ loading.value = false;
+ }
}
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([]); // 租户列表
+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),
},
];
});
-
+
diff --git a/apps/web-naive/src/views/_core/authentication/register.vue b/apps/web-naive/src/views/_core/authentication/register.vue
index daf89c447..734f4c27d 100644
--- a/apps/web-naive/src/views/_core/authentication/register.vue
+++ b/apps/web-naive/src/views/_core/authentication/register.vue
@@ -1,18 +1,126 @@
-
+
diff --git a/apps/web-naive/src/views/_core/authentication/social-login.vue b/apps/web-naive/src/views/_core/authentication/social-login.vue
new file mode 100644
index 000000000..1052a9a18
--- /dev/null
+++ b/apps/web-naive/src/views/_core/authentication/social-login.vue
@@ -0,0 +1,210 @@
+
+
+
+
+
diff --git a/apps/web-naive/src/views/_core/authentication/sso-login.vue b/apps/web-naive/src/views/_core/authentication/sso-login.vue
new file mode 100644
index 000000000..1bc7ad9b1
--- /dev/null
+++ b/apps/web-naive/src/views/_core/authentication/sso-login.vue
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+ {{ `${client.name} 👋🏻` }}
+
+
+
+ 此第三方应用请求获得以下权限:
+
+
+
+
+
+
+
+
+ 同意授权
+
+
+ 拒绝
+
+
+
+