chore: init project

This commit is contained in:
vben
2024-05-19 21:20:42 +08:00
commit 399334ac57
630 changed files with 45623 additions and 0 deletions

View File

@@ -0,0 +1,183 @@
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings';
import { filterTree, mapTree, traverseTreeValues } from '@vben-core/toolkit';
import type { RouteRecordRaw, Router } from 'vue-router';
import { useAccessStore } from '@vben/stores';
import { dynamicRoutes } from '../routes';
// 登录页面路由 path
const LOGIN_ROUTE_PATH = '/auth/login';
// 不需要权限的页面白名单
const WHITE_ROUTE_NAMES = new Set<string>([]);
/**
* 权限访问守卫配置
* @param router
*/
function configAccessGuard(router: Router) {
router.beforeEach(async (to, from) => {
const accessStore = useAccessStore();
const accessToken = accessStore.getAccessToken;
// accessToken 检查
if (!accessToken) {
// 明确声明忽略权限访问权限,则可以访问
if (to.meta.ignoreAccess) {
return true;
}
// 白名单路由列表检查
if (WHITE_ROUTE_NAMES.has(to.name as string)) {
return true;
}
// 没有访问权限,跳转登录页面
if (to.fullPath !== LOGIN_ROUTE_PATH) {
return {
path: LOGIN_ROUTE_PATH,
// 如不需要,直接删除 query
query: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
}
return to;
}
const accessRoutes = accessStore.getAccessRoutes;
// 是否已经生成过动态路由
if (accessRoutes && accessRoutes.length > 0) {
return true;
}
// 生成路由表
// 当前登录用户拥有的角色标识列表
const userRoles = accessStore.getUserRoles;
const routes = await generatorRoutes(userRoles);
// 动态添加到router实例内
routes.forEach((route) => router.addRoute(route));
const menus = await generatorMenus(routes, router);
// 保存菜单信息和路由信息
accessStore.setAccessMenus(menus);
accessStore.setAccessRoutes(routes);
const redirectPath = (from.query.redirect || to.path) as string;
const redirect = decodeURIComponent(redirectPath);
return {
path: redirect,
replace: true,
};
});
}
/**
* 动态生成路由
*/
async function generatorRoutes(roles: string[]): Promise<RouteRecordRaw[]> {
// 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
return filterTree(dynamicRoutes, (route) => {
return hasVisible(route) && hasAuthority(route, roles);
});
}
/**
* 根据 routes 生成菜单列表
* @param routes
*/
async function generatorMenus(
routes: RouteRecordRaw[],
router: Router,
): Promise<MenuRecordRaw[]> {
// 获取所有router最终的path及name
const finalRoutes = traverseTreeValues(
router.getRoutes(),
({ name, path }) => {
return {
name,
path,
};
},
);
const menus = mapTree<ExRouteRecordRaw, MenuRecordRaw>(routes, (route) => {
// 路由表的路径写法有多种这里从router获取到最终的path并赋值
const matchRoute = finalRoutes.find(
(finalRoute) => finalRoute.name === route.name,
);
// 转换为菜单结构
const path = matchRoute?.path ?? route.path;
const { meta, name: routeName, redirect, children } = route;
const {
badge,
badgeType,
badgeVariants,
hideChildrenInMenu = false,
icon,
orderNo,
title = '',
} = meta || {};
const name = (title || routeName || '') as string;
// 隐藏子菜单
const resultChildren = hideChildrenInMenu
? []
: (children as MenuRecordRaw[]);
// 将菜单的所有父级和父级菜单记录到菜单项内
if (resultChildren && resultChildren.length > 0) {
resultChildren.forEach((child) => {
child.parents = [...(route.parents || []), path];
child.parent = path;
});
}
// 隐藏子菜单
const resultPath = hideChildrenInMenu ? redirect : path;
return {
badge,
badgeType,
badgeVariants,
icon,
name,
orderNo,
parent: route.parent,
parents: route.parents,
path: resultPath,
children: resultChildren,
};
});
return menus;
}
/**
* 判断路由是否有权限访问
* @param route
* @param access
*/
function hasAuthority(route: RouteRecordRaw, access: string[]) {
const authority = route.meta?.authority;
if (!authority) {
return true;
}
const authSet = new Set(authority);
return access.some((value) => {
return authSet.has(value);
});
}
/**
* 判断路由是否需要在菜单中显示
* @param route
*/
function hasVisible(route: RouteRecordRaw) {
return !route.meta?.hideInMenu;
}
export { configAccessGuard };

View File

@@ -0,0 +1,43 @@
import { startProgress, stopProgress } from '@vben-core/toolkit';
import type { Router } from 'vue-router';
import { preference } from '@vben/preference';
import { configAccessGuard } from './access';
/**
* 通用守卫配置
* @param router
*/
function configCommonGuard(router: Router) {
const loadedPaths = new Set<string>();
router.beforeEach(async (to) => {
if (preference.pageProgress) {
startProgress();
}
to.meta.loaded = loadedPaths.has(to.path);
return true;
});
router.afterEach((to) => {
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
loadedPaths.add(to.path);
if (preference.pageProgress) {
stopProgress();
}
});
}
/**
* 项目守卫配置
* @param router
*/
function createRouteGuard(router: Router) {
/** 通用 */
configCommonGuard(router);
/** 权限访问 */
configAccessGuard(router);
}
export { createRouteGuard };

View File

@@ -0,0 +1,59 @@
import { traverseTreeValues } from '@vben-core/toolkit';
import type { RouteRecordName, RouteRecordRaw } from 'vue-router';
import { createRouter, createWebHashHistory } from 'vue-router';
import { createRouteGuard } from './guard';
import { staticRoutes } from './routes';
/**
* @zh_CN 创建vue-router实例
*/
const router = createRouter({
history: createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH),
// 应该添加到路由的初始路由列表。
routes: staticRoutes,
scrollBehavior: (to, from, savedPosition) => {
if (to.path !== from.path) {
setTimeout(() => {
const app = document.querySelector('#app');
if (app) {
app.scrollTop = 0;
}
});
}
return savedPosition || { left: 0, top: 0 };
},
});
/**
* @zh_CN 重置所有路由,如有指定白名单除外
*/
function resetRoutes() {
// 获取静态路由所有节点包含子节点的 name并排除不存在 name 字段的路由
const staticRouteNames = traverseTreeValues<
RouteRecordRaw,
RouteRecordName | undefined
>(staticRoutes, (route) => {
// 这些路由需要指定 name防止在路由重置时不能删除没有指定 name 的路由
if (!route.name) {
console.warn(
`The route with the path ${route.path} needs to specify the field name.`,
);
}
return route.name;
});
const { getRoutes, hasRoute, removeRoute } = router;
const routes = getRoutes();
routes.forEach(({ name }) => {
// 存在于路由表且非白名单才需要删除
if (name && !staticRouteNames.includes(name) && hasRoute(name)) {
removeRoute(name);
}
});
}
// 创建路由守卫
createRouteGuard(router);
export { resetRoutes, router };

