项目梳理

文件目录

项目根目录结构
├── PROXY.md
├── README.md # 说明文档
├── cache.dockerfile
├── commitlint.config.js # commintlint 规范
├── docker # docker 部署配置文件
│ └── nginx.conf # 若开启 browerhistroy 可参考配置
├── docs
├── globals.d.ts
├── index.html # 主 html 文件
├── jsx.d.ts
├── mock # mock 目录
│ └── index.ts
├── node_modules # 项目依赖
├── package-lock.json
├── package.json
├── public
│ └── favicon.ico
├── shims-vue.d.ts
├── src # 页面代码
├── tsconfig.json
└── vite.config.js # vite 配置文件

src目录如下
├─assets #图片资源,svg图标等
├─components #公共组件库
├─config #全局配置文件,颜色、请求代理、样式、全局变量
├─constants #全局静态配置文件,表单
├─hooks
├─layouts #布局,头部、尾部、内容、菜单、面包屑、全局通知、搜索、设置
├─pages
├─router #路由表
│ └─modules
├─service #网络请求封装 request,原先在util中,现统一
│ └─api #所有api封装
├─store #vuex
│ └─modules
├─style #全局样式
│ └─theme #主题包
└─utils #全局工具类

菜单生成

layouts/components/MenuContent.ts的useRenderNav方法加入以下逻辑

  1. // 对于meta中hide的路由不生成菜单
  2. if (item.meta.hide) {
  3. return undefined;
  4. }

路由配置

  • meta.hide 隐藏菜单,需改写useRenderNav逻辑,如上
  • meta.single 只显示一级菜单
  • 动态路由目前只支持2级,所以更深的配置到同级加上冗余的路径名,配置如下:
    1. {
    2. path: '/project',
    3. name: 'project',
    4. meta: { title: '项目', icon: 'server' },
    5. component: Layout,
    6. children: [
    7. {
    8. path: 'manage',
    9. name: 'projectManage',
    10. component: () => import('@/pages/project/manage/index.vue'),
    11. meta: { title: '项目管理' },
    12. },
    13. {
    14. path: 'add',
    15. name: 'projectAdd',
    16. component: () => import('@/pages/project/add/index.vue'),
    17. meta: { title: '新建项目', hide: true },
    18. },
    19. {
    20. path: ':projectId',
    21. name: 'projectShow',
    22. component: () => import('@/pages/project/show/index.vue'),
    23. meta: { title: '查看项目', hide: true },
    24. },
    25. {
    26. path: ':projectId/floor/:floorId',
    27. name: 'floorShow',
    28. component: () => import('@/pages/floor/show/index.vue'),
    29. meta: { title: '查看楼层', hide: true },
    30. },
    31. {
    32. path: ':projectId/floor/add',
    33. name: 'floorAdd',
    34. component: () => import('@/pages/floor/add/index.vue'),
    35. meta: { title: '增加楼层', hide: true },
    36. },
    37. ],
    38. },

    网络请求封装

    原项目在utils目录下,现service/request.ts

