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

2
apps/antd-view/.env Normal file
View File

@@ -0,0 +1,2 @@
# spa-title
VITE_GLOB_APP_TITLE = Vben Admin Pro

View File

@@ -0,0 +1,6 @@
# public path
VITE_PUBLIC_PATH = /
# Basic interface address SPA
VITE_GLOB_API_URL=/vben-api

View File

@@ -0,0 +1,3 @@
VITE_PUBLIC_PATH = /
VITE_GLOB_API_URL=/vben-api

View File

@@ -0,0 +1,5 @@
# public path
VITE_PUBLIC_PATH = /
# Basic interface address SPA
VITE_GLOB_API_URL=/vben-api

22
apps/antd-view/index.html Normal file
View File

@@ -0,0 +1,22 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="description" content="A Modern Back-end Management System" />
<meta name="keywords" content="Vben Admin Pro Vue3 Vite" />
<meta name="author" content="Vben" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<!-- 由 vite 注入 VITE_GLOB_APP_TITLE 变量,在 . env 内配置 -->
<title><%= VITE_GLOB_APP_TITLE %></title>
<link rel="icon" href="/favicon.ico" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,33 @@
function resultSuccess<T = Record<string, any>>(
result: T,
{ message = 'ok' } = {},
) {
return {
code: 0,
message,
result,
type: 'success',
};
}
function resultError(
message = 'Request failed',
{ code = -1, result = null } = {},
) {
return {
code,
message,
result,
type: 'error',
};
}
/**
* @zh_CN 本函数用于从request数据中获取token请根据项目的实际情况修改
*
*/
function getRequestToken({ headers }: any): string | undefined {
return headers?.authorization;
}
export { getRequestToken, resultError, resultSuccess };

101
apps/antd-view/mock/user.ts Normal file
View File

@@ -0,0 +1,101 @@
import { getRequestToken, resultError, resultSuccess } from './_util';
const fakeUserList = [
{
accessToken: 'fakeAdminToken',
avatar: '',
desc: 'manager',
homePath: '/welcome',
password: '123456',
realName: 'Vben Admin',
roles: [
{
roleName: 'Super Admin',
value: 'super',
},
],
userId: '1',
username: 'vben',
},
{
accessToken: 'fakeTestToken',
avatar: '',
desc: 'tester',
homePath: '/welcome',
password: '123456',
realName: 'test user',
roles: [
{
roleName: 'Tester',
value: 'test',
},
],
userId: '2',
username: 'test',
},
];
export default [
{
method: 'post',
response: ({ body }: any) => {
const { password, username } = body;
const checkUser = fakeUserList.find(
(item) => item.username === username && password === item.password,
);
if (!checkUser) {
return resultError('Incorrect account or password');
}
const {
accessToken,
desc,
realName,
roles,
userId,
username: _username,
} = checkUser;
return resultSuccess({
accessToken,
desc,
realName,
roles,
userId,
username: _username,
});
},
timeout: 200,
url: '/vben-api/login',
},
{
method: 'get',
response: (request: any) => {
const token = getRequestToken(request);
if (!token) return resultError('Invalid token');
const checkUser = fakeUserList.find((item) => item.accessToken === token);
if (!checkUser) {
return resultError(
'The corresponding user information was not obtained!',
);
}
const { accessToken: _token, password: _pwd, ...rest } = checkUser;
return resultSuccess(rest);
},
url: '/vben-api/getUserInfo',
},
{
method: 'get',
response: (request: any) => {
const token = getRequestToken(request);
if (!token) return resultError('Invalid token');
const checkUser = fakeUserList.find((item) => item.accessToken === token);
if (!checkUser) {
return resultError('Invalid token!');
}
return resultSuccess(undefined, {
message: 'Token has been destroyed',
});
},
timeout: 200,
url: '/vben-api/logout',
},
];

View File

