项目梳理
文件目录
项目根目录结构
├── 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方法加入以下逻辑
// 对于meta中hide的路由不生成菜单
if (item.meta.hide) {
return undefined;
}
路由配置
- meta.hide 隐藏菜单,需改写useRenderNav逻辑,如上
- meta.single 只显示一级菜单
- 动态路由目前只支持2级,所以更深的配置到同级加上冗余的路径名,配置如下:
{
path: '/project',
name: 'project',
meta: { title: '项目', icon: 'server' },
component: Layout,
children: [
{
path: 'manage',
name: 'projectManage',
component: () => import('@/pages/project/manage/index.vue'),
meta: { title: '项目管理' },
},
{
path: 'add',
name: 'projectAdd',
component: () => import('@/pages/project/add/index.vue'),
meta: { title: '新建项目', hide: true },
},
{
path: ':projectId',
name: 'projectShow',
component: () => import('@/pages/project/show/index.vue'),
meta: { title: '查看项目', hide: true },
},
{
path: ':projectId/floor/:floorId',
name: 'floorShow',
component: () => import('@/pages/floor/show/index.vue'),
meta: { title: '查看楼层', hide: true },
},
{
path: ':projectId/floor/add',
name: 'floorAdd',
component: () => import('@/pages/floor/add/index.vue'),
meta: { title: '增加楼层', hide: true },
},
],
},
网络请求封装
原项目在utils目录下,现service/request.ts
提供基础的get/post/upload方法,主要看HTTP接口中的泛型使用(83行),配合api的index中方法传入该泛型(定义在types中)
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { MessagePlugin } from 'tdesign-vue-next';
import qs from 'qs';
import proxy from '../config/proxy';
import { TOKEN_NAME } from '@/config/global';
import { showMessage } from './status';
const env = import.meta.env.MODE || 'development';
const host = env === 'mock' ? '/' : proxy[env].host; // 如果是mock模式 就不配置host 会走本地Mock拦截
const instance = axios.create({
baseURL: host,
timeout: 20000,
// withCredentials: true,
});
const reqMethods = ['get', 'head'];
instance.interceptors.request.use((config: AxiosRequestConfig) => {
const token = localStorage.getItem(TOKEN_NAME);
if (token) {
config.headers[TOKEN_NAME] = `Bearer ${token}`;
}
// 数组参数序列化
if (reqMethods.includes(config.method)) {
// eslint-disable-next-line func-names
config.paramsSerializer = function (params) {
return qs.stringify(params, { arrayFormat: 'repeat' });
};
}
return config;
});
instance.defaults.timeout = 5000;
instance.interceptors.response.use(
(response: AxiosResponse) => {
const token = response.headers.authorization;
if (token) {
localStorage.setItem(TOKEN_NAME, token);
}
if (response.status !== 200) {
showMessage(response.status);
}
return response;
},
(err) => {
// 错误提示
const { response } = err;
if (response) {
const {
data: { message },
} = response;
if (message) {
MessagePlugin.error(message);
} else {
showMessage(response.status);
}
} else {
MessagePlugin.error('网络连接异常,请稍后再试');
}
const { config } = err;
if (!config || !config.retry) return Promise.reject(err);
config.retryCount = config.retryCount || 0;
if (config.retryCount >= config.retry) {
return Promise.reject(err);
}
config.retryCount += 1;
const backoff = new Promise((resolve) => {
setTimeout(() => {
resolve(null);
}, config.retryDelay || 1);
});
return backoff.then(() => instance(config));
},
);
// 封装常用请求,这里是重点泛型T由封装api的方法传入
interface Http {
get<T>(url: string, params?: unknown): Promise<T>;
post<T>(url: string, params?: unknown): Promise<T>;
upload<T>(url: string, params: unknown): Promise<T>;
download(url: string): void;
}
const http: Http = {
get(url, params) {
return instance({
url,
method: 'get',
params,
})
.then((res) => res.data)
.catch((err) => err.data);
},
post(url, data) {
return instance({
url,
method: 'post',
data,
})
.then((res) => res.data)
.catch((err) => err.data);
},
upload(url, data, params = {}) {
return instance({
url,
method: 'post',
params,
headers: {
// 'content-type': 'text/csv'
'content-type': 'multipart/form-data',
},
data,
})
.then((res) => res.data)
.catch((err) => err.data);
},
download(url) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
// eslint-disable-next-line func-names
iframe.onload = function () {
document.body.removeChild(iframe);
};
document.body.appendChild(iframe);
},
};
export default instance;
export { http };
api封装
index 请求方法
import { USER_ID } from '@/config/global';
import { http } from '@/service/request';
import { ILoginData, IUserInfo } from './types';
const base = '/armory/barrack/v1';
const api = {
login: '/users/login',
userInfo: '/users',
};
const baseParam = {
domain: 'xxx',
};
// 登录
export function login(params): Promise<ILoginData> {
return http.post<ILoginData>(base + api.login, { ...baseParam, ...params });
}
// 获取用户信息
export function getInfo(): Promise<IUserInfo[]> {
const userId = localStorage.getItem(USER_ID);
const url = `${base + api.userInfo}/${Number(userId)}`;
return http.get<IUserInfo[]>(url, { ...baseParam });
}
types 数据接口定义
export interface ILoginData {
token: string;
userId: string;
userName: string;
}
interface Role {
id: string;
name: string;
code: string;
}
export interface IUserInfo {
id: number;
name: string;
domain: string;
createTime: string;
updateTime: string;
loginType: string;
role: Role;
state: string;
}
权限管理
根目录下的permission.ts配合store/modules/user.ts
主要改写user中的login、getUserInfo方法,即可实现自己的权限逻辑,配合token和roles判断
改写roles获取不同角色的路由生成菜单
大致登录过程如下:
开发避坑
vue-router页面回退不显示
如果路由页面没有指定根标签,那么在路由返回时(go(-1),history.back()等),找不到可替换的标签,页面无法正常加载,vue3支持不设置根标签带来的影响
故在编写路由页面时必须指定根标签,组件页面可不受此约束