feat: init naive base
This commit is contained in:
@@ -1,66 +1,68 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { NotificationItem } from '@vben/layouts';
|
import type { NotificationItem } from '@vben/layouts';
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import type { SystemTenantApi } from '#/api/system/tenant';
|
||||||
|
|
||||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
|
import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui';
|
||||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||||
import { useWatermark } from '@vben/hooks';
|
import { isTenantEnable, useTabs, useWatermark } from '@vben/hooks';
|
||||||
import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
|
import {
|
||||||
|
AntdProfileOutlined,
|
||||||
|
BookOpenText,
|
||||||
|
CircleHelp,
|
||||||
|
SvgGithubIcon,
|
||||||
|
} from '@vben/icons';
|
||||||
import {
|
import {
|
||||||
BasicLayout,
|
BasicLayout,
|
||||||
|
Help,
|
||||||
LockScreen,
|
LockScreen,
|
||||||
Notification,
|
Notification,
|
||||||
|
TenantDropdown,
|
||||||
UserDropdown,
|
UserDropdown,
|
||||||
} from '@vben/layouts';
|
} from '@vben/layouts';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
import { openWindow } from '@vben/utils';
|
import { formatDateTime, openWindow } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import {
|
||||||
|
getUnreadNotifyMessageCount,
|
||||||
|
getUnreadNotifyMessageList,
|
||||||
|
updateAllNotifyMessageRead,
|
||||||
|
updateNotifyMessageRead,
|
||||||
|
} from '#/api/system/notify/message';
|
||||||
|
import { getSimpleTenantList } from '#/api/system/tenant';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import { router } from '#/router';
|
||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
import LoginForm from '#/views/_core/authentication/login.vue';
|
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||||
|
|
||||||
const notifications = ref<NotificationItem[]>([
|
|
||||||
{
|
|
||||||
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
|
||||||
date: '3小时前',
|
|
||||||
isRead: true,
|
|
||||||
message: '描述信息描述信息描述信息',
|
|
||||||
title: '收到了 14 份新周报',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'https://avatar.vercel.sh/1',
|
|
||||||
date: '刚刚',
|
|
||||||
isRead: false,
|
|
||||||
message: '描述信息描述信息描述信息',
|
|
||||||
title: '朱偏右 回复了你',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'https://avatar.vercel.sh/1',
|
|
||||||
date: '2024-01-01',
|
|
||||||
isRead: false,
|
|
||||||
message: '描述信息描述信息描述信息',
|
|
||||||
title: '曲丽丽 评论了你',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'https://avatar.vercel.sh/satori',
|
|
||||||
date: '1天前',
|
|
||||||
isRead: false,
|
|
||||||
message: '描述信息描述信息描述信息',
|
|
||||||
title: '代办提醒',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
const { destroyWatermark, updateWatermark } = useWatermark();
|
const { destroyWatermark, updateWatermark } = useWatermark();
|
||||||
const showDot = computed(() =>
|
const { closeOtherTabs, refreshTab } = useTabs();
|
||||||
notifications.value.some((item) => !item.isRead),
|
|
||||||
);
|
const notifications = ref<NotificationItem[]>([]);
|
||||||
|
const unreadCount = ref(0);
|
||||||
|
const showDot = computed(() => unreadCount.value > 0);
|
||||||
|
|
||||||
|
const [HelpModal, helpModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Help,
|
||||||
|
});
|
||||||
|
|
||||||
const menus = computed(() => [
|
const menus = computed(() => [
|
||||||
|
{
|
||||||
|
handler: () => {
|
||||||
|
router.push({ name: 'Profile' });
|
||||||
|
},
|
||||||
|
icon: AntdProfileOutlined,
|
||||||
|
text: $t('ui.widgets.profile'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
handler: () => {
|
handler: () => {
|
||||||
openWindow(VBEN_DOC_URL, {
|
openWindow(VBEN_DOC_URL, {
|
||||||
@@ -81,9 +83,7 @@ const menus = computed(() => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
handler: () => {
|
handler: () => {
|
||||||
openWindow(`${VBEN_GITHUB_URL}/issues`, {
|
helpModalApi.open();
|
||||||
target: '_blank',
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
icon: CircleHelp,
|
icon: CircleHelp,
|
||||||
text: $t('ui.widgets.qa'),
|
text: $t('ui.widgets.qa'),
|
||||||
@@ -98,14 +98,108 @@ async function handleLogout() {
|
|||||||
await authStore.logout(false);
|
await authStore.logout(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNoticeClear() {
|
/** 获得未读消息数 */
|
||||||
|
async function handleNotificationGetUnreadCount() {
|
||||||
|
unreadCount.value = await getUnreadNotifyMessageCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得消息列表 */
|
||||||
|
async function handleNotificationGetList() {
|
||||||
|
const list = await getUnreadNotifyMessageList();
|
||||||
|
notifications.value = list.map((item) => ({
|
||||||
|
avatar: preferences.app.defaultAvatar,
|
||||||
|
date: formatDateTime(item.createTime) as string,
|
||||||
|
isRead: false,
|
||||||
|
id: item.id,
|
||||||
|
message: item.templateContent,
|
||||||
|
title: item.templateNickname,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跳转我的站内信 */
|
||||||
|
function handleNotificationViewAll() {
|
||||||
|
router.push({
|
||||||
|
name: 'MyNotifyMessage',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记所有已读 */
|
||||||
|
async function handleNotificationMakeAll() {
|
||||||
|
await updateAllNotifyMessageRead();
|
||||||
|
unreadCount.value = 0;
|
||||||
notifications.value = [];
|
notifications.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMakeAll() {
|
/** 清空通知 */
|
||||||
notifications.value.forEach((item) => (item.isRead = true));
|
async function handleNotificationClear() {
|
||||||
|
await handleNotificationMakeAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 标记单个已读 */
|
||||||
|
async function handleNotificationRead(item: NotificationItem) {
|
||||||
|
if (!item.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await updateNotifyMessageRead([item.id]);
|
||||||
|
await handleNotificationGetUnreadCount();
|
||||||
|
notifications.value = notifications.value.filter((n) => n.id !== item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理通知打开 */
|
||||||
|
function handleNotificationOpen(open: boolean) {
|
||||||
|
if (!open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleNotificationGetList();
|
||||||
|
handleNotificationGetUnreadCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 租户列表
|
||||||
|
const tenants = ref<SystemTenantApi.Tenant[]>([]);
|
||||||
|
const tenantEnable = computed(
|
||||||
|
() => hasAccessByCodes(['system:tenant:visit']) && isTenantEnable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 获取租户列表 */
|
||||||
|
async function handleGetTenantList() {
|
||||||
|
if (tenantEnable.value) {
|
||||||
|
tenants.value = await getSimpleTenantList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理租户切换 */
|
||||||
|
async function handleTenantChange(tenant: SystemTenantApi.Tenant) {
|
||||||
|
if (!tenant || !tenant.id) {
|
||||||
|
message.error('切换租户失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 设置访问租户 ID
|
||||||
|
accessStore.setVisitTenantId(tenant.id as number);
|
||||||
|
// 关闭其他标签页,只保留当前页
|
||||||
|
await closeOtherTabs();
|
||||||
|
// 刷新当前页面
|
||||||
|
await refreshTab();
|
||||||
|
// 提示切换成功
|
||||||
|
message.success(`切换当前租户为: ${tenant.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 初始化 ==========
|
||||||
|
onMounted(() => {
|
||||||
|
// 首次加载未读数量
|
||||||
|
handleNotificationGetUnreadCount();
|
||||||
|
// 获取租户列表
|
||||||
|
handleGetTenantList();
|
||||||
|
// 轮询刷新未读数量
|
||||||
|
setInterval(
|
||||||
|
() => {
|
||||||
|
if (userStore.userInfo) {
|
||||||
|
handleNotificationGetUnreadCount();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
1000 * 60 * 2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => ({
|
() => ({
|
||||||
enable: preferences.app.watermark,
|
enable: preferences.app.watermark,
|
||||||
@@ -116,7 +210,7 @@ watch(
|
|||||||
await updateWatermark({
|
await updateWatermark({
|
||||||
content:
|
content:
|
||||||
content ||
|
content ||
|
||||||
`${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
`${userStore.userInfo?.id} - ${userStore.userInfo?.nickname}`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
destroyWatermark();
|
destroyWatermark();
|
||||||
@@ -134,9 +228,9 @@ watch(
|
|||||||
<UserDropdown
|
<UserDropdown
|
||||||
:avatar
|
:avatar
|
||||||
:menus
|
:menus
|
||||||
:text="userStore.userInfo?.realName"
|
:text="userStore.userInfo?.nickname"
|
||||||
description="ann.vben@gmail.com"
|
:description="userStore.userInfo?.email"
|
||||||
tag-text="Pro"
|
:tag-text="userStore.userInfo?.username"
|
||||||
@logout="handleLogout"
|
@logout="handleLogout"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -144,10 +238,23 @@ watch(
|
|||||||
<Notification
|
<Notification
|
||||||
:dot="showDot"
|
:dot="showDot"
|
||||||
:notifications="notifications"
|
:notifications="notifications"
|
||||||
@clear="handleNoticeClear"
|
@clear="handleNotificationClear"
|
||||||
@make-all="handleMakeAll"
|
@make-all="handleNotificationMakeAll"
|
||||||
|
@view-all="handleNotificationViewAll"
|
||||||
|
@open="handleNotificationOpen"
|
||||||
|
@read="handleNotificationRead"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<template #header-right-1>
|
||||||
|
<div v-if="tenantEnable">
|
||||||
|
<TenantDropdown
|
||||||
|
class="mr-2"
|
||||||
|
:tenant-list="tenants"
|
||||||
|
:visit-tenant-id="accessStore.visitTenantId"
|
||||||
|
@success="handleTenantChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<AuthenticationLoginExpiredModal
|
<AuthenticationLoginExpiredModal
|
||||||
v-model:open="accessStore.loginExpired"
|
v-model:open="accessStore.loginExpired"
|
||||||
@@ -160,4 +267,5 @@ watch(
|
|||||||
<LockScreen :avatar @to-login="handleLogout" />
|
<LockScreen :avatar @to-login="handleLogout" />
|
||||||
</template>
|
</template>
|
||||||
</BasicLayout>
|
</BasicLayout>
|
||||||
|
<HelpModal />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import type {
|
import type {
|
||||||
|
AppRouteRecordRaw,
|
||||||
ComponentRecordType,
|
ComponentRecordType,
|
||||||
GenerateMenuAndRoutesOptions,
|
GenerateMenuAndRoutesOptions,
|
||||||
} from '@vben/types';
|
} from '@vben/types';
|
||||||
|
|
||||||
import { generateAccessible } from '@vben/access';
|
import { generateAccessible } from '@vben/access';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
import { convertServerMenuToRouteRecordStringComponent } from '@vben/utils';
|
||||||
|
|
||||||
import { message } from '#/adapter/naive';
|
|
||||||
import { getAllMenusApi } from '#/api';
|
|
||||||
import { BasicLayout, IFrameView } from '#/layouts';
|
import { BasicLayout, IFrameView } from '#/layouts';
|
||||||
import { $t } from '#/locales';
|
|
||||||
|
|
||||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||||
|
|
||||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
|
||||||
const layoutMap: ComponentRecordType = {
|
const layoutMap: ComponentRecordType = {
|
||||||
BasicLayout,
|
BasicLayout,
|
||||||
@@ -24,10 +25,10 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
|||||||
return await generateAccessible(preferences.app.accessMode, {
|
return await generateAccessible(preferences.app.accessMode, {
|
||||||
...options,
|
...options,
|
||||||
fetchMenuListAsync: async () => {
|
fetchMenuListAsync: async () => {
|
||||||
message.loading(`${$t('common.loadingMenu')}...`, {
|
// 由于 yudao 通过 accessStore 读取,所以不在进行 message.loading 提示
|
||||||
duration: 1.5,
|
// 补充说明:accessStore.accessMenus 一开始是 AppRouteRecordRaw 类型(后端加载),后面被赋值成 MenuRecordRaw 类型(前端转换)
|
||||||
});
|
const accessMenus = accessStore.accessMenus as AppRouteRecordRaw[];
|
||||||
return await getAllMenusApi();
|
return convertServerMenuToRouteRecordStringComponent(accessMenus);
|
||||||
},
|
},
|
||||||
// 可以指定没有权限跳转403页面
|
// 可以指定没有权限跳转403页面
|
||||||
forbiddenComponent,
|
forbiddenComponent,
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import type { Router } from 'vue-router';
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
import { LOGIN_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useDictStore, useUserStore } from '@vben/stores';
|
||||||
import { startProgress, stopProgress } from '@vben/utils';
|
import { startProgress, stopProgress } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { getSimpleDictDataList } from '#/api/system/dict/data';
|
||||||
import { accessRoutes, coreRouteNames } from '#/router/routes';
|
import { accessRoutes, coreRouteNames } from '#/router/routes';
|
||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
@@ -49,6 +52,7 @@ function setupAccessGuard(router: Router) {
|
|||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const dictStore = useDictStore();
|
||||||
|
|
||||||
// 基本路由,这些路由不需要进入权限拦截
|
// 基本路由,这些路由不需要进入权限拦截
|
||||||
if (coreRouteNames.includes(to.name as string)) {
|
if (coreRouteNames.includes(to.name as string)) {
|
||||||
@@ -89,10 +93,26 @@ function setupAccessGuard(router: Router) {
|
|||||||
if (accessStore.isAccessChecked) {
|
if (accessStore.isAccessChecked) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载字典数据(不阻塞加载)
|
||||||
|
dictStore.setDictCacheByApi(getSimpleDictDataList);
|
||||||
|
|
||||||
// 生成路由表
|
// 生成路由表
|
||||||
// 当前登录用户拥有的角色标识列表
|
// 当前登录用户拥有的角色标识列表
|
||||||
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
|
let userInfo = userStore.userInfo;
|
||||||
const userRoles = userInfo.roles ?? [];
|
if (!userInfo) {
|
||||||
|
// add by 芋艿:由于 yudao 是 fetchUserInfo 统一加载用户 + 权限信息,所以将 fetchMenuListAsync
|
||||||
|
const loading = message.loading(`${$t('common.loadingMenu')}...`);
|
||||||
|
try {
|
||||||
|
const authPermissionInfo = await authStore.fetchUserInfo();
|
||||||
|
if (authPermissionInfo) {
|
||||||
|
userInfo = authPermissionInfo.user;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const userRoles = userStore.userRoles ?? [];
|
||||||
|
|
||||||
// 生成菜单和路由
|
// 生成菜单和路由
|
||||||
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
||||||
@@ -106,9 +126,10 @@ function setupAccessGuard(router: Router) {
|
|||||||
accessStore.setAccessMenus(accessibleMenus);
|
accessStore.setAccessMenus(accessibleMenus);
|
||||||
accessStore.setAccessRoutes(accessibleRoutes);
|
accessStore.setAccessRoutes(accessibleRoutes);
|
||||||
accessStore.setIsAccessChecked(true);
|
accessStore.setIsAccessChecked(true);
|
||||||
|
userStore.setUserRoles(userRoles);
|
||||||
const redirectPath = (from.query.redirect ??
|
const redirectPath = (from.query.redirect ??
|
||||||
(to.path === preferences.app.defaultHomePath
|
(to.path === preferences.app.defaultHomePath
|
||||||
? userInfo.homePath || preferences.app.defaultHomePath
|
? userInfo?.homePath || preferences.app.defaultHomePath
|
||||||
: to.fullPath)) as string;
|
: to.fullPath)) as string;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { resetStaticRoutes } from '@vben/utils';
|
|||||||
|
|
||||||
import { createRouterGuard } from './guard';
|
import { createRouterGuard } from './guard';
|
||||||
import { routes } from './routes';
|
import { routes } from './routes';
|
||||||
|
import { setupBaiduTongJi } from './tongji';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 创建vue-router实例
|
* @zh_CN 创建vue-router实例
|
||||||
@@ -33,5 +34,7 @@ const resetRoutes = () => resetStaticRoutes(router, routes);
|
|||||||
|
|
||||||
// 创建路由守卫
|
// 创建路由守卫
|
||||||
createRouterGuard(router);
|
createRouterGuard(router);
|
||||||
|
// 设置百度统计
|
||||||
|
setupBaiduTongJi(router);
|
||||||
|
|
||||||
export { resetRoutes, router };
|
export { resetRoutes, router };
|
||||||
|
|||||||
@@ -90,6 +90,23 @@ const coreRoutes: RouteRecordRaw[] = [
|
|||||||
title: $t('page.auth.register'),
|
title: $t('page.auth.register'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'SocialLogin',
|
||||||
|
path: 'social-login',
|
||||||
|
component: () =>
|
||||||
|
import('#/views/_core/authentication/social-login.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.login'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SSOLogin',
|
||||||
|
path: 'sso-login',
|
||||||
|
component: () => import('#/views/_core/authentication/sso-login.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.login'),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -34,4 +34,14 @@ const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
|||||||
|
|
||||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||||
export { accessRoutes, coreRouteNames, routes };
|
|
||||||
|
// add by 芋艿:from https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/router/routes/index.ts#L38-L45
|
||||||
|
const componentKeys: string[] = Object.keys(
|
||||||
|
import.meta.glob('../../views/**/*.vue'),
|
||||||
|
)
|
||||||
|
.filter((item) => !item.includes('/modules/'))
|
||||||
|
.map((v) => {
|
||||||
|
const path = v.replace('../../views/', '/');
|
||||||
|
return path.endsWith('.vue') ? path.slice(0, -4) : path;
|
||||||
|
});
|
||||||
|
export { accessRoutes, componentKeys, coreRouteNames, routes };
|
||||||
|
|||||||
30
apps/web-naive/src/router/tongji.ts
Normal file
30
apps/web-naive/src/router/tongji.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
_hmt: any[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const HM_ID = import.meta.env.VITE_APP_BAIDU_CODE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置百度统计
|
||||||
|
* @param router
|
||||||
|
*/
|
||||||
|
function setupBaiduTongJi(router: Router) {
|
||||||
|
// 如果没有配置百度统计的 ID,则不进行设置
|
||||||
|
if (!HM_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// _hmt:用于 router push
|
||||||
|
window._hmt = window._hmt || [];
|
||||||
|
|
||||||
|
router.afterEach((to) => {
|
||||||
|
// 添加到 _hmt 中
|
||||||
|
window._hmt.push(['_trackPageview', to.fullPath]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { setupBaiduTongJi };
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { Recordable, UserInfo } from '@vben/types';
|
import type { AuthPermissionInfo, Recordable, UserInfo } from '@vben/types';
|
||||||
|
|
||||||
|
import type { AuthApi } from '#/api';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
@@ -10,7 +12,14 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
import { notification } from '#/adapter/naive';
|
import { notification } from '#/adapter/naive';
|
||||||
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
|
import {
|
||||||
|
getAuthPermissionInfoApi,
|
||||||
|
loginApi,
|
||||||
|
logoutApi,
|
||||||
|
register,
|
||||||
|
smsLogin,
|
||||||
|
socialLogin,
|
||||||
|
} from '#/api';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
@@ -23,33 +32,53 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
/**
|
/**
|
||||||
* 异步处理登录操作
|
* 异步处理登录操作
|
||||||
* Asynchronously handle the login process
|
* Asynchronously handle the login process
|
||||||
|
* @param type 登录类型
|
||||||
* @param params 登录表单数据
|
* @param params 登录表单数据
|
||||||
|
* @param onSuccess 登录成功后的回调函数
|
||||||
*/
|
*/
|
||||||
async function authLogin(
|
async function authLogin(
|
||||||
|
type: 'mobile' | 'register' | 'social' | 'username',
|
||||||
params: Recordable<any>,
|
params: Recordable<any>,
|
||||||
onSuccess?: () => Promise<void> | void,
|
onSuccess?: () => Promise<void> | void,
|
||||||
) {
|
) {
|
||||||
// 异步处理用户登录操作并获取 accessToken
|
// 异步处理用户登录操作并获取 accessToken
|
||||||
let userInfo: null | UserInfo = null;
|
let userInfo: null | UserInfo = null;
|
||||||
try {
|
try {
|
||||||
|
let loginResult: AuthApi.LoginResult;
|
||||||
loginLoading.value = true;
|
loginLoading.value = true;
|
||||||
const { accessToken } = await loginApi(params);
|
switch (type) {
|
||||||
|
case 'mobile': {
|
||||||
|
loginResult = await smsLogin(params as AuthApi.SmsLoginParams);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'register': {
|
||||||
|
loginResult = await register(params as AuthApi.RegisterParams);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'social': {
|
||||||
|
loginResult = await socialLogin(params as AuthApi.SocialLoginParams);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
loginResult = await loginApi(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { accessToken, refreshToken } = loginResult;
|
||||||
|
|
||||||
// 如果成功获取到 accessToken
|
// 如果成功获取到 accessToken
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
// 将 accessToken 存储到 accessStore 中
|
|
||||||
accessStore.setAccessToken(accessToken);
|
accessStore.setAccessToken(accessToken);
|
||||||
|
accessStore.setRefreshToken(refreshToken);
|
||||||
|
|
||||||
// 获取用户信息并存储到 accessStore 中
|
// 获取用户信息并存储到 userStore、accessStore 中
|
||||||
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
// TODO @芋艿:清理掉 accessCodes 相关的逻辑
|
||||||
fetchUserInfo(),
|
// const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||||
getAccessCodesApi(),
|
// fetchUserInfo(),
|
||||||
]);
|
// // getAccessCodesApi(),
|
||||||
|
// ]);
|
||||||
|
const fetchUserInfoResult = await fetchUserInfo();
|
||||||
|
|
||||||
userInfo = fetchUserInfoResult;
|
userInfo = fetchUserInfoResult.user;
|
||||||
|
|
||||||
userStore.setUserInfo(userInfo);
|
|
||||||
accessStore.setAccessCodes(accessCodes);
|
|
||||||
|
|
||||||
if (accessStore.loginExpired) {
|
if (accessStore.loginExpired) {
|
||||||
accessStore.setLoginExpired(false);
|
accessStore.setLoginExpired(false);
|
||||||
@@ -61,10 +90,10 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userInfo?.realName) {
|
if (userInfo?.nickname) {
|
||||||
notification.success({
|
notification.success({
|
||||||
content: $t('authentication.loginSuccess'),
|
content: $t('authentication.loginSuccess'),
|
||||||
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.nickname}`,
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -80,7 +109,10 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
|
|
||||||
async function logout(redirect: boolean = true) {
|
async function logout(redirect: boolean = true) {
|
||||||
try {
|
try {
|
||||||
await logoutApi();
|
const accessToken = accessStore.accessToken as string;
|
||||||
|
if (accessToken) {
|
||||||
|
await logoutApi(accessToken);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 不做任何处理
|
// 不做任何处理
|
||||||
}
|
}
|
||||||
@@ -99,10 +131,16 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchUserInfo() {
|
async function fetchUserInfo() {
|
||||||
let userInfo: null | UserInfo = null;
|
// 加载
|
||||||
userInfo = await getUserInfoApi();
|
let authPermissionInfo: AuthPermissionInfo | null = null;
|
||||||
userStore.setUserInfo(userInfo);
|
authPermissionInfo = await getAuthPermissionInfoApi();
|
||||||
return userInfo;
|
// userStore
|
||||||
|
userStore.setUserInfo(authPermissionInfo.user);
|
||||||
|
userStore.setUserRoles(authPermissionInfo.roles);
|
||||||
|
// accessStore
|
||||||
|
accessStore.setAccessMenus(authPermissionInfo.menus);
|
||||||
|
accessStore.setAccessCodes(authPermissionInfo.permissions);
|
||||||
|
return authPermissionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
function $reset() {
|
function $reset() {
|
||||||
|
|||||||
@@ -2,24 +2,100 @@
|
|||||||
import type { VbenFormSchema } from '@vben/common-ui';
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
import type { Recordable } from '@vben/types';
|
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 { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||||
|
import { isTenantEnable } from '@vben/hooks';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { sendSmsCode } from '#/api';
|
||||||
|
import { getTenantByWebsite, getTenantSimpleList } from '#/api/core/auth';
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
defineOptions({ name: 'CodeLogin' });
|
defineOptions({ name: 'CodeLogin' });
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const tenantEnable = isTenantEnable();
|
||||||
|
|
||||||
const loading = ref(false);
|
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[] => {
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
return [
|
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',
|
component: 'VbenInput',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: $t('authentication.mobile'),
|
placeholder: $t('authentication.mobile'),
|
||||||
},
|
},
|
||||||
fieldName: 'phoneNumber',
|
fieldName: 'mobile',
|
||||||
label: $t('authentication.mobile'),
|
label: $t('authentication.mobile'),
|
||||||
rules: z
|
rules: z
|
||||||
.string()
|
.string()
|
||||||
@@ -40,6 +116,29 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
return text;
|
return text;
|
||||||
},
|
},
|
||||||
placeholder: $t('authentication.code'),
|
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 });
|
||||||
|
message.success('验证码发送成功');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fieldName: 'code',
|
fieldName: 'code',
|
||||||
label: $t('authentication.code'),
|
label: $t('authentication.code'),
|
||||||
@@ -55,13 +154,17 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
* @param values 登录表单数据
|
* @param values 登录表单数据
|
||||||
*/
|
*/
|
||||||
async function handleLogin(values: Recordable<any>) {
|
async function handleLogin(values: Recordable<any>) {
|
||||||
// eslint-disable-next-line no-console
|
try {
|
||||||
console.log(values);
|
await authStore.authLogin('mobile', values);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in handleLogin:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationCodeLogin
|
<AuthenticationCodeLogin
|
||||||
|
ref="loginRef"
|
||||||
:form-schema="formSchema"
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@submit="handleLogin"
|
@submit="handleLogin"
|
||||||
|
|||||||
@@ -2,40 +2,212 @@
|
|||||||
import type { VbenFormSchema } from '@vben/common-ui';
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
import type { Recordable } from '@vben/types';
|
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 { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||||
|
import { isTenantEnable } from '@vben/hooks';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { sendSmsCode, smsResetPassword } from '#/api';
|
||||||
|
import { getTenantByWebsite, getTenantSimpleList } from '#/api/core/auth';
|
||||||
|
|
||||||
defineOptions({ name: 'ForgetPassword' });
|
defineOptions({ name: 'ForgetPassword' });
|
||||||
|
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const tenantEnable = isTenantEnable();
|
||||||
|
|
||||||
const loading = ref(false);
|
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[] => {
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
return [
|
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',
|
component: 'VbenInput',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: 'example@example.com',
|
placeholder: $t('authentication.mobile'),
|
||||||
},
|
},
|
||||||
fieldName: 'email',
|
fieldName: 'mobile',
|
||||||
label: $t('authentication.email'),
|
label: $t('authentication.mobile'),
|
||||||
rules: z
|
rules: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: $t('authentication.emailTip') })
|
.min(1, { message: $t('authentication.mobileTip') })
|
||||||
.email($t('authentication.emailValidErrorTip')),
|
.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<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 });
|
||||||
|
message.success($t('authentication.resetPasswordSuccess'));
|
||||||
|
// 重置成功后跳转到首页
|
||||||
|
router.push('/');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重置密码失败:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationForgetPassword
|
<AuthenticationForgetPassword
|
||||||
|
ref="forgetPasswordRef"
|
||||||
:form-schema="formSchema"
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
|
|||||||
@@ -1,98 +1,192 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { VbenFormSchema } from '@vben/common-ui';
|
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 { $t } from '@vben/locales';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import {
|
||||||
|
checkCaptcha,
|
||||||
|
getCaptcha,
|
||||||
|
getTenantByWebsite,
|
||||||
|
getTenantSimpleList,
|
||||||
|
socialAuthRedirect,
|
||||||
|
} from '#/api/core/auth';
|
||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
defineOptions({ name: 'Login' });
|
defineOptions({ name: 'Login' });
|
||||||
|
|
||||||
|
const { query } = useRoute();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const tenantEnable = isTenantEnable();
|
||||||
|
const captchaEnable = isCaptchaEnable();
|
||||||
|
|
||||||
const MOCK_USER_OPTIONS: BasicOption[] = [
|
const loginRef = ref();
|
||||||
{
|
const verifyRef = ref();
|
||||||
label: 'Super',
|
|
||||||
value: 'vben',
|
const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWord'
|
||||||
},
|
|
||||||
{
|
/** 获取租户列表,并默认选中 */
|
||||||
label: 'Admin',
|
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||||
value: 'admin',
|
async function fetchTenantList() {
|
||||||
},
|
if (!tenantEnable) {
|
||||||
{
|
return;
|
||||||
label: 'User',
|
}
|
||||||
value: 'jack',
|
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[] => {
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
component: 'VbenSelect',
|
component: 'VbenSelect',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
options: MOCK_USER_OPTIONS,
|
options: tenantList.value.map((item) => ({
|
||||||
placeholder: $t('authentication.selectAccount'),
|
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',
|
component: 'VbenInput',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: $t('authentication.usernameTip'),
|
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',
|
fieldName: 'username',
|
||||||
label: $t('authentication.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',
|
component: 'VbenInputPassword',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: $t('authentication.password'),
|
placeholder: $t('authentication.passwordTip'),
|
||||||
},
|
},
|
||||||
fieldName: 'password',
|
fieldName: 'password',
|
||||||
label: $t('authentication.password'),
|
label: $t('authentication.password'),
|
||||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
rules: z
|
||||||
},
|
.string()
|
||||||
{
|
.min(1, { message: $t('authentication.passwordTip') })
|
||||||
component: markRaw(SliderCaptcha),
|
.default(import.meta.env.VITE_APP_DEFAULT_PASSWORD),
|
||||||
fieldName: 'captcha',
|
|
||||||
rules: z.boolean().refine((value) => value, {
|
|
||||||
message: $t('authentication.verifyRequiredTip'),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
<AuthenticationLogin
|
<AuthenticationLogin
|
||||||
|
ref="loginRef"
|
||||||
:form-schema="formSchema"
|
:form-schema="formSchema"
|
||||||
:loading="authStore.loginLoading"
|
:loading="authStore.loginLoading"
|
||||||
@submit="authStore.authLogin"
|
@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>
|
</template>
|
||||||
|
|||||||
@@ -1,18 +1,126 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { VbenFormSchema } from '@vben/common-ui';
|
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 { $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' });
|
defineOptions({ name: 'Register' });
|
||||||
|
|
||||||
const loading = ref(false);
|
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[] => {
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
return [
|
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',
|
component: 'VbenInput',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
@@ -22,6 +130,15 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
label: $t('authentication.username'),
|
label: $t('authentication.username'),
|
||||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
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',
|
component: 'VbenInputPassword',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
@@ -67,7 +184,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
h(
|
h(
|
||||||
'a',
|
'a',
|
||||||
{
|
{
|
||||||
class: 'vben-link ml-1',
|
class: 'vben-link ml-1 ',
|
||||||
href: '',
|
href: '',
|
||||||
},
|
},
|
||||||
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
|
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
|
||||||
@@ -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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
<AuthenticationRegister
|
<AuthenticationRegister
|
||||||
|
ref="registerRef"
|
||||||
:form-schema="formSchema"
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@submit="handleSubmit"
|
@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>
|
</template>
|
||||||
|
|||||||
210
apps/web-naive/src/views/_core/authentication/social-login.vue
Normal file
210
apps/web-naive/src/views/_core/authentication/social-login.vue
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
<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 { getUrlValue } from '@vben/utils';
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组件挂载时获取租户信息 */
|
||||||
|
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-naive/src/views/_core/authentication/sso-login.vue
Normal file
221
apps/web-naive/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>
|
||||||
Reference in New Issue
Block a user