官网菜单结构
在中医在线官网中,菜单的结构是有父级菜单和子级菜单。
根据api接口文档,可以看到后端是父级菜单和子级菜单分开查询的。
接口调用
这里是一个直接封装好的 http 请求
import axios, {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig,
} from "axios";
import { message } from "ant-design-vue";
import { useUserStore } from "../store/user";
const config = {
baseURL: "http://localhost:8089/medicine-online-client-backend",
timeout: 10000,
};
// 定义返回值类型
export interface Result<T = unknown> {
code: number;
msg: string;
data: T;
}
class Http {
// axios 实例
private instance: AxiosInstance;
// 构造函数里初始化
constructor(config: AxiosRequestConfig) {
this.instance = axios.create(config);
// 定义拦截器
this.interceptors();
}
// 拦截器
private interceptors() {
// axios 发送请求之前的处理
this.instance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const store = useUserStore();
const token = store.token;
if (token) {
config.headers!["Authorization"] = token;
}
return config;
},
(error) => {
error.data = {};
error.data.msg = "服务器异常,请联系管理员!";
return Promise.reject(error);
}
);
// axios 请求返回之后的处理
this.instance.interceptors.response.use(
(res: AxiosResponse) => {
if (res.data.code == 0) {
return res.data;
} else {
message.error(res.data.msg || "服务器出错!");
return Promise.reject(res.data.msg || "服务器出错");
}
},
(error) => {
error.data = {};
if (error && error.response) {
switch (error.response.status) {
case 400:
error.data.msg = "错误请求";
break;
case 401:
error.data.msg = "未授权,请重新登录";
break;
case 403:
error.data.msg = "拒绝访问";
break;
case 404:
error.data.msg = "请求错误, 未找到接口";
break;
case 405:
error.data.msg = "请求方法未允许";
break;
case 408:
error.data.msg = "请求超时";
break;
case 500:
error.data.msg = "服务器端出错";
break;
case 501:
error.data.msg = "网络未实现";
break;
case 502:
error.data.msg = "网络错误";
break;
case 503:
error.data.msg = "服务不可用";
break;
case 504:
error.data.msg = "网络超时";
break;
case 505:
error.data.msg = "HTTP版本不支持该请求";
break;
default:
error.data.msg = `连接错误 ${error.response.status}`;
}
message.error(error.data.msg);
} else {
error.data.msg = "连接到服务器失败";
message.error(error.data.msg);
}
return Promise.reject(error);
}
);
}
/* GET 方法 */
get<T = Result>(url: string, params?: object): Promise<T> {
return this.instance.get(url, { params });
}
/* POST 方法 */
post<T = Result>(url: string, data?: object): Promise<T> {
return this.instance.post(url, data);
}
/* PUT 方法 */
put<T = Result>(url: string, data?: object): Promise<T> {
return this.instance.put(url, data);
}
/* DELETE 方法 */
delete<T = Result>(url: string): Promise<T> {
return this.instance.delete(url);
}
// 图片上传
upload<T = Result>(url: string, params?: object): Promise<T> {
return this.instance.post(url, params, {
headers: {
"Content-Type": "multipart/form-data",
},
});
}
}
export default new Http(config);
因为已经封装好了 http ,可以直接在 api 下新建你的模块的文件夹,在文件夹中新建文件名为 index.ts ,并在该文件中调用接口。我这里页面的路径是 src/api/video/index.ts
import http from "../../http";
// 获取视频父级菜单
export const fetchCategoryData = () => {
return http.get("/index/item/category").then((response) => {
return response.data;
});
};
// 获取视频子级菜单
export const fetchCategoryListData = () => {
return http.get("/index/item/categorylist").then((response) => {
return response.data;
});
};
使用
引用接口
首先,要在你的页面中引用刚刚写的两个接口
import { fetchCategoryData,fetchCategoryListData } from "@/api/video/index.ts";
初始化空数组
我原本在页面中是用了父子结构的数组模拟的。每个菜单项都是一个键值对。
// 菜单数据
const menuData = reactive([
{
key: "sub1",
title: "中医方剂",
items: [
{ key: "1", label: "泻下剂" },
{ key: "2", label: "解和剂" },
{ key: "3", label: "补益剂" },
{ key: "4", label: "安神剂" },
{ key: "5", label: "理血剂" },
{ key: "6", label: "开窍剂" },
{ key: "7", label: "方剂实战" },
],
},
{
key: "sub2",
title: "中医基础",
items: [
{ key: "8", label: "绪论" },
{ key: "9", label: "阴阳五行" },
{ key: "10", label: "藏象" },
{ key: "11", label: "经络" },
{ key: "12", label: "病因病机" },
{ key: "13", label: "养生防病" },
{ key: "14", label: "治则治法" },
{ key: "15", label: "气血津液" },
],
},
// 其他省略
现在可以把这个数组换成动态数组
// 菜单数据
const menuData = reactive([]);
调用接口
调用接口的逻辑就是用接口中的数据替换我们的数组中的数据。
合并菜单
这里写了一个函数叫 fetchAndMergeMenuData ,用于合并菜单。
- 分别获取父级菜单和子级菜单的数据。
- 把父级菜单初始化成一个键值对,键是 pkId,值是一个对象,包含了 key 、 title 、 items 。其中 items 里面存放的是子菜单项。
- 遍历子级菜单数据 , 通过
parentId
找到对应的父菜单 , 如果找到父菜单,则将当前子菜单项(包含key
和label
)添加到父菜单的items
数组中。 (看上面的 API 接口可知,查询子菜单的接口有个 parentId 参数) - 生成最终菜单, 遍历
menuMap
的值,将每个父菜单对象添加到menuData
中 。
// 合并菜单数据
async function fetchAndMergeMenuData() {
try {
// 获取父级菜单数据
const categories = await fetchCategoryData();
// 获取子级菜单数据
const categoryList = await fetchCategoryListData();
// 将父级菜单数据进行初始化
const menuMap = new Map();
categories.forEach((category) => {
menuMap.set(category.pkId, {
key: `sub${category.pkId}`, // 唯一键值
title: category.title, // 父菜单标题
items: [],// 存放子菜单项
});
});
// 根据 parentId 将子菜单项添加到父级菜单
categoryList.forEach((item) => {
const parentMenu = menuMap.get(item.parentId);// 找到父菜单
if (parentMenu) {
parentMenu.items.push({
key: item.pkId.toString(), // 子菜单的唯一标识
label: item.title,// 子菜单标题
});
}
});
// 将合并后的菜单数据赋值给 menuData
menuData.length = 0;// 清空原数据
menuMap.forEach((menu) => {
menuData.push(menu);// 添加合并后的菜单
});
} catch (error) {
console.error(error);
}
}
到这一步,接口调用完成。
也就是说当你的父级菜单数据是
[
{ "pkId": 1, "title": "菜单一" },
{ "pkId": 2, "title": "菜单二" }
]
子级菜单数据是
[
{ "pkId": 101, "title": "子菜单1-1", "parentId": 1 },
{ "pkId": 102, "title": "子菜单1-2", "parentId": 1 },
{ "pkId": 201, "title": "子菜单2-1", "parentId": 2 }
]
的时候,合并出的数组数据为
[
{
"key": "sub1",
"title": "菜单一",
"items": [
{ "key": "101", "label": "子菜单1-1" },
{ "key": "102", "label": "子菜单1-2" }
]
},
{
"key": "sub2",
"title": "菜单二",
"items": [
{ "key": "201", "label": "子菜单2-1" }
]
}
]
直接使用
如果不需要改变接口的结构,可以直接调用。
这里有两个处理方法,假设调用接口获取的数据就是数组,直接使用 menuData.splice(0, menuData.length)
清空原数组 ,然后...response
将 response
数据逐项添加到 menuData
中 。
假设接口的返回体包含 code 和 msg 字段,获取的值就是response.data
。
// 从接口获取数据并赋值给 menuData
async function loadMenuData() {
try {
const response = await fetchAudioMenu();
// 如果返回的是数组,直接赋值给 menuData
if (Array.isArray(response)) {
menuData.splice(0, menuData.length, ...response);
} else {
// 如果是对象且包含 code 和 msg 字段
if (response.code === 0) {
menuData.splice(0, menuData.length, ...response.data);
} else {
console.error(response.msg);
}
}
} catch (error) {
console.error(error);
}
}