@@ -0,0 +1,47 @@
{
"name": "@vben/antd-view",
"version": "4.0.0-alpha.1",
"type": "module",
"author": {
"name": "vben",
"email": "anncwb@126.com",
"url": "https://github.com/anncwb"
},
"license": "MIT",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "apps/vben-admin"
},
"bugs": {
"url": "https://github.com/vbenjs/vue-vben-admin/issues"
},
"scripts": {
"build": "pnpm vite build",
"dev": "pnpm vite",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"dependencies": {
"@vben-core/design": "workspace:*",
"@vben-core/design-tokens": "workspace:*",
"@vben-core/toolkit": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/preference": "workspace:*",
"@vben/stores": "workspace:*",
"ant-design-vue": "^4.2.1",
"axios": "^1.6.8",
"dayjs": "^1.11.11",
"vue": "^3.4.27",
"vue-router": "^4.3.2"
},
"devDependencies": {
"vite-plugin-mock": "^3.0.2"
}
}

View File

@@ -0,0 +1 @@
export { default } from '@vben/tailwind-config/postcss';

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

View File

@@ -0,0 +1,40 @@
<script lang="ts" setup>
import 'dayjs/locale/zh-cn';
import { GlobalProvider } from '@vben/common-ui';
import { preference, usePreference } from '@vben/preference';
import { ConfigProvider, theme } from 'ant-design-vue';
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
import { computed } from 'vue';
defineOptions({ name: 'App' });
dayjs.locale(zhCN.locale);
const { isDark } = usePreference();
const tokenTheme = computed(() => {
const { colorPrimary, compact } = preference;
const algorithms = isDark.value
? [theme.darkAlgorithm]
: [theme.defaultAlgorithm];
// antd 紧凑模式算法
if (compact) {
algorithms.push(theme.compactAlgorithm);
}
return {
algorithms,
token: { colorPrimary },
};
});
</script>
<template>
<GlobalProvider>
<ConfigProvider :locale="zhCN" :theme="tokenTheme">
<RouterView />
</ConfigProvider>
</GlobalProvider>
</template>

View File

@@ -0,0 +1,127 @@
<script lang="ts" setup>
import type { NotificationItem } from '@vben/common-ui';
import { openWindow } from '@vben-core/toolkit';
import { Notification, UserDropdown } from '@vben/common-ui';
import {
IcRoundCreditScore,
IcRoundSettingsSuggest,
MdiDriveDocument,
MdiGithub,
} from '@vben/icons';
import { BasicLayout } from '@vben/layouts';
import { $t } from '@vben/locales';
import { preference } from '@vben/preference';
import { useAccessStore } from '@vben/stores';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
// https://avatar.vercel.sh/vercel.svg?text=Vaa
// https://avatar.vercel.sh/1
// https://avatar.vercel.sh/nextjs
// https://avatar.vercel.sh/satori
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 menus = computed(() => [
{
handler: () => {
openWindow('https://github.com/vbenjs/vue-vben-admin', {
target: '_blank',
});
},
icon: MdiDriveDocument,
text: $t('widgets.document'),
},
{
handler: () => {
openWindow('https://github.com/vbenjs/vue-vben-admin', {
target: '_blank',
});
},
icon: MdiGithub,
text: 'GitHub',
},
{
handler: () => {
openWindow('https://github.com/vbenjs/vue-vben-admin/issues', {
target: '_blank',
});
},
icon: IcRoundCreditScore,
text: $t('widgets.qa'),
},
{
handler: () => {
// openWindow('https://github.com/vbenjs/vue-vben-admin', {
// target: '_blank',
// });
},
icon: IcRoundSettingsSuggest,
text: $t('widgets.setting'),
},
]);
const accessStore = useAccessStore();
const router = useRouter();
function handleLogout() {
accessStore.$reset();
router.replace('/auth/login');
}
function handleNoticeClear() {
notifications.value = [];
}
</script>
<template>
<BasicLayout>
<template #user-dropdown>
<UserDropdown
:avatar="preference.defaultAvatar"
:menus="menus"
text="Vben Admin"
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
/>
</template>
<template #notification>
<Notification
dot
:notifications="notifications"
@clear="handleNoticeClear"
/>
</template>
</BasicLayout>
</template>

