Browse Source

perf: 同步完整版分支代码

i18n
xiaoxian521 3 years ago
parent
commit
eeb1d378f2
  1. 4
      .env.development
  2. 3
      .env.production
  3. 3
      build/index.ts
  4. 1
      mock/asyncRoutes.ts
  5. 2
      package.json
  6. 2
      pnpm-lock.yaml
  7. 1
      public/serverConfig.json
  8. 2
      src/directives/permission/index.ts
  9. 3
      src/layout/components/sidebar/horizontal.vue
  10. 3
      src/layout/components/sidebar/vertical.vue
  11. 2
      src/layout/components/tag/index.scss
  12. 86
      src/layout/components/tag/index.vue
  13. 15
      src/layout/routerView/parent.vue
  14. 3
      src/layout/types.ts
  15. 279
      src/router/index.ts
  16. 25
      src/router/modules/index.ts
  17. 9
      src/router/types.ts
  18. 289
      src/router/utils.ts
  19. 13
      src/store/modules/multiTags.ts
  20. 24
      src/store/modules/permission.ts
  21. 1
      src/store/modules/types.ts
  22. 2
      src/views/login.vue
  23. 6
      src/views/permission/button/index.vue
  24. 6
      src/views/permission/page/index.vue
  25. 2
      types/global.d.ts

4
.env.development

@ -7,6 +7,8 @@ VITE_PUBLIC_PATH = /
# 开发环境代理 # 开发环境代理
VITE_PROXY_DOMAIN = /api VITE_PROXY_DOMAIN = /api
# 开发环境路由历史模式
VITE_ROUTER_HISTORY = "hash"
# 开发环境后端地址 # 开发环境后端地址
VITE_PROXY_DOMAIN_REAL = "http://127.0.0.1:3000" VITE_PROXY_DOMAIN_REAL = "http://127.0.0.1:3000"

3
.env.production

@ -1,5 +1,8 @@
# 线上环境项目打包路径 # 线上环境项目打包路径
VITE_PUBLIC_PATH = / VITE_PUBLIC_PATH = /
# 线上环境路由历史模式
VITE_ROUTER_HISTORY = "hash"
# 线上环境后端地址 # 线上环境后端地址
VITE_PROXY_DOMAIN_REAL = "" VITE_PROXY_DOMAIN_REAL = ""

3
build/index.ts