View File

@@ -0,0 +1,78 @@
import type { RouteRecordRaw } from 'vue-router';
import { Fallback } from '@vben/common-ui';
import { AuthPageLayout } from './layout';
/** 静态路由列表,访问这些页面可以不需要权限 */
const builtinRoutes: RouteRecordRaw[] = [
{
component: AuthPageLayout,
meta: {
title: 'Authentication',
},
name: 'Authentication',
path: '/auth',
children: [
{
name: 'Login',
path: 'login',
component: () => import('@/views/authentication/login.vue'),
meta: {
ignoreAccess: true,
title: 'Login',
},
},
{
name: 'CodeLogin',
path: 'code-login',
component: () => import('@/views/authentication/code-login.vue'),
meta: {
ignoreAccess: true,
title: 'CodeLogin',
},
},
{
name: 'QrCodeLogin',
path: 'qrcode-login',
component: () => import('@/views/authentication/qrcode-login.vue'),
meta: {
ignoreAccess: true,
title: 'QrCodeLogin',
},
},
{
name: 'ForgetPassword',
path: 'forget-password',
component: () => import('@/views/authentication/forget-password.vue'),
meta: {
ignoreAccess: true,
title: 'ForgetPassword',
},
},
{
name: 'Register',
path: 'register',
component: () => import('@/views/authentication/register.vue'),
meta: {
ignoreAccess: true,
title: 'Register',
},
},
],
},
// 错误页
{
component: Fallback,
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: 'Fallback',
},
name: 'Fallback',
path: '/:path(.*)*',
},
];
export { builtinRoutes };