View File

@@ -0,0 +1,43 @@
import '@vben-core/design/tailwind';
import '@vben-core/design';
import '@vben-core/design-tokens';
import { setupI18n } from '@vben/locales';
import { preference, setupPreference } from '@vben/preference';
import { setupStore } from '@vben/stores';
import { createApp } from 'vue';
import App from './app.vue';
import { overridesPreference } from './preference';
import { router } from './router';
async function bootstrap(cachePrefix: string) {
// app偏好设置
await setupPreference({
cachePrefix,
overrides: overridesPreference,
});
const app = createApp(App);
// 国际化 i18n 配置
await setupI18n(app, { defaultLocale: preference.locale });
// 配置 pinia-store
await setupStore(app, { cachePrefix });
// 配置路由及路由守卫
app.use(router);
app.mount('#app');
// production mock server
if (import.meta.env.PROD) {
import('./mock-prod-server').then(({ setupProdMockServer }) => {
setupProdMockServer();
});
}
}
bootstrap('vben-admin-pro-antd');

View File

@@ -0,0 +1,10 @@
import { createProdMockServer } from 'vite-plugin-mock/client';
// 逐一导入您的mock.ts文件
// 如果使用vite.mock.config.ts只需直接导入文件
// 可以使用 import.meta.glob功能来进行全部导入
import userModule from '../mock/user';
export function setupProdMockServer() {
createProdMockServer([...userModule]);
}

View File

@@ -0,0 +1,7 @@
import type { Preference } from '@vben-core/typings';
/**
* @description 项目配置文件
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
*/
export const overridesPreference: Partial<Preference> = {};

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 文档(缓存)',
},
},
],
},
];

View File

@@ -0,0 +1 @@
export * from './modules/user';

View File

@@ -0,0 +1,23 @@
import type { UserInfo } from '@vben-core/typings';
import { request } from '@/services/request';
import type { UserApi } from './typing';
/**
* 登录
*/
async function userLogin(data: UserApi.LoginParams) {
return request<UserApi.LoginResult>('/login', { data, method: 'post' });
}
/**
* 获取用户信息
*/
async function getUserInfo() {
return request<UserInfo>('/getUserInfo', { method: 'get' });
}
export { getUserInfo, userLogin };
export type { UserApi } from './typing';

View File

@@ -0,0 +1,18 @@
namespace UserApi {
/** 登录接口参数 */
export interface LoginParams {
password: string;
username: string;
}
/** 登录接口返回值 */
export interface LoginResult {
accessToken: string;
desc: string;
realName: string;
userId: string;
username: string;
}
}
export type { UserApi };

View File

