feat: 用户登录和用户权限菜单

This commit is contained in:
戴业伟 2024-01-22 18:02:18 +08:00
parent 47d72c9f21
commit 19757a9a49
25 changed files with 789 additions and 262 deletions

View File

@ -2,6 +2,8 @@ import axios, { AxiosResponse, AxiosRequestConfig } from "axios";
import { ResultEnum } from "@/enums/httpEnum"; import { ResultEnum } from "@/enums/httpEnum";
// import { ErrorPageNameMap } from "@/enums/pageEnum"; // import { ErrorPageNameMap } from "@/enums/pageEnum";
// import { redirectErrorPage } from "@/utils"; // import { redirectErrorPage } from "@/utils";
import { storage } from "@/utils";
import { StorageEnum } from "@/enums/storageEnum";
const axiosInstance = axios.create({ const axiosInstance = axios.create({
baseURL: import.meta.env.DEV baseURL: import.meta.env.DEV
@ -13,6 +15,7 @@ const axiosInstance = axios.create({
// 请求拦截器 // 请求拦截器
axiosInstance.interceptors.request.use( axiosInstance.interceptors.request.use(
(config) => { (config) => {
config.headers.Authorization = storage.get(StorageEnum.ZS_ACCESS_TOKEN);
return config; return config;
}, },
(error: AxiosRequestConfig) => { (error: AxiosRequestConfig) => {

View File

@ -17,9 +17,6 @@ export const get = (url: string, params?: object) => {
url: url, url: url,
method: RequestHttpEnum.GET, method: RequestHttpEnum.GET,
params: params, params: params,
headers: {
Authorization: "7df1bd95-bf13-4660-a07d-90b8b1b314e8",
},
}); });
}; };
@ -30,7 +27,6 @@ export const post = (url: string, data?: object, headersType?: string) => {
data: data, data: data,
headers: { headers: {
"Content-Type": headersType || ContentTypeEnum.JSON, "Content-Type": headersType || ContentTypeEnum.JSON,
Authorization: "7df1bd95-bf13-4660-a07d-90b8b1b314e8",
}, },
}); });
}; };

View File

@ -3,12 +3,22 @@ import { get, post } from "@/api/http";
const fix = "/user"; const fix = "/user";
const url = { const url = {
login: `${fix}/login`,
userInfo: `${fix}/userInfo`,
insert: `${fix}/insert`, insert: `${fix}/insert`,
page: `${fix}/page`, page: `${fix}/page`,
update: `${fix}/update`, update: `${fix}/update`,
delete: `${fix}/delete`, delete: `${fix}/delete`,
}; };
export const login = (params: Object) => {
return post(url.login, params);
};
export const getUserInfo = () => {
return get(url.userInfo);
};
export const insertUser = (params: Object) => { export const insertUser = (params: Object) => {
return post(url.insert, params); return post(url.insert, params);
}; };

View File