@ -5,7 +5,8 @@ const warpperEnv = (envConf: Recordable): ViteEnv => {
VITE_PORT: 8848, VITE_PORT: 8848,
VITE_PUBLIC_PATH: "", VITE_PUBLIC_PATH: "",
VITE_PROXY_DOMAIN: "", VITE_PROXY_DOMAIN: "",
VITE_PROXY_DOMAIN_REAL: ""
VITE_PROXY_DOMAIN_REAL: "",
VITE_ROUTER_HISTORY: ""
}; };
for (const envName of Object.keys(envConf)) { for (const envName of Object.keys(envConf)) {

1
mock/asyncRoutes.ts

@ -1,7 +1,6 @@
// 根据角色动态生成路由 // 根据角色动态生成路由
import { MockMethod } from "vite-plugin-mock"; import { MockMethod } from "vite-plugin-mock";
// http://mockjs.com/examples.html#Object
const permissionRouter = { const permissionRouter = {
path: "/permission", path: "/permission",
name: "permission", name: "permission",

2
package.json

@ -58,7 +58,7 @@
"typescript-cookie": "^1.0.0", "typescript-cookie": "^1.0.0",
"vue": "^3.2.24", "vue": "^3.2.24",
"vue-i18n": "^9.2.0-beta.3", "vue-i18n": "^9.2.0-beta.3",
"vue-router": "^4.0.11",
"vue-router": "^4.0.12",
"vue-types": "^4.1.0" "vue-types": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {

2
pnpm-lock.yaml

@ -73,7 +73,7 @@ specifiers:
vue: ^3.2.24 vue: ^3.2.24
vue-eslint-parser: 7.10.0 vue-eslint-parser: 7.10.0
vue-i18n: ^9.2.0-beta.3 vue-i18n: ^9.2.0-beta.3
vue-router: ^4.0.11
vue-router: ^4.0.12
vue-types: ^4.1.0 vue-types: ^4.1.0
dependencies: dependencies:

1
public/serverConfig.json

@ -13,7 +13,6 @@
"HideTabs": false, "HideTabs": false,
"MapConfigure": { "MapConfigure": {
"amapKey": "97b3248d1553172e81f168cf94ea667e", "amapKey": "97b3248d1553172e81f168cf94ea667e",
"baiduKey": "wTHbkkEweiFqZLKunMIjcrb2RcqNXkhc",
"options": { "options": {
"resizeEnable": true, "resizeEnable": true,
"center": [113.6401, 34.72468], "center": [113.6401, 34.72468],

2
src/directives/permission/index.ts

@ -9,7 +9,7 @@ export const auth: Directive = {
const authRoles = value; const authRoles = value;
const hasAuth = usePermissionStoreHook().buttonAuth.includes(authRoles); const hasAuth = usePermissionStoreHook().buttonAuth.includes(authRoles);
if (!hasAuth) { if (!hasAuth) {
el.style.display = "none";
el.parentNode.removeChild(el);
} }
} else { } else {
throw new Error("need roles! Like v-auth=\"['admin','test']\""); throw new Error("need roles! Like v-auth=\"['admin','test']\"");

3
src/layout/components/sidebar/horizontal.vue

@ -29,7 +29,6 @@ const title =
getCurrentInstance().appContext.config.globalProperties.$config?.Title; getCurrentInstance().appContext.config.globalProperties.$config?.Title;
const menuRef = templateRef<ElRef | null>("menu", null); const menuRef = templateRef<ElRef | null>("menu", null);
const routeStore = usePermissionStoreHook();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const routers = useRouter().options.routes; const routers = useRouter().options.routes;
@ -133,7 +132,7 @@ onMounted(() => {
@select="menuSelect" @select="menuSelect"
> >
<sidebar-item <sidebar-item
v-for="route in routeStore.wholeRoutes"
v-for="route in usePermissionStoreHook().wholeMenus"
:key="route.path" :key="route.path"
:item="route" :item="route"
:base-path="route.path" :base-path="route.path"

3
src/layout/components/sidebar/vertical.vue

@ -12,7 +12,6 @@ import { usePermissionStoreHook } from "/@/store/modules/permission";
const route = useRoute(); const route = useRoute();
const pureApp = useAppStoreHook(); const pureApp = useAppStoreHook();
const router = useRouter().options.routes; const router = useRouter().options.routes;
const routeStore = usePermissionStoreHook();
const showLogo = ref(storageLocal.getItem("logoVal") || "1"); const showLogo = ref(storageLocal.getItem("logoVal") || "1");
const isCollapse = computed(() => { const isCollapse = computed(() => {
return !pureApp.getSidebarStatus; return !pureApp.getSidebarStatus;
@ -72,7 +71,7 @@ onBeforeMount(() => {
@select="menuSelect" @select="menuSelect"
> >
<sidebar-item <sidebar-item
v-for="route in routeStore.wholeRoutes"
v-for="route in usePermissionStoreHook().wholeMenus"
:key="route.path" :key="route.path"
:item="route" :item="route"
class="outer-most" class="outer-most"

2
src/layout/components/tag/index.scss

@ -142,7 +142,7 @@
transition: transform 0.5s ease-in-out; transition: transform 0.5s ease-in-out;
.scroll-item { .scroll-item {
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
&:nth-child(1) { &:nth-child(1) {
margin-left: 5px; margin-left: 5px;

86
src/layout/components/tag/index.vue

@ -17,16 +17,18 @@ import closeLeft from "/@/assets/svg/close_left.svg";
import closeOther from "/@/assets/svg/close_other.svg"; import closeOther from "/@/assets/svg/close_other.svg";
import closeRight from "/@/assets/svg/close_right.svg"; import closeRight from "/@/assets/svg/close_right.svg";
import { isEqual } from "lodash-es";
import { emitter } from "/@/utils/mitt"; import { emitter } from "/@/utils/mitt";
import { transformI18n } from "/@/plugins/i18n"; import { transformI18n } from "/@/plugins/i18n";
import { storageLocal } from "/@/utils/storage"; import { storageLocal } from "/@/utils/storage";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { RouteConfigs, tagsViewsType } from "../../types"; import { RouteConfigs, tagsViewsType } from "../../types";
import { handleAliveRoute, delAliveRoutes } from "/@/router";
import { useSettingStoreHook } from "/@/store/modules/settings"; import { useSettingStoreHook } from "/@/store/modules/settings";
import { handleAliveRoute, delAliveRoutes } from "/@/router/utils";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { usePermissionStoreHook } from "/@/store/modules/permission"; import { usePermissionStoreHook } from "/@/store/modules/permission";
import { toggleClass, removeClass, hasClass } from "/@/utils/operate"; import { toggleClass, removeClass, hasClass } from "/@/utils/operate";
import { templateRef, useResizeObserver, useDebounceFn } from "@vueuse/core"; import { templateRef, useResizeObserver, useDebounceFn } from "@vueuse/core";
const route = useRoute(); const route = useRoute();
@ -45,6 +47,61 @@ let multiTags: ComputedRef<Array<RouteConfigs>> = computed(() => {
return useMultiTagsStoreHook()?.multiTags; return useMultiTagsStoreHook()?.multiTags;
}); });
const linkIsActive = computed(() => {
return item => {
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return "is-active";
} else {
return "";
}
} else {
if (isEqual(route?.query, item?.query)) {
return "is-active";
} else {
return "";
}
}
};
});
const scheduleIsActive = computed(() => {
return item => {
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return "schedule-active";
} else {
return "";
}
} else {
if (isEqual(route?.query, item?.query)) {
return "schedule-active";
} else {
return "";
}
}
};
});
const iconIsActive = computed(() => {
return (item, index) => {
if (index === 0) return;
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return true;
} else {
return false;
}
} else {
if (isEqual(route?.query, item?.query)) {
return true;
} else {
return false;
}
}
};
});
const dynamicTagView = () => { const dynamicTagView = () => {
const index = multiTags.value.findIndex(item => { const index = multiTags.value.findIndex(item => {
return item.path === route.path; return item.path === route.path;
@ -228,7 +285,13 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
// //
let delAliveRouteList = []; let delAliveRouteList = [];
let valueIndex: number = multiTags.value.findIndex((item: any) => { let valueIndex: number = multiTags.value.findIndex((item: any) => {
if (item.query) {
if (item.path === obj.path) {
return item.query === obj.query;
}
} else {
return item.path === obj.path; return item.path === obj.path;
}
}); });
const spliceRoute = ( const spliceRoute = (
@ -279,7 +342,8 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
if (tag === "left") return; if (tag === "left") return;
nextTick(() => { nextTick(() => {
router.push({ router.push({
path: newRoute[0].path
path: newRoute[0].path,
query: newRoute[0].query
}); });
}); });
} else { } else {
@ -291,7 +355,8 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
}); });
!isHasActiveTag && !isHasActiveTag &&
router.push({ router.push({
path: newRoute[0].path
path: newRoute[0].path,
query: newRoute[0].query
}); });
} }
} }
@ -477,7 +542,8 @@ function openMenu(tag, e) {
// tags // tags
function tagOnClick(item) { function tagOnClick(item) {
router.push({ router.push({
path: item?.path
path: item?.path,
query: item?.query
}); });
showMenuModel(item?.path); showMenuModel(item?.path);
} }
@ -563,7 +629,7 @@ onBeforeMount(() => {
:key="index" :key="index"
:class="[ :class="[
'scroll-item is-closable', 'scroll-item is-closable',
$route.path === item.path ? 'is-active' : '',
linkIsActive(item),
$route.path === item.path && showModel === 'card' $route.path === item.path && showModel === 'card'
? 'card-active' ? 'card-active'
: '' : ''
@ -573,12 +639,12 @@ onBeforeMount(() => {
@mouseleave.prevent="onMouseleave(item, index)" @mouseleave.prevent="onMouseleave(item, index)"
@click="tagOnClick(item)" @click="tagOnClick(item)"
> >
<router-link :to="item.path">{{
transformI18n(item.meta.title, item.meta.i18n)
}}</router-link>
<router-link :to="item.path"
>{{ transformI18n(item.meta.title, item.meta.i18n) }}
</router-link>
<el-icon <el-icon
v-if=" v-if="
($route.path === item.path && index !== 0) ||
iconIsActive(item, index) ||
(index === activeIndex && index !== 0) (index === activeIndex && index !== 0)
" "
class="el-icon-close" class="el-icon-close"
@ -589,7 +655,7 @@ onBeforeMount(() => {
<div <div
:ref="'schedule' + index" :ref="'schedule' + index"
v-if="showModel !== 'card'" v-if="showModel !== 'card'"
:class="[$route.path === item.path ? 'schedule-active' : '']"
:class="[scheduleIsActive(item)]"
></div> ></div>
</div> </div>
</div> </div>

15
src/layout/routerView/parent.vue

@ -0,0 +1,15 @@
<template>
<router-view>
<template #default="{ Component, route }">
<transition appear name="fade-transform" mode="out-in">
<component :is="Component" :key="route.fullPath" />
</transition>
</template>
</router-view>
</template>
<script lang="ts">
export default {
name: "layoutParentView"
};
</script>

3
src/layout/types.ts

@ -14,13 +14,16 @@ export const routerArrays: Array<RouteConfigs> = [
export type RouteConfigs = { export type RouteConfigs = {
path?: string; path?: string;
parentPath?: string; parentPath?: string;
query?: object;
meta?: { meta?: {
title?: string; title?: string;
i18n?: boolean; i18n?: boolean;
icon?: string; icon?: string;
showLink?: boolean; showLink?: boolean;
savedPosition?: boolean; savedPosition?: boolean;
authority?: Array<string>;
}; };
children?: RouteConfigs[];
name?: string; name?: string;
}; };

279
src/router/index.ts

@ -1,135 +1,27 @@
import {
Router,
RouteMeta,
createRouter,
RouteComponent,
RouteRecordName,
createWebHashHistory,
RouteRecordNormalized
} from "vue-router";
import { toRouteType } from "./types";
import { openLink } from "/@/utils/link"; import { openLink } from "/@/utils/link";
import NProgress from "/@/utils/progress"; import NProgress from "/@/utils/progress";
import { split, uniqBy } from "lodash-es";
import { useTimeoutFn } from "@vueuse/core";
import { RouteConfigs } from "/@/layout/types";
import { constantRoutes } from "./modules";
import { transformI18n } from "/@/plugins/i18n"; import { transformI18n } from "/@/plugins/i18n";
import remainingRouter from "./modules/remaining";
import { split, find, findIndex } from "lodash-es";
import { storageSession, storageLocal } from "/@/utils/storage"; import { storageSession, storageLocal } from "/@/utils/storage";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
// 静态路由
import homeRouter from "./modules/home";
import Layout from "/@/layout/index.vue";
import errorRouter from "./modules/error";
import externalLink from "./modules/externalLink";
import remainingRouter from "./modules/remaining";
// 动态路由
import { getAsyncRoutes } from "/@/api/routes";
// https://cn.vitejs.dev/guide/features.html#glob-import
const modulesRoutes = import.meta.glob("/src/views/*/*/*.vue");
const constantRoutes: Array<RouteComponent> = [
homeRouter,
externalLink,
errorRouter
];
// 按照路由中meta下的rank等级升序来排序路由
export const ascending = arr => {
return arr.sort((a: any, b: any) => {
return a?.meta?.rank - b?.meta?.rank;
});
};
// 将所有静态路由导出
export const constantRoutesArr: Array<RouteComponent> = ascending(
constantRoutes
).concat(...remainingRouter);
// 过滤meta中showLink为false的路由
export const filterTree = data => {
const newTree = data.filter(v => v.meta.showLink);
newTree.forEach(v => v.children && (v.children = filterTree(v.children)));
return newTree;
};
// 从路由中提取keepAlive为true的name组成数组(此处本项目中并没有用到,只是暴露个方法)
export const getAliveRoute = () => {
const alivePageList = [];
const recursiveSearch = treeLists => {
if (!treeLists || !treeLists.length) {
return;
}
for (let i = 0; i < treeLists.length; i++) {
if (treeLists[i]?.meta?.keepAlive) alivePageList.push(treeLists[i].name);
recursiveSearch(treeLists[i].children);
}
};
recursiveSearch(router.options.routes);
return alivePageList;
};
// 批量删除缓存路由
export const delAliveRoutes = (delAliveRouteList: Array<RouteConfigs>) => {
delAliveRouteList.forEach(route => {
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: route?.name
});
});
};
// 处理缓存路由(添加、删除、刷新)
export const handleAliveRoute = (
matched: RouteRecordNormalized[],
mode?: string
) => {
switch (mode) {
case "add":
matched.forEach(v => {
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
});
break;
case "delete":
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: matched[matched.length - 1].name
});
break;
default:
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: matched[matched.length - 1].name
});
useTimeoutFn(() => {
matched.forEach(v => {
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
});
}, 100);
}
};
// 过滤后端传来的动态路由 重新生成规范路由
export const addAsyncRoutes = (arrRoutes: Array<RouteComponent>) => {
if (!arrRoutes || !arrRoutes.length) return;
arrRoutes.forEach((v: any) => {
if (v.redirect) {
v.component = Layout;
} else {
v.component = modulesRoutes[`/src/views${v.path}/index.vue`];
}
if (v.children) {
addAsyncRoutes(v.children);
}
});
return arrRoutes;
};
import { usePermissionStoreHook } from "/@/store/modules/permission";
import { Router, RouteMeta, createRouter, RouteRecordName } from "vue-router";
import {
initRouter,
getHistoryMode,
getParentPaths,
findRouteByPath,
handleAliveRoute
} from "./utils";
// 创建路由实例 // 创建路由实例
export const router: Router = createRouter({ export const router: Router = createRouter({
history: createWebHashHistory(),
routes: ascending(constantRoutes).concat(...remainingRouter),
history: getHistoryMode(),
routes: constantRoutes.concat(...remainingRouter),
strict: true,
scrollBehavior(to, from, savedPosition) { scrollBehavior(to, from, savedPosition) {
return new Promise(resolve => { return new Promise(resolve => {
if (savedPosition) { if (savedPosition) {
@ -145,96 +37,10 @@ export const router: Router = createRouter({
} }
}); });
// 初始化路由
export const initRouter = name => {
return new Promise(resolve => {
getAsyncRoutes({ name }).then(({ info }) => {
if (info.length === 0) {
usePermissionStoreHook().changeSetting(info);
} else {
addAsyncRoutes(info).map((v: any) => {
// 防止重复添加路由
if (
router.options.routes.findIndex(value => value.path === v.path) !==
-1
) {
return;
} else {
// 切记将路由push到routes后还需要使用addRoute,这样路由才能正常跳转
router.options.routes.push(v);
// 最终路由进行升序
ascending(router.options.routes);
router.addRoute(v.name, v);
usePermissionStoreHook().changeSetting(info);
}
resolve(router);
});
}
router.addRoute({
path: "/:pathMatch(.*)",
redirect: "/error/404"
});
});
});
};
// 重置路由
export function resetRouter() {
router.getRoutes().forEach(route => {
const { name } = route;
if (name) {
router.hasRoute(name) && router.removeRoute(name);
}
});
}
function findRouteByPath(path, routes) {
let res = routes.find(item => item.path == path);
if (res) {
return res;
} else {
for (let i = 0; i < routes.length; i++) {
if (
routes[i].children instanceof Array &&
routes[i].children.length > 0
) {
res = findRouteByPath(path, routes[i].children);
if (res) {
return res;
}
}
}
return null;
}
}
function getParentPaths(path, routes) {
// 深度遍历查找
function dfs(routes, path, parents) {
for (let i = 0; i < routes.length; i++) {
const item = routes[i];
// 找到path则返回父级path
if (item.path === path) return parents;
// children不存在或为空则不递归
if (!item.children || !item.children.length) continue;
// 往下查找时将当前path入栈
parents.push(item.path);
if (dfs(item.children, path, parents).length) return parents;
// 深度遍历查找未找到时当前path 出栈
parents.pop();
}
// 未找到时返回空数组
return [];
}
return dfs(routes, path, []);
}
// 路由白名单 // 路由白名单
const whiteList = ["/login"]; const whiteList = ["/login"];
router.beforeEach((to, _from, next) => {
router.beforeEach((to: toRouteType, _from, next) => {
if (to.meta?.keepAlive) { if (to.meta?.keepAlive) {
const newMatched = to.matched; const newMatched = to.matched;
handleAliveRoute(newMatched, "add"); handleAliveRoute(newMatched, "add");
@ -266,7 +72,7 @@ router.beforeEach((to, _from, next) => {
} }
} else { } else {
// 刷新 // 刷新
if (usePermissionStoreHook().wholeRoutes.length === 0)
if (usePermissionStoreHook().wholeMenus.length === 0)
initRouter(name.username).then((router: Router) => { initRouter(name.username).then((router: Router) => {
if (!useMultiTagsStoreHook().getMultiTagsCache) { if (!useMultiTagsStoreHook().getMultiTagsCache) {
const handTag = ( const handTag = (
@ -282,16 +88,43 @@ router.beforeEach((to, _from, next) => {
meta meta
}); });
}; };
const parentPath = to.matched[0]?.path;
// 未开启标签页缓存,刷新页面重定向到顶级路由(参考标签页操作例子,只针对静态路由)
if (to.meta?.realPath) { if (to.meta?.realPath) {
const { path, name, meta } = to.matched[0]?.children[0];
handTag(path, parentPath, name, meta);
return router.push(path);
const routes = router.options.routes;
const { refreshRedirect } = to.meta;
const { name, meta } = findRouteByPath(refreshRedirect, routes);
handTag(
refreshRedirect,
getParentPaths(refreshRedirect, routes)[1],
name,
meta
);
return router.push(refreshRedirect);
} else { } else {
const { path } = to; const { path } = to;
const routes = router.options.routes;
const index = findIndex(remainingRouter, v => {
return v.path == path;
});
const routes =
index === -1
? router.options.routes[0].children
: router.options.routes;
const route = findRouteByPath(path, routes); const route = findRouteByPath(path, routes);
const routePartent = getParentPaths(path, routes); const routePartent = getParentPaths(path, routes);
// 未开启标签页缓存,刷新页面重定向到顶级路由(参考标签页操作例子,只针对动态路由)
if (routePartent.length === 0) {
const { name, meta } = findRouteByPath(
route?.meta?.refreshRedirect,
routes
);
handTag(
route.meta?.refreshRedirect,
getParentPaths(route.meta?.refreshRedirect, routes)[0],
name,
meta
);
return router.push(route.meta?.refreshRedirect);
} else {
handTag( handTag(
route.path, route.path,
routePartent[routePartent.length - 1], routePartent[routePartent.length - 1],
@ -301,10 +134,14 @@ router.beforeEach((to, _from, next) => {
return router.push(path); return router.push(path);
} }
} }
router.push(to.path);
}
router.push(to.fullPath);
// 刷新页面更新标签栏与页面路由匹配 // 刷新页面更新标签栏与页面路由匹配
const localRoutes = storageLocal.getItem("responsive-tags"); const localRoutes = storageLocal.getItem("responsive-tags");
const optionsRoutes = router.options?.routes;
const home = find(router.options?.routes, route => {
return route.path === "/";
});
const optionsRoutes = [home, ...router.options?.routes[0].children];
const newLocalRoutes = []; const newLocalRoutes = [];
optionsRoutes.forEach(ors => { optionsRoutes.forEach(ors => {
localRoutes.forEach(lrs => { localRoutes.forEach(lrs => {
@ -313,10 +150,6 @@ router.beforeEach((to, _from, next) => {
} }
}); });
}); });
storageLocal.setItem(
"responsive-tags",
uniqBy(newLocalRoutes, "path")
);
}); });
next(); next();
} }

25
src/router/modules/index.ts

@ -0,0 +1,25 @@
// 静态路由
import homeRouter from "./home";
import errorRouter from "./error";
import externalLink from "./externalLink";
import remainingRouter from "./remaining";
import { RouteRecordRaw, RouteComponent } from "vue-router";
import {
ascending,
formatTwoStageRoutes,
formatFlatteningRoutes
} from "../utils";
// 原始静态路由(未做任何处理)
const routes = [homeRouter, errorRouter, externalLink];
// 导出处理后的静态路由(三级及以上的路由全部拍成二级)
export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(
formatFlatteningRoutes(ascending(routes))
);
// 用于渲染菜单,保持原始层级
export const constantMenus: Array<RouteComponent> = ascending(routes).concat(
...remainingRouter
);

9
src/router/types.ts

@ -0,0 +1,9 @@
import { RouteLocationNormalized } from "vue-router";
export interface toRouteType extends RouteLocationNormalized {
meta: {
keepAlive: boolean;
refreshRedirect: string;
realPath: string;
};
}

289
src/router/utils.ts

@ -0,0 +1,289 @@
import {
RouterHistory,
RouteRecordRaw,
RouteComponent,
createWebHistory,
createWebHashHistory,
RouteRecordNormalized
} from "vue-router";
import { router } from "./index";
import { loadEnv } from "../../build";
import Layout from "/@/layout/index.vue";
import { useTimeoutFn } from "@vueuse/core";
import { RouteConfigs } from "/@/layout/types";
import { usePermissionStoreHook } from "/@/store/modules/permission";
// https://cn.vitejs.dev/guide/features.html#glob-import
const modulesRoutes = import.meta.glob("/src/views/*/*/*.vue");
// 动态路由
import { getAsyncRoutes } from "/@/api/routes";
// 按照路由中meta下的rank等级升序来排序路由
const ascending = (arr: any[]) => {
return arr.sort(
(a: { meta: { rank: number } }, b: { meta: { rank: number } }) => {
return a?.meta?.rank - b?.meta?.rank;
}
);
};
// 过滤meta中showLink为false的路由
const filterTree = (data: RouteComponent[]) => {
const newTree = data.filter(
(v: { meta: { showLink: boolean } }) => v.meta.showLink
);
newTree.forEach(
(v: { children }) => v.children && (v.children = filterTree(v.children))
);
return newTree;
};
// 批量删除缓存路由(keepalive)
const delAliveRoutes = (delAliveRouteList: Array<RouteConfigs>) => {
delAliveRouteList.forEach(route => {
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: route?.name
});
});
};
// 通过path获取父级路径
const getParentPaths = (path: string, routes: RouteRecordRaw[]) => {
// 深度遍历查找
function dfs(routes: RouteRecordRaw[], path: string, parents: string[]) {
for (let i = 0; i < routes.length; i++) {
const item = routes[i];
// 找到path则返回父级path
if (item.path === path) return parents;
// children不存在或为空则不递归
if (!item.children || !item.children.length) continue;
// 往下查找时将当前path入栈
parents.push(item.path);
if (dfs(item.children, path, parents).length) return parents;
// 深度遍历查找未找到时当前path 出栈
parents.pop();
}
// 未找到时返回空数组
return [];
}
return dfs(routes, path, []);
};
// 查找对应path的路由信息
const findRouteByPath = (path: string, routes: RouteRecordRaw[]) => {
let res = routes.find((item: { path: string }) => item.path == path);
if (res) {
return res;
} else {
for (let i = 0; i < routes.length; i++) {
if (
routes[i].children instanceof Array &&
routes[i].children.length > 0
) {
res = findRouteByPath(path, routes[i].children);
if (res) {
return res;
}
}
}
return null;
}
};
// 重置路由
const resetRouter = (): void => {
router.getRoutes().forEach(route => {
const { name } = route;
if (name) {
router.hasRoute(name) && router.removeRoute(name);
}
});
};
// 初始化路由
const initRouter = (name: string) => {
return new Promise(resolve => {
getAsyncRoutes({ name }).then(({ info }) => {
if (info.length === 0) {
usePermissionStoreHook().changeSetting(info);
} else {
formatFlatteningRoutes(addAsyncRoutes(info)).map(
(v: RouteRecordRaw) => {
// 防止重复添加路由
if (
router.options.routes[0].children.findIndex(
value => value.path === v.path
) !== -1
) {
return;
} else {
// 切记将路由push到routes后还需要使用addRoute,这样路由才能正常跳转
router.options.routes[0].children.push(v);
// 最终路由进行升序
ascending(router.options.routes[0].children);
if (!router.hasRoute(v?.name)) router.addRoute(v);
}
resolve(router);
}
);
usePermissionStoreHook().changeSetting(info);
}
router.addRoute({
path: "/:pathMatch(.*)",
redirect: "/error/404"
});
});
});
};
/**
*
* @param routesList
* @returns
*/
const formatFlatteningRoutes = (routesList: RouteRecordRaw[]) => {
if (routesList.length <= 0) return routesList;
for (let i = 0; i < routesList.length; i++) {
if (routesList[i].children) {
routesList = routesList
.slice(0, i + 1)
.concat(routesList[i].children, routesList.slice(i + 1));
}
}
return routesList;
};
/**
* keep-alive
* https://github.com/xiaoxian521/vue-pure-admin/issues/67
* @param routesList
* @returns
*/
const formatTwoStageRoutes = (routesList: RouteRecordRaw[]) => {
if (routesList.length <= 0) return routesList;
const newRoutesList: RouteRecordRaw[] = [];
routesList.forEach((v: RouteRecordRaw) => {
if (v.path === "/") {
newRoutesList.push({
component: v.component,
name: v.name,
path: v.path,
redirect: v.redirect,
meta: v.meta,
children: []
});
} else {
newRoutesList[0].children.push({ ...v });
}
});
return newRoutesList;
};
// 处理缓存路由(添加、删除、刷新)
const handleAliveRoute = (matched: RouteRecordNormalized[], mode?: string) => {
switch (mode) {
case "add":
matched.forEach(v => {
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
});
break;
case "delete":
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: matched[matched.length - 1].name
});
break;
default:
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: matched[matched.length - 1].name
});
useTimeoutFn(() => {
matched.forEach(v => {
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
});
}, 100);
}
};
// 过滤后端传来的动态路由 重新生成规范路由
const addAsyncRoutes = (arrRoutes: Array<RouteRecordRaw>) => {
if (!arrRoutes || !arrRoutes.length) return;
arrRoutes.forEach((v: RouteRecordRaw) => {
if (v.redirect) {
v.component = Layout;
} else {
if (v.meta.realPath) {
v.component = modulesRoutes[`/src/views${v.meta.realPath}/index.vue`];
} else {
v.component = modulesRoutes[`/src/views${v.path}/index.vue`];
}
}
if (v.children) {
addAsyncRoutes(v.children);
}
});
return arrRoutes;
};
// 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html
const getHistoryMode = (): RouterHistory => {
const routerHistory = loadEnv().VITE_ROUTER_HISTORY;
// len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1
const historyMode = routerHistory.split(",");
const leftMode = historyMode[0];
const rightMode = historyMode[1];
// no param
if (historyMode.length === 1) {
if (leftMode === "hash") {
return createWebHashHistory("");
} else if (leftMode === "h5") {
return createWebHistory("");
}
} //has param
else if (historyMode.length === 2) {
if (leftMode === "hash") {
return createWebHashHistory(rightMode);
} else if (leftMode === "h5") {
return createWebHistory(rightMode);
}
}
};
// 是否有权限
const hasPermissions = (value: Array<string>): boolean => {
if (value && value instanceof Array && value.length > 0) {
const roles = usePermissionStoreHook().buttonAuth;
const permissionRoles = value;
const hasPermission = roles.some(role => {
return permissionRoles.includes(role);
});
if (!hasPermission) {
return false;
}
return true;
} else {
return false;
}
};
export {
ascending,
filterTree,
initRouter,
resetRouter,
hasPermissions,
getHistoryMode,
addAsyncRoutes,
delAliveRoutes,
getParentPaths,
findRouteByPath,
handleAliveRoute,
formatTwoStageRoutes,
formatFlatteningRoutes
};

13
src/store/modules/multiTags.ts

@ -1,5 +1,6 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { store } from "/@/store"; import { store } from "/@/store";
import { isEqual } from "lodash-es";
import { storageLocal } from "/@/utils/storage"; import { storageLocal } from "/@/utils/storage";
import { multiType, positionType } from "./types"; import { multiType, positionType } from "./types";
@ -54,12 +55,18 @@ export const useMultiTagsStore = defineStore({
case "push": case "push":
{ {
const tagVal = value as multiType; const tagVal = value as multiType;
// 判断tag是否已存在:
// 判断tag是否已存在
const tagHasExits = this.multiTags.some(tag => { const tagHasExits = this.multiTags.some(tag => {
return tag.path === tagVal?.path; return tag.path === tagVal?.path;
}); });
if (tagHasExits) return;
// 判断tag中的query键值是否相等
const tagQueryHasExits = this.multiTags.some(tag => {
return isEqual(tag.query, tagVal.query);
});
if (tagHasExits && tagQueryHasExits) return;
const meta = tagVal?.meta; const meta = tagVal?.meta;
const dynamicLevel = meta?.dynamicLevel ?? -1; const dynamicLevel = meta?.dynamicLevel ?? -1;
if (dynamicLevel > 0) { if (dynamicLevel > 0) {
@ -85,10 +92,8 @@ export const useMultiTagsStore = defineStore({
this.multiTags.splice(position?.startIndex, position?.length); this.multiTags.splice(position?.startIndex, position?.length);
this.tagsCache(this.multiTags); this.tagsCache(this.multiTags);
return this.multiTags; return this.multiTags;
break;
case "slice": case "slice":
return this.multiTags.slice(-1); return this.multiTags.slice(-1);
break;
} }
} }
} }

24
src/store/modules/permission.ts

@ -1,28 +1,32 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { store } from "/@/store"; import { store } from "/@/store";
import { cacheType } from "./types"; import { cacheType } from "./types";
import { constantRoutesArr, ascending, filterTree } from "/@/router/index";
import { RouteConfigs } from "/@/layout/types";
import { constantMenus } from "/@/router/modules";
import { ascending, filterTree } from "/@/router/utils";
export const usePermissionStore = defineStore({ export const usePermissionStore = defineStore({
id: "pure-permission", id: "pure-permission",
state: () => ({ state: () => ({
// 静态路由
constantRoutes: constantRoutesArr,
wholeRoutes: [],
// 静态路由生成的菜单
constantMenus,
// 整体路由生成的菜单(静态、动态)
wholeMenus: [],
buttonAuth: [], buttonAuth: [],
// 缓存页面keepAlive // 缓存页面keepAlive
cachePageList: [] cachePageList: []
}), }),
actions: { actions: {
// 获取异步路由菜单
asyncActionRoutes(routes) { asyncActionRoutes(routes) {
if (this.wholeRoutes.length > 0) return;
this.wholeRoutes = filterTree(
ascending(this.constantRoutes.concat(routes))
if (this.wholeMenus.length > 0) return;
this.wholeMenus = filterTree(
ascending(this.constantMenus.concat(routes))
); );
const getButtonAuth = (arrRoutes: Array<string>) => {
const getButtonAuth = (arrRoutes: Array<RouteConfigs>) => {
if (!arrRoutes || !arrRoutes.length) return; if (!arrRoutes || !arrRoutes.length) return;
arrRoutes.forEach((v: any) => {
arrRoutes.forEach((v: RouteConfigs) => {
if (v.meta && v.meta.authority) { if (v.meta && v.meta.authority) {
this.buttonAuth.push(...v.meta.authority); this.buttonAuth.push(...v.meta.authority);
} }
@ -32,7 +36,7 @@ export const usePermissionStore = defineStore({
}); });
}; };
getButtonAuth(this.wholeRoutes);
getButtonAuth(this.wholeMenus);
}, },
async changeSetting(routes) { async changeSetting(routes) {
await this.asyncActionRoutes(routes); await this.asyncActionRoutes(routes);

1
src/store/modules/types.ts

@ -25,6 +25,7 @@ export type multiType = {
path: string; path: string;
parentPath: string; parentPath: string;
name: string; name: string;
query: object;
meta: any; meta: any;
}; };

2
src/views/login.vue

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { initRouter } from "/@/router";
import { initRouter } from "/@/router/utils";
import { storageSession } from "/@/utils/storage"; import { storageSession } from "/@/utils/storage";
import { addClass, removeClass } from "/@/utils/operate"; import { addClass, removeClass } from "/@/utils/operate";
import bg from "/@/assets/login/bg.png"; import bg from "/@/assets/login/bg.png";

6
src/views/permission/button/index.vue

@ -1,3 +1,9 @@
<script lang="ts">
export default {
name: "permissionButton"
};
</script>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { storageSession } from "/@/utils/storage"; import { storageSession } from "/@/utils/storage";

6
src/views/permission/page/index.vue

@ -1,3 +1,9 @@
<script lang="ts">
export default {
name: "permissionPage"
};
</script>
<script setup lang="ts"> <script setup lang="ts">
import { ref, unref } from "vue"; import { ref, unref } from "vue";
import { storageSession } from "/@/utils/storage"; import { storageSession } from "/@/utils/storage";

2
types/global.d.ts

@ -68,6 +68,7 @@ declare global {
VITE_PUBLIC_PATH: string; VITE_PUBLIC_PATH: string;
VITE_PROXY_DOMAIN: string; VITE_PROXY_DOMAIN: string;
VITE_PROXY_DOMAIN_REAL: string; VITE_PROXY_DOMAIN_REAL: string;
VITE_ROUTER_HISTORY: string;
} }
declare interface ServerConfigs { declare interface ServerConfigs {
@ -85,7 +86,6 @@ declare global {
HideTabs?: boolean; HideTabs?: boolean;
MapConfigure?: { MapConfigure?: {
amapKey?: string; amapKey?: string;
baiduKey?: string;
options: { options: {
resizeEnable?: boolean; resizeEnable?: boolean;
center?: number[]; center?: number[];

Loading…
Cancel
Save