@@ -0,0 +1,152 @@
/**
* 该文件可自行根据业务逻辑进行调整
*/
import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import axios, {
AxiosError,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios';
// 后端需要的 token 存放在header内的key字段
// 可以根据自己的需要修改
const REQUEST_HEADER_TOKEN_KEY = 'Authorization';
type HttpConfig = InternalAxiosRequestConfig;
interface HttpResponse<T = any> {
code: number;
message: string;
result: T;
}
// 用于存储每个请求的标识和取消函数
const pendingMap = new Map<string, AbortController>();
const getPendingUrl = (config: AxiosRequestConfig): string => {
return [config.method, config.url].join('&');
};
/**
* 添加请求
* @param config 请求配置
*/
function addRequestSignal(config: AxiosRequestConfig): void {
abortRequest(config);
const url = getPendingUrl(config);
const controller = new AbortController();
config.signal = config.signal || controller.signal;
if (!pendingMap.has(url)) {
// 如果当前请求不在等待中,将其添加到等待中
pendingMap.set(url, controller);
}
}
/**
* 清除所有等待中的请求
*/
function abortAllRequest() {
pendingMap.forEach((abortController) => {
if (abortController) {
abortController.abort();
}
});
pendingMap.clear();
}
/**
* 移除请求
* @param config 请求配置
*/
function abortRequest(config: AxiosRequestConfig): void {
if (!config) {
return;
}
const url = getPendingUrl(config);
if (pendingMap.has(url)) {
// 如果当前请求在等待中,取消它并将其从等待中移除
const abortController = pendingMap.get(url);
if (abortController) {
abortController.abort(url);
}
pendingMap.delete(url);
}
}
const axiosInstance = axios.create({
// .env 环境获取请求地址
baseURL: import.meta.env.VITE_GLOB_API_URL,
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
timeout: 10 * 1000,
});
// 请求拦截器
axiosInstance.interceptors.request.use(
(config: HttpConfig) => {
addRequestSignal(config);
// 携带 getAccessToken 在请求头
const accessStore = useAccessStore();
const getAccessToken = accessStore.getAccessToken;
if (getAccessToken) {
config.headers[REQUEST_HEADER_TOKEN_KEY] = getAccessToken;
}
return config;
},
(error: AxiosError) => {
return Promise.reject(error);
},
);
// 添加响应拦截器
axiosInstance.interceptors.response.use(
(response: AxiosResponse<HttpResponse>) => {
const { data: responseData, status } = response;
const { code, message: msg, result } = responseData;
abortRequest(response.config);
if (status === 200 && code === 0) {
return result;
} else {
message.error(msg);
throw new Error(msg);
}
},
(error: any) => {
if (axios.isCancel(error)) {
return Promise.reject(error);
}
const err: string = error?.toString?.() ?? '';
let errMsg = '';
if (err?.includes('Network Error')) {
errMsg = '网络错误。';
} else if (error?.message?.includes?.('timeout')) {
errMsg = '请求超时。';
} else {
errMsg = error?.response?.data?.error?.message ?? '';
}
message.error(errMsg);
return Promise.reject(error);
},
);
async function request<T>(url: string, config: AxiosRequestConfig): Promise<T> {
try {
const response: AxiosResponse<T> = await axiosInstance({
url,
...config,
});
return response as T;
} catch (error: any) {
throw error.response ? error.response.data : error;
}
}
export { abortAllRequest, request };

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
defineOptions({ name: 'About' });
onMounted(() => {
// eslint-disable-next-line no-console
console.log('About');
});
</script>
<template>
<div>
about
<input class="bg-background border-border" />
</div>
</template>

View File

@@ -0,0 +1,24 @@
<script lang="ts" setup>
import type { LoginCodeParams } from '@vben/common-ui';
import { AuthenticationCodeLogin } from '@vben/common-ui';
import { ref } from 'vue';
defineOptions({ name: 'CodeLogin' });
const loading = ref(false);
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param values 登录表单数据
*/
async function handleLogin(values: LoginCodeParams) {
// eslint-disable-next-line no-console
console.log(values);
}
</script>
<template>
<AuthenticationCodeLogin :loading="loading" @submit="handleLogin" />
</template>

View File

@@ -0,0 +1,17 @@
<script lang="ts" setup>
import { AuthenticationForgetPassword } from '@vben/common-ui';
import { ref } from 'vue';
defineOptions({ name: 'ForgetPassword' });
const loading = ref(false);
function handleSubmit(value: string) {
// eslint-disable-next-line no-console
console.log('reset email:', value);
}
</script>
<template>
<AuthenticationForgetPassword :loading="loading" @submit="handleSubmit" />
</template>

View File

@@ -0,0 +1,69 @@
<script lang="ts" setup>
import type { LoginAndRegisterParams } from '@vben/common-ui';
import { getUserInfo, userLogin } from '@/services';
import { AuthenticationLogin } from '@vben/common-ui';
import { useRequest } from '@vben/hooks';
import { $t } from '@vben/locales';
import { useAccessStore } from '@vben/stores';
import { notification } from 'ant-design-vue';
import { computed } from 'vue';
import { useRouter } from 'vue-router';
defineOptions({ name: 'Login' });
const router = useRouter();
const accessStore = useAccessStore();
const { loading, runAsync: runUserLogin } = useRequest(userLogin, {
manual: true,
});
const { loading: userInfoLoading, runAsync: runGetUserInfo } = useRequest(
getUserInfo,
{
manual: true,
},
);
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param values 登录表单数据
*/
async function handleLogin(values: LoginAndRegisterParams) {
// 异步处理用户登录操作并获取 accessToken
// Asynchronously handle the user login operation and obtain the accessToken
const { accessToken } = await runUserLogin(values);
// 如果成功获取到 accessToken
// If accessToken is successfully obtained
if (accessToken) {
// 将 accessToken 存储到 accessStore 中
// Store the accessToken in accessStore
accessStore.setAccessToken(accessToken);
// 获取用户信息并存储到 accessStore 中
// Get user information and store it in accessStore
const userInfo = await runGetUserInfo();
accessStore.setUserInfo(userInfo);
// 跳转到用户信息中定义的 homePath 路径
// Redirect to the homePath defined in the user information
await router.push(userInfo.homePath);
notification.success({
description: `${$t('authentication.login-success-desc')}:${userInfo.realName}`,
duration: 3,
message: $t('authentication.login-success'),
});
}
}
const loginLoading = computed(() => {
return loading.value || userInfoLoading.value;
});
</script>
<template>
<AuthenticationLogin :loading="loginLoading" @submit="handleLogin" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { AuthenticationQrCodeLogin } from '@vben/common-ui';
defineOptions({ name: 'QrCodeLogin' });
</script>
<template>
<AuthenticationQrCodeLogin />
</template>

View File

@@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { LoginAndRegisterParams } from '@vben/common-ui';
import { AuthenticationRegister } from '@vben/common-ui';
import { ref } from 'vue';
defineOptions({ name: 'Register' });
const loading = ref(false);
function handleSubmit(value: LoginAndRegisterParams) {
// eslint-disable-next-line no-console
console.log('register submit:', value);
}
</script>
<template>
<AuthenticationRegister :loading="loading" @submit="handleSubmit" />
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
defineOptions({ name: 'WelCome' });
</script>
<template>
<div>dashboard</div>
</template>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
defineOptions({ name: 'Menu1' });
onMounted(() => {
// eslint-disable-next-line no-console
console.log('Menu1');
});
</script>
<template>
<div class="p-5">
menu1
<input class="bg-background border-border" />
</div>
</template>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
defineOptions({ name: 'Menu2' });
onMounted(() => {
// eslint-disable-next-line no-console
console.log('Menu2');
});
</script>
<template>
<div class="p-5">
menu2
<input class="bg-background border-border" />
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
defineOptions({ name: 'Menu31' });
onMounted(() => {
// eslint-disable-next-line no-console
console.log('Menu3-1');
});
</script>
<template>
<div class="p-5">
Menu3-1
<input class="bg-background border-border" />
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
defineOptions({ name: 'Menu321' });
onMounted(() => {
// eslint-disable-next-line no-console
console.log('Menu3-2-1');
});
</script>
<template>
<div class="p-5">
menu-3-2-1
<input class="bg-background border-border" />
</div>
</template>

View File

@@ -0,0 +1 @@
export { default } from '@vben/tailwind-config';

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web-app.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"references": [{ "path": "./tsconfig.node.json" }],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"compilerOptions": {
"composite": true,
"noEmit": false
},
"include": ["vite.config.mts"]
}

View File

@@ -0,0 +1,35 @@
import { defineConfig } from '@vben/vite-config';
export default defineConfig({
appcation: {
compress: false,
compressTypes: ['brotli', 'gzip'],
importmap: false,
importmapOptions: {
// 通过 Importmap CDN 方式引入,
// 目前只有esm.sh源兼容性好一点jspm.io对于 esm 入口要求高
defaultProvider: 'esm.sh',
importmap: [
{ name: 'vue' },
{ name: 'pinia' },
{ name: 'vue-router' },
{ name: 'vue-i18n' },
{ name: 'dayjs' },
{ name: 'vue-demi' },
],
},
visualizer: false,
},
vite: {
server: {
proxy: {
'/vben-api': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/vben-api/, ''),
target: 'http://localhost:3000',
ws: true,
},
},
},
},
});