@ -54,7 +54,6 @@ export function useDataSource(
const sizeField = APISETTING.sizeField; const sizeField = APISETTING.sizeField;
const totalField = APISETTING.totalField; const totalField = APISETTING.totalField;
const listField = APISETTING.listField; const listField = APISETTING.listField;
console.log(listField);
const itemCount = APISETTING.countField; const itemCount = APISETTING.countField;
let pageParams = {}; let pageParams = {};

View File

@ -0,0 +1,13 @@
import logoImage from "@/assets/images/logo.png";
interface WebsiteConfig {
title: string;
logo: string;
loginImage: string;
loginDesc: string;
}
export const websiteConfig: WebsiteConfig = Object.freeze({
title: "中盛起元基础框架",
logo: logoImage,
loginImage: logoImage,
loginDesc: "中盛起元基础框架",
});

View File

@ -2,6 +2,7 @@
* @description: * @description:
*/ */
export enum ResultEnum { export enum ResultEnum {
STATUS = 200,
DATA_SUCCESS = 0, DATA_SUCCESS = 0,
SUCCESS = 2000, SUCCESS = 2000,
SERVER_ERROR = 500, SERVER_ERROR = 500,

View File

@ -1,12 +1,17 @@
import { ResultEnum } from "@/enums/httpEnum"; import { ResultEnum } from "@/enums/httpEnum";
export enum PageEnum { export enum PageEnum {
// 登录
BASE_LOGIN = "/login",
BASE_LOGIN_NAME = "lOGIN",
//重定向
REDIRECT = "/redirect",
REDIRECT_NAME = "Redirect",
// 首页
BASE_HOME = "/system/menu",
// 错误 // 错误
ERROR_PAGE_NAME_403 = "ErrorPage403", ERROR_PAGE_NAME_403 = "ErrorPage403",
ERROR_PAGE_NAME_404 = "ErrorPage404", ERROR_PAGE_NAME_404 = "ErrorPage404",
BASE_LOGIN = "/login",
REDIRECT = "/redirect",
REDIRECT_NAME = "Redirect",
// ERROR_PAGE_NAME_500 = "ErrorPage500", // ERROR_PAGE_NAME_500 = "ErrorPage500",
} }

View File

@ -2,7 +2,9 @@ export enum StorageEnum {
// 全局设置 // 全局设置
ZS_SYSTEM_SETTING_STORE = "YSTEM_SETTING", ZS_SYSTEM_SETTING_STORE = "YSTEM_SETTING",
// token 等信息 // token 等信息
ZS_ACCESS_TOKEN_STORE = "ACCESS_TOKEN", ZS_ACCESS_TOKEN = "ACCESS_TOKEN",
// 登录信息 // 登录信息
ZS_LOGIN_INFO_STORE = "LOGIN_INFO", ZS_LOGIN_INFO_STORE = "LOGIN_INFO",
// 当前用户信息
ZS_CURRENT_USER = "CURRENT_USER",
} }

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { UserOutlined } from "@vicons/antd"; import { UserOutlined } from "@vicons/antd";
import { logout } from "@/utils"; import { useUserStore } from "@/store/modules/user";
const userStore = useUserStore();
// //
const avatarSelect = (key) => { const avatarSelect = (key) => {
switch (key) { switch (key) {
@ -19,7 +20,7 @@ const avatarOptions = [
// 退 // 退
const doLogout = () => { const doLogout = () => {
logout(); userStore.logout();
}; };
</script> </script>

View File

@ -13,11 +13,12 @@
<aside> <aside>
<n-space vertical class="sider-top"> 中盛起元基础框架 </n-space> <n-space vertical class="sider-top"> 中盛起元基础框架 </n-space>
<n-menu <n-menu
:options="generatorMenu(asyncRoutes)" :options="menus"
:collapsed-width="60" :collapsed-width="64"
:collapsed-icon-size="22" :collapsed-icon-size="20"
:indent="24"
:expanded-keys="state.openKeys" :expanded-keys="state.openKeys"
:value="currentRoute.meta?.activeMenu" :value="getSelectedKeys"
@update:value="clickMenuItem" @update:value="clickMenuItem"
@update:expanded-keys="menuExpanded" @update:expanded-keys="menuExpanded"
/> />
@ -28,8 +29,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { asyncRoutes } from "@/router";
import { routerTurnByName, generatorMenu } from "@/utils"; import { routerTurnByName, generatorMenu } from "@/utils";
import { useAsyncRouteStore } from "@/store/modules/asyncRoute";
// //
const currentRoute = useRoute(); const currentRoute = useRoute();
// //
@ -37,12 +39,49 @@ const matched = currentRoute.matched;
const getOpenKeys = const getOpenKeys =
matched && matched.length ? matched.map((item) => item.name) : []; matched && matched.length ? matched.map((item) => item.name) : [];
const asyncRouteStore = useAsyncRouteStore();
const menus = ref<any[]>([]);
const selectedKeys = ref<string>(currentRoute.name as string);
const state = reactive({ const state = reactive({
openKeys: getOpenKeys, openKeys: getOpenKeys,
}); });
//
watch(
() => currentRoute.fullPath,
() => {
console.log(123);
updateMenu();
}
);
function updateSelectedKeys() {
const matched = currentRoute.matched;
state.openKeys = matched.map((item) => item.name);
const activeMenu: string = (currentRoute.meta?.activeMenu as string) || "";
selectedKeys.value = activeMenu
? (activeMenu as string)
: (currentRoute.name as string);
console.log(selectedKeys.value);
}
function updateMenu() {
menus.value = generatorMenu(asyncRouteStore.getMenus);
updateSelectedKeys();
}
const getSelectedKeys = computed(() => {
return unref(selectedKeys);
});
const clickMenuItem = (key: string) => { const clickMenuItem = (key: string) => {
routerTurnByName(key); if (/http(s)?:/.test(key)) {
window.open(key);
} else {
routerTurnByName(key);
}
}; };
// //
@ -63,7 +102,6 @@ function menuExpanded(openKeys: string[]) {
function findChildrenLen(key: string) { function findChildrenLen(key: string) {
if (!key) return false; if (!key) return false;
const subRouteChildren: string[] = []; const subRouteChildren: string[] = [];
const menus = generatorMenu(asyncRoutes);
for (const { children, key } of unref(menus)) { for (const { children, key } of unref(menus)) {
if (children && children.length) { if (children && children.length) {
subRouteChildren.push(key as string); subRouteChildren.push(key as string);
@ -72,7 +110,9 @@ function findChildrenLen(key: string) {
return subRouteChildren.includes(key); return subRouteChildren.includes(key);
} }
// onMounted(() => {}); onMounted(() => {
updateMenu();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -82,6 +122,5 @@ function findChildrenLen(key: string) {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-top: 10px; margin-top: 10px;
// margin-bottom: 20px;
} }
</style> </style>

View File

@ -1,12 +1,12 @@
import { RouteRecordRaw } from "vue-router"; import { RouteRecordRaw } from "vue-router";
import type { AppRouteRecordRaw } from "@/router/types"; import type { AppRouteRecordRaw } from "@/router/types";
import { ErrorPage404, ErrorPage403 } from "@/router/constant"; import { ErrorPage404, ErrorPage403, Layout } from "@/router/constant";
import { PageEnum } from "@/enums/pageEnum"; import { PageEnum } from "@/enums/pageEnum";
// import { GoReload } from "@/components/GoReload"; // import { GoReload } from "@/components/GoReload";
export const LoginRoute: RouteRecordRaw = { export const LoginRoute: RouteRecordRaw = {
path: "/login", path: PageEnum.BASE_LOGIN,
name: "Login", name: PageEnum.BASE_LOGIN_NAME,
component: () => import("@/views/login/index.vue"), component: () => import("@/views/login/index.vue"),
meta: { meta: {
title: "登录", title: "登录",
@ -40,14 +40,25 @@ export const HttpErrorPage: RouteRecordRaw[] = [
// }, // },
]; ];
export const ErrorPageRoute: AppRouteRecordRaw = { export const ErrorPageRoute: RouteRecordRaw = {
path: "/:path(.*)*", path: "/:path(.*)*",
name: "ErrorPage", name: "ErrorPage",
component: ErrorPage404, component: Layout,
meta: { meta: {
title: PageEnum.ERROR_PAGE_NAME_404, title: "ErrorPage",
hideBreadcrumb: true, // hideBreadcrumb: true,
}, },
children: [
{
path: "/:path(.*)*",
name: "ErrorPageSon",
component: ErrorPage404,
meta: {
title: "ErrorPage",
// hideBreadcrumb: true,
},
},
],
}; };
// export const ReloadRoute: AppRouteRecordRaw = { // export const ReloadRoute: AppRouteRecordRaw = {
@ -59,22 +70,23 @@ export const ErrorPageRoute: AppRouteRecordRaw = {
// }, // },
// }; // };
// export const RedirectRoute: AppRouteRecordRaw = { export const RedirectRoute: AppRouteRecordRaw = {
// path: PageEnum.REDIRECT, path: PageEnum.REDIRECT,
// name: PageEnum.REDIRECT_NAME, name: PageEnum.REDIRECT_NAME,
// component: Layout, component: Layout,
// meta: { meta: {
// title: PageEnum.REDIRECT_NAME, title: PageEnum.REDIRECT_NAME,
// }, hideBreadcrumb: true,
// children: [ },
// { children: [
// path: "/redirect/:path(.*)", {
// name: PageEnum.REDIRECT_NAME, path: "/redirect/:path(.*)",
// component: () => import("@/views/redirect/index.vue"), name: PageEnum.REDIRECT_NAME,
// meta: { component: () => import("@/views/redirect/index.vue"),
// title: PageEnum.REDIRECT_NAME, meta: {
// hideBreadcrumb: true, title: PageEnum.REDIRECT_NAME,
// }, hideBreadcrumb: true,
// }, },
// ], },
// }; ],
};

126
src/router/generator.ts Normal file
View File

@ -0,0 +1,126 @@
import { getMenuTree } from "@/api/system/menu";
import { constantRouterIcon } from "./icons";
import { RouteRecordRaw } from "vue-router";
import { Layout, ParentLayout } from "@/router/constant";
// import { storage } from "@/utils";
// import { StorageEnum } from "@/enums/storageEnum";
import type { AppRouteRecordRaw } from "@/router/types";
const Iframe = () => import("@/views/iframe/index.vue");
const LayoutMap = new Map<string, () => Promise<typeof import("*.vue")>>();
LayoutMap.set("Layout", Layout);
LayoutMap.set("iframe", Iframe);
/**
*
* @param routerMap
* @param parent
* @returns {*}
*/
export const generateRoutes = (routerMap, parent?): any[] => {
return routerMap.map((item) => {
const currentRoute: any = {
// 路由地址 动态拼接生成如 /dashboard/workplace
path: `${(parent && parent.path) ?? ""}/${item.path}`,
// 路由名称,建议唯一
name: item.name ?? "",
key: item.name ?? "",
// 该路由对应页面的 组件
component: item.component,
// meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)
meta: {
...item.meta,
label: item.meta.title,
icon: constantRouterIcon[item.meta.icon] || null,
permissions: item.meta.permissions || null,
},
};
// 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠
currentRoute.path = currentRoute.path.replace("//", "/");
// 重定向
item.redirect && (currentRoute.redirect = item.redirect);
// 是否有子菜单,并递归处理
if (item.children && item.children.length > 0) {
//如果未定义 redirect 默认第一个子路由为 redirect
!item.redirect &&
(currentRoute.redirect = `${item.path}/${item.children[0].path}`);
// Recursion
currentRoute.children = generateRoutes(item.children, currentRoute);
}
return currentRoute;
});
};
/**
*
* @returns {Promise<Router>}
*/
export const generateDynamicRoutes = async (): Promise<RouteRecordRaw[]> => {
const { data } = await getMenuTree();
const router = generateRoutes(data);
asyncImportRoute(router);
return router;
};
/**
* views中对应的组件文件
* */
let viewsModules: Record<string, () => Promise<Recordable>>;
export const asyncImportRoute = (
routes: AppRouteRecordRaw[] | undefined
): void => {
viewsModules = viewsModules || import.meta.glob("../views/**/*.{vue,tsx}");
if (!routes) return;
routes.forEach((item) => {
if (!item.component && item.meta?.frameSrc) {
item.component = "iframe";
}
const { component, name } = item;
const { children } = item;
if (component) {
const layoutFound = LayoutMap.get(component as string);
// console.log(layoutFound, "layoutFound");
if (layoutFound) {
item.component = layoutFound;
} else {
item.component = dynamicImport(viewsModules, component as string);
}
} else if (name) {
item.component = ParentLayout;
}
children && asyncImportRoute(children);
});
};
/**
*
* */
export const dynamicImport = (
viewsModules: Record<string, () => Promise<Recordable>>,
component: string
) => {
const keys = Object.keys(viewsModules);
const matchKeys = keys.filter((key) => {
let k = key.replace("../views", "");
const lastIndex = k.lastIndexOf(".");
k = k.substring(0, lastIndex);
return k === component;
});
if (matchKeys?.length === 1) {
const matchKey = matchKeys[0];
return viewsModules[matchKey];
}
if (matchKeys?.length > 1) {
console.warn(
"Please do not create `.vue` and `.TSX` files with the same file name in the same hierarchical directory under the views folder. This will cause dynamic introduction failure"
);
return;
}
};

7
src/router/icons.ts Normal file
View File

@ -0,0 +1,7 @@
import { renderIcon } from "@/utils/index";
import { DashboardOutlined } from "@vicons/antd";
//前端路由图标映射表
export const constantRouterIcon = {
DashboardOutlined: renderIcon(DashboardOutlined),
};

View File

@ -1,49 +1,41 @@
import type { App } from "vue"; import type { App } from "vue";
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"; import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
import { createRouterGuards } from "./router-guards"; import { createRouterGuards } from "./router-guards";
import { HttpErrorPage, LoginRoute, ErrorPageRoute } from "@/router/base"; import { LoginRoute, RedirectRoute } from "@/router/base";
// import type { IModuleType } from "./types";
// import modules from "@/router/modules"; // const modules = import.meta.glob<IModuleType>("./modules/**/*router.ts", {
import type { IModuleType } from "./types"; // eager: true,
// });
const modules = import.meta.glob<IModuleType>("./modules/**/*router.ts", { // const routeModuleList: RouteRecordRaw[] = Object.keys(modules).reduce<
eager: true, // RouteRecordRaw[]
}); // >((list, key) => {
// const mod = modules[key].default ?? {};
// const modList = Array.isArray(mod) ? [...mod] : [mod];
// return [...list, ...modList];
// }, []);
const routeModuleList: RouteRecordRaw[] = Object.keys(modules).reduce< // function sortRoute(a, b) {
RouteRecordRaw[] // return (a.meta?.sort ?? 0) - (b.meta?.sort ?? 0);
>((list, key) => { // }
const mod = modules[key].default ?? {};
const modList = Array.isArray(mod) ? [...mod] : [mod];
return [...list, ...modList];
}, []);
function sortRoute(a, b) { // routeModuleList.sort(sortRoute);
return (a.meta?.sort ?? 0) - (b.meta?.sort ?? 0);
}
routeModuleList.sort(sortRoute);
export const RootRoute: RouteRecordRaw = { export const RootRoute: RouteRecordRaw = {
path: "/", path: "/",
name: "Root", name: "Root",
redirect: "/system", redirect: "/system/menu",
meta: { meta: {
title: "Root", title: "Root",
}, },
}; };
//需要验证权限 //需要验证权限
export const asyncRoutes = [...routeModuleList]; export const asyncRoutes = [];
const constantRouter: any[] = [ //普通路由 无需验证权限
LoginRoute, export const constantRouter: any[] = [LoginRoute, RootRoute, RedirectRoute];
RootRoute,
...asyncRoutes,
ErrorPageRoute,
...HttpErrorPage,
];
/** /**
* *

View File

@ -1,52 +0,0 @@
import { RouteRecordRaw } from "vue-router";
import { Layout } from "@/router/constant";
import { renderIcon } from "@/utils";
// import { icons } from "@/plugins";
import { OptionsSharp } from "@vicons/ionicons5";
// 引入路径
const importPath = {
USER: () => import("@/views/system/user/user.vue"),
ROLE: () => import("@/views/system/role/role.vue"),
MENU: () => import("@/views/system/menu/menu.vue"),
};
const systemRoutes: RouteRecordRaw = {
path: "/system",
name: "System",
redirect: "/system/menu",
component: Layout,
meta: {
title: "系统设置",
icons: renderIcon(OptionsSharp),
sort: 1,
},
children: [
{
path: "role",
name: "system_role",
meta: {
title: "角色管理",
},
component: () => import("@/views/system/role/role.vue"),
},
{
path: "user",
name: "system_user",
meta: {
title: "用户管理",
},
component: () => import("@/views/system/user/user.vue"),
},
{
path: "menu",
name: "system_menu",
meta: {
title: "菜单管理",
},
component: importPath["MENU"],
},
],
};
export default systemRoutes;

View File

@ -1,35 +1,123 @@
import { Router } from "vue-router"; import { Router, isNavigationFailure } from "vue-router";
import type { RouteRecordRaw } from "vue-router";
import { loginCheck } from "@/utils/router"; import { loginCheck } from "@/utils/router";
import { useUser } from "@/store/modules/user";
import { useAsyncRoute } from "@/store/modules/asyncRoute";
import { PageEnum } from "@/enums/pageEnum";
import { ErrorPageRoute } from "@/router/base";
export function createRouterGuards(router: Router) { const LOGIN_PATH = PageEnum.BASE_LOGIN;
const whitePathList = [LOGIN_PATH]; // 没有重定向白名单
export async function createRouterGuards(router: Router) {
const userStore = useUser();
const asyncRouteStore = useAsyncRoute();
// 前置; // 前置;
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const Loading = window["$loading"]; const Loading = window["$loading"] || null;
Loading && Loading.start(); Loading && Loading.start();
const isErrorPage = router
.getRoutes() if (from.path === LOGIN_PATH && to.name === "errorPage") {
.findIndex((item) => item.name === to.name); next(PageEnum.BASE_HOME);
if (isErrorPage === -1) { return;
next({ name: "ErrorPage404" }); }
if (whitePathList.includes(to.path as PageEnum)) {
next();
return;
} }
if (!loginCheck()) { if (!loginCheck()) {
if (to.name === "Login") { // 不需要登录鉴权的路由
if (to.meta.ignoreAuth) {
next(); next();
return;
} }
next({ name: "Login" }); // 重定向到登录页
const redirectData: {
path: string;
replace: boolean;
query?: Recordable<string>;
} = {
path: LOGIN_PATH,
replace: true,
};
if (to.path) {
redirectData.query = {
...redirectData.query,
redirect: to.path,
};
}
next(redirectData);
return;
} }
next(); if (asyncRouteStore.getIsDynamicRouteAdded) {
}); next();
return;
}
router.afterEach((to) => { await userStore.getInfo();
const Loading = window["$loading"]; const routes = await asyncRouteStore.generateRoutes();
document.title = (to?.meta?.title as string) || document.title;
// 动态添加可访问路由表
routes.forEach((item: unknown) => {
router.addRoute(item as unknown as RouteRecordRaw);
});
//添加404
const isErrorPage = router
.getRoutes()
.findIndex((item) => item.name === ErrorPageRoute.name);
if (isErrorPage === -1) {
router.addRoute(ErrorPageRoute as unknown as RouteRecordRaw);
}
const redirectPath = (from.query.redirect || to.path) as string;
const redirect = decodeURIComponent(redirectPath);
const nextData =
to.path === redirect ? { ...to, replace: true } : { path: redirect };
asyncRouteStore.setDynamicRouteAdded(true);
next(nextData);
Loading && Loading.finish();
});
router.afterEach((to, _, failure) => {
document.title = (to?.meta?.title as string) || document.title;
if (isNavigationFailure(failure)) {
console.log("导航失败", failure);
}
const asyncRouteStore = useAsyncRoute();
// 在这里设置需要缓存的组件名称
const keepAliveComponents = asyncRouteStore.keepAliveComponents;
const currentComName: any = to.matched.find(
(item) => item.name == to.name
)?.name;
if (
currentComName &&
!keepAliveComponents.includes(currentComName) &&
to.meta?.keepAlive
) {
// 需要缓存的组件
keepAliveComponents.push(currentComName);
} else if (!to.meta?.keepAlive || to.name == "Redirect") {
// 不需要缓存的组件
const index = asyncRouteStore.keepAliveComponents.findIndex(
(name) => name == currentComName
);
if (index != -1) {
keepAliveComponents.splice(index, 1);
}
}
asyncRouteStore.setKeepAliveComponents(keepAliveComponents);
const Loading = window["$loading"] || null;
Loading && Loading.finish(); Loading && Loading.finish();
}); });
// 错误
router.onError((error) => { router.onError((error) => {
console.log(error, "路由错误"); console.log(error, "路由错误");
}); });

View File

@ -0,0 +1,107 @@
import { defineStore } from "pinia";
import { RouteRecordRaw } from "vue-router";
import { store } from "@/store";
import { asyncRoutes, constantRouter } from "@/router/index";
import { generateDynamicRoutes } from "@/router/generator";
interface TreeHelperConfig {
id: string;
children: string;
pid: string;
}
const DEFAULT_CONFIG: TreeHelperConfig = {
id: "id",
children: "children",
pid: "pid",
};
const getConfig = (config: Partial<TreeHelperConfig>) =>
Object.assign({}, DEFAULT_CONFIG, config);
export interface IAsyncRouteState {
menus: RouteRecordRaw[];
routers: any[];
routersAdded: any[];
keepAliveComponents: string[];
isDynamicRouteAdded: boolean;
}
function filter<T = any>(
tree: T[],
func: (n: T) => boolean,
config: Partial<TreeHelperConfig> = {}
): T[] {
config = getConfig(config);
const children = config.children as string;
function listFilter(list: T[]) {
return list
.map((node: any) => ({ ...node }))
.filter((node) => {
node[children] = node[children] && listFilter(node[children]);
return func(node) || (node[children] && node[children].length);
});
}
return listFilter(tree);
}
export const useAsyncRouteStore = defineStore({
id: "app-async-route",
state: (): IAsyncRouteState => ({
menus: [],
routers: constantRouter,
routersAdded: [],
keepAliveComponents: [],
// Whether the route has been dynamically added
isDynamicRouteAdded: false,
}),
getters: {
getMenus(): RouteRecordRaw[] {
return this.menus;
},
getIsDynamicRouteAdded(): boolean {
return this.isDynamicRouteAdded;
},
},
actions: {
getRouters() {
return toRaw(this.routersAdded);
},
setDynamicRouteAdded(added: boolean) {
this.isDynamicRouteAdded = added;
},
// 设置动态路由
setRouters(routers: RouteRecordRaw[]) {
this.routersAdded = routers;
this.routers = constantRouter.concat(routers);
},
setMenus(menus: RouteRecordRaw[]) {
// 设置动态路由
this.menus = menus;
},
setKeepAliveComponents(compNames: string[]) {
// 设置需要缓存的组件
this.keepAliveComponents = compNames;
},
async generateRoutes() {
let accessedRouters;
// 动态获取菜单
try {
accessedRouters = await generateDynamicRoutes();
} catch (error) {
console.log(error);
}
// accessedRouters = accessedRouters.filter(routeFilter);
this.setRouters(accessedRouters);
this.setMenus(accessedRouters);
return toRaw(accessedRouters);
},
},
});
// Need to be used outside the setup
export function useAsyncRoute() {
return useAsyncRouteStore(store);
}

106
src/store/modules/user.ts Normal file
View File

@ -0,0 +1,106 @@
import { defineStore } from "pinia";
import { store } from "@/store";
import { StorageEnum } from "@/enums/storageEnum";
import { ResultEnum } from "@/enums/httpEnum";
import { storage, routerTurnByName } from "@/utils";
import { login, getUserInfo } from "@/api/system/user";
import { PageEnum } from "@/enums/pageEnum";
export type UserInfoType = {
// TODO: add your own data
name: string;
email: string;
};
export interface IUserState {
token: string;
username: string;
welcome: string;
avatar: string;
permissions: any[];
info: UserInfoType;
}
export const useUserStore = defineStore({
id: "app-user",
state: (): IUserState => ({
token: storage.get(StorageEnum.ZS_ACCESS_TOKEN, ""),
username: "",
welcome: "",
avatar: "",
permissions: [],
info: storage.get(StorageEnum.ZS_CURRENT_USER, {}),
}),
getters: {
getToken(): string {
return this.token;
},
getAvatar(): string {
return this.avatar;
},
getNickname(): string {
return this.username;
},
getPermissions(): [any][] {
return this.permissions;
},
getUserInfo(): UserInfoType {
return this.info;
},
},
actions: {
setToken(token: string) {
this.token = token;
},
setAvatar(avatar: string) {
this.avatar = avatar;
},
setPermissions(permissions) {
this.permissions = permissions;
},
setUserInfo(info: UserInfoType) {
this.info = info;
},
// 登录
async login(params: any) {
const res = await login(params);
const { data } = res;
if (res.status === ResultEnum.STATUS) {
const ex = 7 * 24 * 60 * 60;
storage.set(StorageEnum.ZS_ACCESS_TOKEN, data, ex);
this.setToken(data);
// this.setUserInfo(result);
}
return res;
},
// 获取用户信息
async getInfo() {
const { data } = await getUserInfo();
console.log(data, "用户信息");
// if (result.permissions && result.permissions.length) {
// const permissionsList = result.permissions;
// this.setPermissions(permissionsList);
this.setUserInfo(data);
// } else {
// throw new Error("getInfo: permissionsList must be a non-null array !");
// }
this.setAvatar(data.avatar);
return data;
},
// 登出
async logout() {
this.setPermissions([]);
this.setUserInfo({ name: "", email: "" });
storage.remove(StorageEnum.ZS_ACCESS_TOKEN);
storage.remove(StorageEnum.ZS_CURRENT_USER);
routerTurnByName(PageEnum.BASE_LOGIN_NAME);
},
},
});
// Need to be used outside the setup
export function useUser() {
return useUserStore(store);
}

View File

@ -22,12 +22,10 @@
width: 10px; width: 10px;
height: 22px; height: 22px;
content: ""; content: "";
background: linear-gradient( background: linear-gradient(90deg,
90deg, #3db8ff 0%,
#3db8ff 0%, rgb(53 138 225 / 77%) 52%,
rgb(53 138 225 / 77%) 52%, #3db8ff 100%);
#3db8ff 100%
);
} }
} }
} }
@ -47,4 +45,4 @@
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: rgb(123 157 213 / 50%); background: rgb(123 157 213 / 50%);
border-radius: 4px; border-radius: 4px;
} }

View File

@ -152,7 +152,7 @@ export function on(
export function off( export function off(
element: Element | HTMLElement | Document | Window, element: Element | HTMLElement | Document | Window,
event: string, event: string,
handler: Fn handler: any
): void { ): void {
if (element && event && handler) { if (element && event && handler) {
element.removeEventListener(event, handler, false); element.removeEventListener(event, handler, false);
@ -161,9 +161,9 @@ export function off(
/* istanbul ignore next */ /* istanbul ignore next */
export function once(el: HTMLElement, event: string, fn: EventListener): void { export function once(el: HTMLElement, event: string, fn: EventListener): void {
const listener = function (this: any, ...args: unknown[]) { const listener: EventListener = function (this: any, evt: Event) {
if (fn) { if (fn) {
fn.apply(this, args); fn.call(this, evt);
} }
off(el, event, listener); off(el, event, listener);
}; };

View File

@ -38,11 +38,13 @@ export function generatorMenu(routerMap: Array<any>) {
key: info.name, key: info.name,
icon: isRoot ? item.meta?.icons : info.meta?.icons, icon: isRoot ? item.meta?.icons : info.meta?.icons,
}; };
// 是否有子菜单,并递归处理 // 是否有子菜单,并递归处理
if (info.children && info.children.length > 0) { if (info.children && info.children.length > 0) {
// Recursion // Recursion
currentMenu.children = generatorMenu(info.children); currentMenu.children = generatorMenu(info.children);
} }
return currentMenu; return currentMenu;
}); });
} }

View File

@ -1,9 +1,10 @@
import router from "@/router"; import router from "@/router";
import { ResultEnum } from "@/enums/httpEnum"; import { ResultEnum } from "@/enums/httpEnum";
import { ErrorPageNameMap } from "@/enums/pageEnum"; import { ErrorPageNameMap } from "@/enums/pageEnum";
import { clearLocalStorage, getLocalStorage } from "./storage";
import { StorageEnum } from "@/enums/storageEnum";
import { cryptoDecode } from "./crypto"; import { cryptoDecode } from "./crypto";
import { StorageEnum } from "@/enums/storageEnum";
import { storage } from "@/utils/storage";
/** /**
* * * *
* @param pageName * @param pageName
@ -73,23 +74,16 @@ export const openNewWindow = (url: string) => {
*/ */
export const loginCheck = () => { export const loginCheck = () => {
try { try {
const info = getLocalStorage(StorageEnum.ZS_LOGIN_INFO_STORE); const token = storage.get(StorageEnum.ZS_ACCESS_TOKEN);
if (!info) return false;
const decodeInfo = cryptoDecode(info);
if (decodeInfo) { if (token) return true;
return true; // const decodeInfo = cryptoDecode(info);
}
return false; // if (decodeInfo) {
// return true;
// }
// return false;
} catch (error) { } catch (error) {
return false; return false;
} }
}; };
/**
* * 退
*/
export const logout = () => {
clearLocalStorage(StorageEnum.ZS_LOGIN_INFO_STORE);
routerTurnByName("Login");
};

View File

@ -1,70 +1,128 @@
// 默认缓存期限为7天
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7;
/** /**
* * *
* @param k * @param {string=} prefixKey -
* @param v stringiiy * @param {Object} [storage=localStorage] - sessionStorage | localStorage
* @returns RemovableRef
*/ */
export const setLocalStorage = <T>(k: string, v: T) => {
try { export default class Storage {
window.localStorage.setItem(k, JSON.stringify(v)); private storage: globalThis.Storage;
} catch (error) { private prefixKey?: string;
return false;
constructor(prefixKey = "", storage = localStorage) {
this.storage = storage;
this.prefixKey = prefixKey;
} }
};
/** private getKey(key: string) {
* * return `${this.prefixKey}${key}`.toUpperCase();
* @param k
* @returns any
*/
export const getLocalStorage = (k: string) => {
const item = window.localStorage.getItem(k);
try {
return item ? JSON.parse(item) : item;
} catch (err) {
return item;
} }
};
/** /**
* * * @description
* @param name * @param {string} key
*/ * @param {*} value
export const clearLocalStorage = (name: string) => { * @param expire
window.localStorage.removeItem(name); */
}; set(key: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) {
const stringData = JSON.stringify({
/** value,
* * expire: expire !== null ? new Date().getTime() + expire * 1000 : null,
* @param k });
* @param v this.storage.setItem(this.getKey(key), stringData);
* @returns RemovableRef
*/
export const setSessionStorage = <T>(k: string, v: T) => {
try {
window.sessionStorage.setItem(k, JSON.stringify(v));
} catch (error) {
return false;
} }
};
/** /**
* * *
* @returns any * @param {string} key
*/ * @param {*=} def
export const getSessionStorage: (k: string) => any = (k: string) => { */
const item = window.sessionStorage.getItem(k); get(key: string, def: any = null) {
try { const item = this.storage.getItem(this.getKey(key));
return item ? JSON.parse(item) : item; if (item) {
} catch (err) { try {
return item; const data = JSON.parse(item);
const { value, expire } = data;
// 在有效期内直接返回
if (expire === null || expire >= Date.now()) {
return value;
}
this.remove(key);
} catch (e) {
return def;
}
}
return def;
} }
};
/** /**
* * *
* @param name * @param {string} key
*/ */
export const clearSessioStorage = (name: string) => { remove(key: string) {
window.sessionStorage.removeItem(name); this.storage.removeItem(this.getKey(key));
}; }
/**
*
* @memberOf Cache
*/
clear(): void {
this.storage.clear();
}
/**
* cookie
* @param {string} name cookie
* @param {*} value cookie
* @param {number=} expire
*
* @example
*/
setCookie(
name: string,
value: any,
expire: number | null = DEFAULT_CACHE_TIME
) {
document.cookie = `${this.getKey(name)}=${value}; Max-Age=${expire}`;
}
/**
* cookie值
* @param name
*/
getCookie(name: string): string {
const cookieArr = document.cookie.split("; ");
for (let i = 0, length = cookieArr.length; i < length; i++) {
const kv = cookieArr[i].split("=");
if (kv[0] === this.getKey(name)) {
return kv[1];
}
}
return "";
}
/**
* cookie
* @param {string} key
*/
removeCookie(key: string) {
this.setCookie(key, 1, -1);
}
/**
* cookie使cookie失效
*/
clearCookie(): void {
const keys = document.cookie.match(/[^ =;]+(?==)/g);
if (keys) {
for (let i = keys.length; i--; ) {
document.cookie = keys[i] + "=0;expire=" + new Date(0).toUTCString();
}
}
}
}
export const storage = new Storage("");

View File

@ -61,12 +61,19 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref } from "vue";
import { PersonOutline, LockClosedOutline } from "@vicons/ionicons5"; import { PersonOutline, LockClosedOutline } from "@vicons/ionicons5";
import logoImage from "@/assets/images/logo.png"; import { useUserStore } from "@/store/modules/user";
import { routerTurnByName, cryptoEncode, setLocalStorage } from "@/utils"; // import { routerTurnByName, cryptoEncode } from "@/utils";
import { StorageEnum } from "@/enums/storageEnum"; // import { StorageEnum } from "@/enums/storageEnum";
const { ZS_LOGIN_INFO_STORE } = StorageEnum; import { ResultEnum } from "@/enums/httpEnum";
import { PageEnum } from "@/enums/pageEnum";
import { useRoute, useRouter } from "vue-router";
import { websiteConfig } from "@/config/website.config";
const userStore = useUserStore();
const router = useRouter();
const route = useRoute();
interface FormState { interface FormState {
username: string; username: string;
password: string; password: string;
@ -77,7 +84,7 @@ const loading = ref(false);
const formInline = reactive({ const formInline = reactive({
username: "admin", username: "admin",
password: "zsqy123", password: "123456",
isCaptcha: true, isCaptcha: true,
}); });
@ -86,16 +93,9 @@ const rules = {
password: { required: true, message: "请输入密码", trigger: "blur" }, password: { required: true, message: "请输入密码", trigger: "blur" },
}; };
const websiteConfig = { const handleSubmit = (e) => {
title: "中盛起元基础框架",
logo: logoImage,
loginImage: logoImage,
loginDesc: "中盛起元基础框架",
};
const handleSubmit = (e: any) => {
e.preventDefault(); e.preventDefault();
formRef.value.validate(async (errors: any) => { formRef.value.validate(async (errors) => {
if (!errors) { if (!errors) {
const { username, password } = formInline; const { username, password } = formInline;
window["$message"].loading("登录中..."); window["$message"].loading("登录中...");
@ -106,27 +106,25 @@ const handleSubmit = (e: any) => {
password, password,
}; };
if (params.username === "admin" && params.password === "zsqy123") { try {
window["$message"].success("登录成功,即将进入系统"); const { status } = await userStore.login(params);
setLocalStorage( window["$message"].destroyAll();
ZS_LOGIN_INFO_STORE, if (status == ResultEnum.STATUS) {
cryptoEncode( const toPath = decodeURIComponent(
JSON.stringify({ (route.query?.redirect || "/") as string
username, );
password, window["$message"].success("登录成功,即将进入系统");
}) if (route.name === PageEnum.BASE_LOGIN_NAME) {
) router.replace("/");
); } else router.replace(toPath);
setTimeout(() => { } else {
routerTurnByName("Root", true); window["$message"].info("登录失败");
}, 3000); }
} else { } finally {
loading.value = false; loading.value = false;
window["$message"].error("用户名或密码错误");
} }
} else { } else {
loading.value = false; window["$message"].error("请填写完整信息,并且进行验证码校验");
window["$message"].error("用户名或密码错误");
} }
}); });
}; };

View File

@ -0,0 +1,22 @@
<script lang="tsx">
import { defineComponent, onBeforeMount } from "vue";
import { useRoute, useRouter } from "vue-router";
import { NEmpty } from "naive-ui";
export default defineComponent({
name: "Redirect",
setup() {
const route = useRoute();
const router = useRouter();
onBeforeMount(() => {
const { params, query } = route;
const { path } = params;
router.replace({
path: "/" + (Array.isArray(path) ? path.join("/") : path),
query,
});
});
return () => <NEmpty />;
},
});
</script>