route.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import type { Route } from "#/global";
  2. import type { RouteRecordRaw } from "vue-router";
  3. import { systemRoutes } from "@/router/routes";
  4. import { resolveRoutePath } from "@/utils";
  5. import useUserStore from "@/store/modules/user";
  6. import { cloneDeep } from "lodash-es";
  7. import useSettingsStore from "./settings";
  8. import api from "@/api";
  9. import { ElMessage } from "element-plus";
  10. const useRouteStore = defineStore(
  11. // 唯一ID
  12. "route",
  13. () => {
  14. const settingsStore = useSettingsStore();
  15. const isGenerate = ref(false);
  16. const routesRaw = ref<Route.recordMainRaw[]>([]);
  17. const filesystemRoutesRaw = ref<RouteRecordRaw[]>([]);
  18. const currentRemoveRoutes = ref<(() => void)[]>([]);
  19. // 将多层嵌套路由处理成两层,保留顶层和最子层路由,中间层级将被拍平
  20. function flatAsyncRoutes<T extends RouteRecordRaw>(route: T): T {
  21. if (route.children) {
  22. route.children = flatAsyncRoutesRecursive(
  23. route.children,
  24. [
  25. {
  26. path: route.path,
  27. title: route.meta?.title,
  28. icon: route.meta?.icon,
  29. hide: !route.meta?.breadcrumb && route.meta?.breadcrumb === false,
  30. },
  31. ],
  32. route.path
  33. );
  34. }
  35. return route;
  36. }
  37. function flatAsyncRoutesRecursive(
  38. routes: RouteRecordRaw[],
  39. breadcrumb: Route.breadcrumb[] = [],
  40. baseUrl = ""
  41. ): RouteRecordRaw[] {
  42. const res: RouteRecordRaw[] = [];
  43. routes.forEach((route) => {
  44. if (route.children) {
  45. const childrenBaseUrl = resolveRoutePath(baseUrl, route.path);
  46. const tmpBreadcrumb = cloneDeep(breadcrumb);
  47. tmpBreadcrumb.push({
  48. path: childrenBaseUrl,
  49. title: route.meta?.title,
  50. icon: route.meta?.icon,
  51. hide: !route.meta?.breadcrumb && route.meta?.breadcrumb === false,
  52. });
  53. const tmpRoute = cloneDeep(route);
  54. tmpRoute.path = childrenBaseUrl;
  55. if (!tmpRoute.meta) {
  56. tmpRoute.meta = {};
  57. }
  58. tmpRoute.meta.breadcrumbNeste = tmpBreadcrumb;
  59. delete tmpRoute.children;
  60. res.push(tmpRoute);
  61. const childrenRoutes = flatAsyncRoutesRecursive(
  62. route.children,
  63. tmpBreadcrumb,
  64. childrenBaseUrl
  65. );
  66. childrenRoutes.forEach((item) => {
  67. // 如果 path 一样则覆盖,因为子路由的 path 可能设置为空,导致和父路由一样,直接注册会提示路由重复
  68. if (res.some((v) => v.path === item.path)) {
  69. res.forEach((v, i) => {
  70. if (v.path === item.path) {
  71. res[i] = item;
  72. }
  73. });
  74. } else {
  75. res.push(item);
  76. }
  77. });
  78. } else {
  79. const tmpRoute = cloneDeep(route);
  80. tmpRoute.path = resolveRoutePath(baseUrl, tmpRoute.path);
  81. // 处理面包屑导航
  82. const tmpBreadcrumb = cloneDeep(breadcrumb);
  83. tmpBreadcrumb.push({
  84. path: tmpRoute.path,
  85. title: tmpRoute.meta?.title,
  86. icon: tmpRoute.meta?.icon,
  87. hide:
  88. !tmpRoute.meta?.breadcrumb && tmpRoute.meta?.breadcrumb === false,
  89. });
  90. if (!tmpRoute.meta) {
  91. tmpRoute.meta = {};
  92. }
  93. tmpRoute.meta.breadcrumbNeste = tmpBreadcrumb;
  94. res.push(tmpRoute);
  95. }
  96. });
  97. return res;
  98. }
  99. // 扁平化路由(将三级及以上路由数据拍平成二级)
  100. const flatRoutes = computed(() => {
  101. const returnRoutes: RouteRecordRaw[] = [];
  102. if (settingsStore.settings.app.routeBaseOn !== "filesystem") {
  103. if (routesRaw.value) {
  104. routesRaw.value.forEach((item) => {
  105. const tmpRoutes = cloneDeep(item.children) as RouteRecordRaw[];
  106. tmpRoutes.map((v) => {
  107. if (!v.meta) {
  108. v.meta = {};
  109. }
  110. v.meta.auth = item.meta?.auth ?? v.meta?.auth;
  111. return v;
  112. });
  113. returnRoutes.push(...tmpRoutes);
  114. });
  115. returnRoutes.forEach((item) => flatAsyncRoutes(item));
  116. }
  117. } else {
  118. returnRoutes.push(
  119. ...(cloneDeep(filesystemRoutesRaw.value) as RouteRecordRaw[])
  120. );
  121. }
  122. return returnRoutes;
  123. });
  124. const flatSystemRoutes = computed(() => {
  125. const routes = [...systemRoutes];
  126. routes.forEach((item) => flatAsyncRoutes(item));
  127. return routes;
  128. });
  129. // TODO 将设置 meta.sidebar 的属性转换成 meta.menu ,过渡处理,未来将被弃用
  130. let isUsedDeprecatedAttribute = false;
  131. function converDeprecatedAttribute<T extends Route.recordMainRaw[]>(
  132. routes: T
  133. ): T {
  134. routes.forEach((route) => {
  135. route.children = converDeprecatedAttributeRecursive(route.children);
  136. });
  137. if (isUsedDeprecatedAttribute) {
  138. // turbo-console-disable-next-line
  139. console.warn(
  140. '[Fantastic-admin] 路由配置中的 "sidebar" 属性即将被弃用, 请尽快替换为 "menu" 属性'
  141. );
  142. }
  143. return routes;
  144. }
  145. function converDeprecatedAttributeRecursive(routes: RouteRecordRaw[]) {
  146. if (routes) {
  147. routes.forEach((route) => {
  148. if (typeof route.meta?.sidebar === "boolean") {
  149. isUsedDeprecatedAttribute = true;
  150. route.meta.menu = route.meta.sidebar;
  151. delete route.meta.sidebar;
  152. }
  153. if (route.children) {
  154. converDeprecatedAttributeRecursive(route.children);
  155. }
  156. });
  157. }
  158. return routes;
  159. }
  160. // 生成路由(前端生成)
  161. function generateRoutesAtFront(asyncRoutes: Route.recordMainRaw[]) {
  162. // 设置 routes 数据
  163. routesRaw.value = converDeprecatedAttribute(
  164. cloneDeep(asyncRoutes) as any
  165. );
  166. isGenerate.value = true;
  167. }
  168. // 格式化后端路由数据
  169. // function formatBackRoutes(routes: any, views = import.meta.glob('../../views/**/*.vue')): Route.recordMainRaw[] {
  170. // return routes.map((route: any) => {
  171. // switch (route.component) {
  172. // case 'Layout':
  173. // route.component = () => import('@/layouts/index.vue')
  174. // break
  175. // default:
  176. // if (route.component) {
  177. // route.component = views[`../../views/${route.component}`]
  178. // }
  179. // else {
  180. // delete route.component
  181. // }
  182. // }
  183. // if (route.children) {
  184. // route.children = formatBackRoutes(route.children, views)
  185. // }
  186. // return route
  187. // })
  188. // }
  189. // 格式化后端路由数据
  190. function formatBackRoutes(
  191. routes: any,
  192. views = import.meta.glob("../../views/**/*.vue")
  193. ): Route.recordMainRaw[] {
  194. // 需要从外层移动到meta对象的字段列表
  195. const metaFields = [
  196. "title",
  197. "icon",
  198. "auth",
  199. "menu",
  200. "breadcrumb",
  201. "activeMenu",
  202. "cache",
  203. "noCache",
  204. "link",
  205. ]; // 从外层移动到meta对象的字段列表
  206. return routes
  207. .filter((route: any) => route.type != 2)
  208. .map((route: any) => {
  209. // 处理组件引用
  210. switch (route.component) {
  211. case "Layout":
  212. route.component = () => import("@/layouts/index.vue");
  213. break;
  214. default:
  215. if (route.component) {
  216. route.component = views[`../../views${route.component}.vue`];
  217. if (route.keepAlive === "1") route.cache = true;
  218. if (route.menu == 0) {
  219. route.menu = false;
  220. }
  221. } else {
  222. delete route.component;
  223. }
  224. }
  225. // 处理元数据格式转换:将指定字段从外层移动到meta对象中
  226. const hasMetaFields = metaFields.some(
  227. (field) => route[field] !== undefined
  228. );
  229. if (hasMetaFields) {
  230. // 确保meta对象存在
  231. if (!route.meta) {
  232. route.meta = {};
  233. }
  234. // 批量移动字段
  235. metaFields.forEach((field) => {
  236. if (route[field] !== undefined) {
  237. route.meta[field] = route[field];
  238. delete route[field];
  239. }
  240. });
  241. }
  242. // 递归处理子路由
  243. if (route.children) {
  244. route.children = formatBackRoutes(route.children, views);
  245. }
  246. return route;
  247. });
  248. }
  249. // 生成路由(后端获取)
  250. async function generateRoutesAtBack() {
  251. await api.menuApi
  252. .getRoleMenuTree()
  253. .then((res) => {
  254. if (res.data.length > 0) {
  255. // 设置 routes 数据
  256. routesRaw.value = converDeprecatedAttribute(
  257. formatBackRoutes(res.data) as any
  258. );
  259. isGenerate.value = true;
  260. } else {
  261. // 退出登录
  262. useUserStore().logout();
  263. // 路由获取失败
  264. ElMessage.error("路由获取失败");
  265. }
  266. })
  267. .catch(() => {});
  268. }
  269. // 生成路由(文件系统生成)
  270. function generateRoutesAtFilesystem(asyncRoutes: RouteRecordRaw[]) {
  271. // 设置 routes 数据
  272. filesystemRoutesRaw.value = cloneDeep(asyncRoutes) as any;
  273. isGenerate.value = true;
  274. }
  275. // 记录 accessRoutes 路由,用于登出时删除路由
  276. function setCurrentRemoveRoutes(routes: (() => void)[]) {
  277. currentRemoveRoutes.value = routes;
  278. }
  279. // 清空动态路由
  280. function removeRoutes() {
  281. isGenerate.value = false;
  282. routesRaw.value = [];
  283. filesystemRoutesRaw.value = [];
  284. currentRemoveRoutes.value.forEach((removeRoute) => {
  285. removeRoute();
  286. });
  287. currentRemoveRoutes.value = [];
  288. }
  289. return {
  290. isGenerate,
  291. routesRaw,
  292. currentRemoveRoutes,
  293. flatRoutes,
  294. flatSystemRoutes,
  295. generateRoutesAtFront,
  296. generateRoutesAtBack,
  297. generateRoutesAtFilesystem,
  298. setCurrentRemoveRoutes,
  299. removeRoutes,
  300. };
  301. }
  302. );
  303. export default useRouteStore;