View File

@@ -0,0 +1,66 @@
import type { RouteRecordRaw } from 'vue-router';
import { builtinRoutes } from './builtin';
import { Layout } from './layout';
import { nestedRoutes } from './modules/nested';
import { outsideRoutes } from './modules/outside';
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = [
// 根路由
{
component: Layout,
meta: {
hideChildrenInMenu: true,
title: '首页',
},
name: 'Home',
path: '/',
redirect: '/welcome',
children: [
{
name: 'Welcome',
path: '/welcome',
component: () => import('@/views/dashboard/index.vue'),
meta: {
affixTab: true,
title: 'Welcome',
},
},
],
},
...nestedRoutes,
...outsideRoutes,
// 关于
{
component: Layout,
meta: {
hideChildrenInMenu: true,
icon: 'https://cdn.jsdelivr.net/gh/vbenjs/vben-cdn-static@0.1.2/vben-admin/admin-logo.png',
keepAlive: false,
title: '关于',
},
name: 'AboutLayout',
path: '/about',
redirect: '/about/index',
children: [
{
name: 'About',
path: 'index',
component: () => import('@/views/about/index.vue'),
meta: {
keepAlive: false,
title: '关于',
},
},
],
},
];
/** 排除在主框架外的路由,这些路由没有菜单和顶部及其他框架内容 */
const externalRoutes: RouteRecordRaw[] = [];
/** 静态路由列表,访问这些页面可以不需要权限 */
const staticRoutes: RouteRecordRaw[] = [...builtinRoutes];
export { dynamicRoutes, externalRoutes, staticRoutes };

View File

@@ -0,0 +1,8 @@
const Layout = () => import('@/layout.vue');
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
const AuthPageLayout = () =>
import('@vben/layouts').then((m) => m.AuthPageLayout);
export { AuthPageLayout, IFrameView, Layout };

View File

@@ -0,0 +1,71 @@
import type { RouteRecordRaw } from 'vue-router';
import { Layout } from '../layout';
export const nestedRoutes: RouteRecordRaw[] = [
{
component: Layout,
meta: {
keepAlive: true,
title: '多级菜单',
},
name: 'Nested',
path: '/nested',
children: [
{
name: 'Menu1',
path: 'menu1',
component: () => import('@/views/nested/menu-1.vue'),
meta: {
keepAlive: true,
title: '菜单1',
},
},
{
name: 'Menu2',
path: 'menu2',
component: () => import('@/views/nested/menu-2.vue'),
meta: {
keepAlive: true,
title: '菜单2',
},
},
{
name: 'Menu3',
path: 'menu3',
meta: {
title: '菜单3',
},
children: [
{
name: 'Menu31',
path: 'menu3-1',
component: () => import('@/views/nested/menu-3-1.vue'),
meta: {
keepAlive: true,
title: '菜单3-1',
},
},
{
name: 'Menu32',
path: 'menu3-2',
meta: {
title: '菜单3-2',
},
children: [
{
name: 'Menu321',
path: 'menu3-2-1',
component: () => import('@/views/nested/menu-3-2-1.vue'),
meta: {
keepAlive: true,
title: '菜单3-2-1',
},
},
],
},
],
},
],
},
];

View File

@@ -0,0 +1,37 @@
import type { RouteRecordRaw } from 'vue-router';
import { IFrameView, Layout } from '../layout';
export const outsideRoutes: RouteRecordRaw[] = [
{
component: Layout,
meta: {
title: '外部页面',
},
name: 'Outside',
path: '/outside',
redirect: '/outside/document',
children: [
{
name: 'Document',
path: 'document',
component: IFrameView,
meta: {
iframeSrc: 'https://doc.vvbin.cn/',
// keepAlive: true,
title: '项目文档',
},
},
{
name: 'IFrameView',
path: 'vue-document',
component: IFrameView,
meta: {
iframeSrc: 'https://cn.vuejs.org/',
keepAlive: true,
title: 'Vue 文档(缓存)',
},
},
],
},
];