提供基础的get/post/upload方法,主要看HTTP接口中的泛型使用(83行),配合api的index中方法传入该泛型(定义在types中)

  1. import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
  2. import { MessagePlugin } from 'tdesign-vue-next';
  3. import qs from 'qs';
  4. import proxy from '../config/proxy';
  5. import { TOKEN_NAME } from '@/config/global';
  6. import { showMessage } from './status';
  7. const env = import.meta.env.MODE || 'development';
  8. const host = env === 'mock' ? '/' : proxy[env].host; // 如果是mock模式 就不配置host 会走本地Mock拦截
  9. const instance = axios.create({
  10. baseURL: host,
  11. timeout: 20000,
  12. // withCredentials: true,
  13. });
  14. const reqMethods = ['get', 'head'];
  15. instance.interceptors.request.use((config: AxiosRequestConfig) => {
  16. const token = localStorage.getItem(TOKEN_NAME);
  17. if (token) {
  18. config.headers[TOKEN_NAME] = `Bearer ${token}`;
  19. }
  20. // 数组参数序列化
  21. if (reqMethods.includes(config.method)) {
  22. // eslint-disable-next-line func-names
  23. config.paramsSerializer = function (params) {
  24. return qs.stringify(params, { arrayFormat: 'repeat' });
  25. };
  26. }
  27. return config;
  28. });
  29. instance.defaults.timeout = 5000;
  30. instance.interceptors.response.use(
  31. (response: AxiosResponse) => {
  32. const token = response.headers.authorization;
  33. if (token) {
  34. localStorage.setItem(TOKEN_NAME, token);
  35. }
  36. if (response.status !== 200) {
  37. showMessage(response.status);
  38. }
  39. return response;
  40. },
  41. (err) => {
  42. // 错误提示
  43. const { response } = err;
  44. if (response) {
  45. const {
  46. data: { message },
  47. } = response;
  48. if (message) {
  49. MessagePlugin.error(message);
  50. } else {
  51. showMessage(response.status);
  52. }
  53. } else {
  54. MessagePlugin.error('网络连接异常,请稍后再试');
  55. }
  56. const { config } = err;
  57. if (!config || !config.retry) return Promise.reject(err);
  58. config.retryCount = config.retryCount || 0;
  59. if (config.retryCount >= config.retry) {
  60. return Promise.reject(err);
  61. }
  62. config.retryCount += 1;
  63. const backoff = new Promise((resolve) => {
  64. setTimeout(() => {
  65. resolve(null);
  66. }, config.retryDelay || 1);
  67. });
  68. return backoff.then(() => instance(config));
  69. },
  70. );
  71. // 封装常用请求,这里是重点泛型T由封装api的方法传入
  72. interface Http {
  73. get<T>(url: string, params?: unknown): Promise<T>;
  74. post<T>(url: string, params?: unknown): Promise<T>;
  75. upload<T>(url: string, params: unknown): Promise<T>;
  76. download(url: string): void;
  77. }
  78. const http: Http = {
  79. get(url, params) {
  80. return instance({
  81. url,
  82. method: 'get',
  83. params,
  84. })
  85. .then((res) => res.data)
  86. .catch((err) => err.data);
  87. },
  88. post(url, data) {
  89. return instance({
  90. url,
  91. method: 'post',
  92. data,
  93. })
  94. .then((res) => res.data)
  95. .catch((err) => err.data);
  96. },
  97. upload(url, data, params = {}) {
  98. return instance({
  99. url,
  100. method: 'post',
  101. params,
  102. headers: {
  103. // 'content-type': 'text/csv'
  104. 'content-type': 'multipart/form-data',
  105. },
  106. data,
  107. })
  108. .then((res) => res.data)
  109. .catch((err) => err.data);
  110. },
  111. download(url) {
  112. const iframe = document.createElement('iframe');
  113. iframe.style.display = 'none';
  114. iframe.src = url;
  115. // eslint-disable-next-line func-names
  116. iframe.onload = function () {
  117. document.body.removeChild(iframe);
  118. };
  119. document.body.appendChild(iframe);
  120. },
  121. };
  122. export default instance;
  123. export { http };

api封装

index 请求方法

  1. import { USER_ID } from '@/config/global';
  2. import { http } from '@/service/request';
  3. import { ILoginData, IUserInfo } from './types';
  4. const base = '/armory/barrack/v1';
  5. const api = {
  6. login: '/users/login',
  7. userInfo: '/users',
  8. };
  9. const baseParam = {
  10. domain: 'xxx',
  11. };
  12. // 登录
  13. export function login(params): Promise<ILoginData> {
  14. return http.post<ILoginData>(base + api.login, { ...baseParam, ...params });
  15. }
  16. // 获取用户信息
  17. export function getInfo(): Promise<IUserInfo[]> {
  18. const userId = localStorage.getItem(USER_ID);
  19. const url = `${base + api.userInfo}/${Number(userId)}`;
  20. return http.get<IUserInfo[]>(url, { ...baseParam });
  21. }

types 数据接口定义

  1. export interface ILoginData {
  2. token: string;
  3. userId: string;
  4. userName: string;
  5. }
  6. interface Role {
  7. id: string;
  8. name: string;
  9. code: string;
  10. }
  11. export interface IUserInfo {
  12. id: number;
  13. name: string;
  14. domain: string;
  15. createTime: string;
  16. updateTime: string;
  17. loginType: string;
  18. role: Role;
  19. state: string;
  20. }

权限管理

根目录下的permission.ts配合store/modules/user.ts
主要改写user中的login、getUserInfo方法,即可实现自己的权限逻辑,配合token和roles判断
改写roles获取不同角色的路由生成菜单
大致登录过程如下:
image.png

开发避坑

vue-router页面回退不显示

如果路由页面没有指定根标签,那么在路由返回时(go(-1),history.back()等),找不到可替换的标签,页面无法正常加载,vue3支持不设置根标签带来的影响
故在编写路由页面时必须指定根标签,组件页面可不受此约束