目录
一、前言
此文章在上次的基础上进行了部分调整,并根据用户体验(我自己)确认了页面整体布局和数据呈现,暂定就先这样,后续有需要或者有不协调的地方再调整。
此项目是在我上一个文章的后续开发, 需要的同学可以关注一下,文章链接如下:SpringBoot+Mybatis+MySQL+Vue+ElementUI前后端分离版:项目搭建(一)
(注:源码我会在文章结尾提供gitee连接,需要的同学可以去自行下载)
二、后端调整
1.实体类调整
1.完善UserEntity.java
java
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
@Data
public class UserEntity extends BaseEntity{
/**
* id 主键
*/
private Integer id;
/**
* name 姓名
*/
private String name;
/**
* age 年龄
*/
private Integer age;
/**
* birthday 生日
*/
@JsonFormat(pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
}
2.新增菜单实体类MenuEntity.java
java
import lombok.Data;
/**
* 菜单表
* @TableName menu
*/
@Data
public class MenuEntity extends BaseEntity {
/**
* 主键
*/
private Integer id;
/**
* 菜单名称
*/
private String menuName;
/**
* 父菜单ID
*/
private Integer parentId;
/**
* 路由路径
*/
private String path;
/**
* 组件路径
*/
private String component;
/**
* 权限标识
*/
private String perms;
/**
* 图标
*/
private String icon;
/**
* 排序
*/
private Integer sort;
/**
* 是否显示(0隐藏,1显示)
*/
private Integer visible;
}
这里在数据库新建menu表,并添加几条测试数据。
sql
CREATE TABLE `menu` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`menu_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单名称',
`parent_id` int DEFAULT '0' COMMENT '父菜单ID',
`path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '路由路径',
`component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '组件路径',
`perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '图标',
`sort` int DEFAULT '0' COMMENT '排序',
`visible` tinyint(1) DEFAULT '1' COMMENT '是否显示(0隐藏,1显示)',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`update_by` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
`del_flag` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '删除标识0未删除,1已删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='菜单表';
-- 插入数据
INSERT INTO `menu` (id, menu_name, parent_id, path, component, perms, icon, sort, visible, create_time, create_by, update_time, update_by, del_flag)
VALUES
(1, '权限管理', 0, '/permission', '', '', 'lock', 1, 1, NOW(), 'admin', NULL, NULL, 0),
(2, '用户管理', 1, '/user', 'src/view/user.vue', 'user:list', 'user', 1, 1, NOW(), 'admin', NULL, NULL, 0),
(3, '角色管理', 1, '/role', 'src/view/role.vue', 'role:list', 'role', 2, 1, NOW(), 'admin', NULL, NULL, 0),
(4, '菜单管理', 1, '/menu', 'src/view/menu.vue', 'menu:list', 'menu', 3, 1, NOW(), 'admin', NULL, NULL, 0);
3.对于实体类公共字段,我提取了一个BaseEntity.java,后续实体类都继承此实体类。
java
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
* 实体类公共字段
* @Author: wal
* @Date: 2025/6/26
*/
@Data
public class BaseEntity {
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 创建人
*/
private String createBy;
/**
* 修改时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**
* 修改人
*/
private String updateBy;
/**
* 删除标记0未删除1已删除(逻辑删除)
*/
private Integer delFlag;
}
2.菜单相关接口
1.MenuController.java
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.wal.userdemo.DTO.resp.TreeDataResp;
import org.wal.userdemo.service.MenuService;
import org.wal.userdemo.utils.Result;
import java.util.List;
@RestController
@RequestMapping("/api/menu")
public class MenuController {
@Autowired
private MenuService menuService;
@GetMapping("/getMenuList")
public Result<List<TreeDataResp>> getMenuList() {
return Result.success(menuService.getMenuList(""));
}
}
2.MenuService.java
java
import org.wal.userdemo.DTO.resp.TreeDataResp;
import java.util.List;
public interface MenuService {
List<TreeDataResp> getMenuList(String userId);
}
3.MenuServiceImpl.java
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.wal.userdemo.DTO.resp.TreeDataResp;
import org.wal.userdemo.entity.MenuEntity;
import org.wal.userdemo.mapper.MenuMapper;
import org.wal.userdemo.service.MenuService;
import org.wal.userdemo.utils.BeanUtils;
import java.util.*;
@Service
public class MenuServiceImpl implements MenuService {
@Autowired
private MenuMapper menuMapper;
/**
* 获取用户菜单列表
* @param userId
* @return
*/
@Override
public List<TreeDataResp> getMenuList(String userId) {
List<MenuEntity> menuList = menuMapper.getMenuList(userId);
List<TreeDataResp> treeDataRespList =BeanUtils.copyAsList(menuList, TreeDataResp.class);
return buildMenuTree(treeDataRespList);
}
/**
* 构建菜单树
* @param menus
* @return
*/
public List<TreeDataResp> buildMenuTree(List<TreeDataResp> menus) {
Map<Integer, TreeDataResp> menuMap = new HashMap<>();
menus.forEach(menu -> menuMap.put(menu.getId(), menu));
List<TreeDataResp> rootMenus = new ArrayList<>();
menus.forEach(menu -> {
Integer parentId = menu.getParentId();
if (parentId == null || parentId == 0) {
rootMenus.add(menu);
} else {
TreeDataResp parent = menuMap.get(parentId);
if (parent != null) {
if (parent.getChildren() == null) {
parent.setChildren(new ArrayList<>());
}
parent.getChildren().add(menu);
}
}
});
return rootMenus;
}
}
4.MenuMapper.java
java
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.wal.userdemo.entity.MenuEntity;
import java.util.List;
/**
* @author Administrator
* @description 针对表【menu(菜单表)】的数据库操作Mapper
* @createDate 2025-07-07 00:12:30
* @Entity org.wal.userdemo.entity.Menu
*/
@Mapper
public interface MenuMapper {
List<MenuEntity> getMenuList(@Param("userId") String userId);
}
5.MenuMapper.xml
XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.wal.userdemo.mapper.MenuMapper">
<resultMap id="BaseResultMap" type="org.wal.userdemo.entity.MenuEntity">
<id property="id" column="id" />
<result property="menuName" column="menu_name" />
<result property="parentId" column="parent_id" />
<result property="path" column="path" />
<result property="component" column="component" />
<result property="perms" column="perms" />
<result property="icon" column="icon" />
<result property="sort" column="sort" />
<result property="visible" column="visible" />
<result property="createTime" column="create_time" />
<result property="createBy" column="create_by" />
<result property="updateTime" column="update_time" />
<result property="updateBy" column="update_by" />
<result property="delFlag" column="del_flag" />
</resultMap>
<select id="getMenuList" parameterType="String" resultMap="BaseResultMap">
SELECT * FROM menu WHERE del_flag = 0 ORDER BY parent_id, sort;
</select>
</mapper>
3.用户相关接口
1.UserController.java
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.wal.userdemo.DTO.req.QueryUserReq;
import org.wal.userdemo.entity.UserEntity;
import org.wal.userdemo.service.UserService;
import org.wal.userdemo.utils.Result;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 获取所有用户信息
*
* @return List<UserEntity>
*/
@PostMapping("/getUserList")
public Result<UserEntity> getUserList(@RequestBody QueryUserReq queryUserReq) {
List<UserEntity> dataList = userService.getUserList(queryUserReq);
Integer total = userService.getUserCount(queryUserReq);
return Result.page(dataList, total);
}
}
2.定义通用分页Result.java(前文已体现,只是新增一个分页构造函数)
java
public static <T> Result<T> page(List<T> list, Integer total) {
Result<T> result = new Result<>();
result.setCode(200);
result.setData(list);
result.setTotal(total);
result.setMessage("success");
return result;
}
3.UserService.java(新增两个接口)
java
/**
* 查询所有用户
*
* @return
*/
List<UserEntity> getUserList(QueryUserReq queryUserReq);
/**
* 查询用户数量
*
* @return
*/
Integer getUserCount(QueryUserReq queryUserReq);
4.UserServiceImpl.java(新增两个实现方法)
java
/**
* 获取所有用户信息
*
* @return List<UserEntity>
*/
@Override
public List<UserEntity> getUserList(QueryUserReq queryUserReq) {
List<UserEntity> resp = userMapper.getUserList(queryUserReq);
return resp;
}
/**
* 获取用户数量
*
* @return Integer
*/
@Override
public Integer getUserCount(QueryUserReq queryUserReq) {
return userMapper.getUserCount(queryUserReq);
}
5.UserMapper.java(新增两个mapper接口)
java
/**
* 查询所有用户
*
* @return
*/
List<UserEntity> getUserList(QueryUserReq queryUserReq);
/**
* 查询用户数量
*
* @return
*/
Integer getUserCount(QueryUserReq queryUserReq);
6.UserMapper.xml(新增两个sql)
XML
<select id="getUserList" resultMap="BaseResultMap" parameterType="org.wal.userdemo.DTO.req.QueryUserReq">
select * from user
<where>
<if test="name != null and name != ''">
and name like concat('%',#{name},'%')
</if>
<if test="birthday != null">
and birthday = #{birthday}
</if>
and del_flag = 0
</where>
limit #{page},#{limit};
</select>
<select id="getUserCount" resultType="Integer" parameterType="org.wal.userdemo.DTO.req.QueryUserReq">
select count(*) from user
<where>
<if test="name != null and name != ''">
and name like concat('%',#{name},'%')
</if>
<if test="birthday != null">
and birthday = #{birthday}
</if>
and del_flag = 0
</where>;
</select>
4.新增工具类
1.新增工具类BeanUtils.java,具体体现在MenuServiceImpl.java类中copy菜单树,如下:
java
/**
* 获取用户菜单列表
* @param userId
* @return
*/
@Override
public List<TreeDataResp> getMenuList(String userId) {
List<MenuEntity> menuList = menuMapper.getMenuList(userId);
List<TreeDataResp> treeDataRespList =BeanUtils.copyAsList(menuList, TreeDataResp.class);
return buildMenuTree(treeDataRespList);
}
(为什么不直接用MenuEntity.java来构建树结构?,为了确保entity无属性、字段、方法侵入,解耦entity,声明resp类更容易理解和维护)。
此工具类是对org.springframework.beans.BeanUtils的封装。有需要的同学可以去一下链接查找:
gitee地址dev-utils分支,此分支是我用来实现和调试、测试工具类的分支。
5.新增菜单树返回类
java
import lombok.Data;
import java.util.List;
@Data
public class TreeDataResp {
/**
* 主键
*/
private Integer id;
/**
* 菜单名称
*/
private String menuName;
/**
* 父菜单ID
*/
private Integer parentId;
/**
* 路由路径
*/
private String path;
/**
* 组件路径
*/
private String component;
/**
* 权限标识
*/
private String perms;
/**
* 图标
*/
private String icon;
/**
* 排序
*/
private Integer sort;
/**
* 是否显示(0隐藏,1显示)
*/
private Integer visible;
/**
* 子菜单
*/
private List<TreeDataResp> children;
}
6.配置类、拦截器
1.新增JwtInterceptor.java拦截web请求,校验token信息。
java
import io.jsonwebtoken.JwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.wal.userdemo.utils.JwtUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
try {
String username = JwtUtil.parseUsername(token);
// 可以将 username 存入 request 或 SecurityContext
log.info("用户 {} 使用正确的token访问了后端接口", username);
return true;
} catch (JwtException e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "无效 Token");
return false;
}
} else {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "缺少 Token");
return false;
}
}
}
2.新增WebConfig.java类,针对特定路由接口挂载JwtInterceptor拦截器,忽略登录接口。
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.wal.userdemo.interceptor.JwtInterceptor;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
/**
* 添加拦截器
* 拦截路径为/api/**的请求,除了 /api/auth/login请求
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor).addPathPatterns("/api/**").excludePathPatterns("/api/auth/login");
}
}
至此,后端的调整暂时就这样。
三、前端调整
1.请求调整
1.重写login.vue的js部分,抽离请求体,在src创建api目录,在api下创建login.js,在js部分引入
javascript
import { login } from '@/api/login';
export default {
name: 'UserLogin',
data() {
return {
formData: {
username: '',
password: ''
},
rules: {
username: [
{ required: true, message: '用户名不能为空', trigger: 'blur' }
],
password: [
{ required: true, message: '密码不能为空', trigger: 'blur' }
],
}
};
},
methods: {
async login() {
try {
const res = await login(this.formData);
console.log('res.code', res)
if (res.data.code === 200) {
const token = res.data.data;
localStorage.setItem('token', token);
this.$router.push('/');
this.$message.success('登录成功');
} else {
this.$message.error(res.data.message || '登录失败');
}
} catch (error) {
this.$message.error('请求异常,请检查网络或服务端状态');
}
}
}
};
2.login.js如下:
javascript
// src/api/login.js
import request from '@/utils/request';
/**
* 用户登录
* @param {Object} data - 登录参数,如用户名和密码
* @returns {Promise}
*/
export function login(data) {
return request({
url: '/auth/login',
method: 'post',
data,
});
}
/**
* 用户退出(登出)
* @returns {Promise}
*/
export function logout() {
return request({
url: '/auth/logout',
method: 'post',
});
}
2.页面布局、样式调整
1.user.vue
1.user.vue布局调整
html
<template>
<div>
<!-- 查询条件 -->
<el-form :inline="true" label-position="right" label-width="80px" :model="queryForm"
class="demo-form-inline query-border-container">
<el-row :gutter="20" justify="center">
<!-- 姓名 -->
<el-col :span="7">
<el-form-item label="姓名">
<el-input v-model="queryForm.name" placeholder="请输入姓名"></el-input>
</el-form-item>
</el-col>
<!-- 出生日期 -->
<el-col :span="7">
<el-form-item label="出生日期">
<el-date-picker v-model="queryForm.birthday" type="date" placeholder="选择日期"
style="width: 100%;"></el-date-picker>
</el-form-item>
</el-col>
<!-- 按钮组 -->
<el-col :span="7">
<el-form-item>
<div style="display: flex; gap: 10px;">
<el-button type="primary" @click="onQuery">查询</el-button>
<el-button @click="onReset">重置</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 用户列表 -->
<el-table :data="tableData" style="width: 100%;" class="table-border-container" max-height="480"
v-loading="loading">
<el-table-column type="index" label="序号" width="100" align="center">
</el-table-column>
<el-table-column prop="name" label="姓名" width="180" align="center">
</el-table-column>
<el-table-column prop="age" label="年龄" width="180" align="center">
</el-table-column>
<el-table-column prop="birthday" label="出生日期" width="180" align="center">
</el-table-column>
<el-table-column prop="birthday" label="出生日期" width="180" align="center">
</el-table-column>
<el-table-column prop="birthday" label="出生日期" width="180" align="center">
</el-table-column>
<el-table-column label="操作" width="180" align="center">
<template #default="scope">
<el-button type="primary" size="small" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination background layout="total,sizes,prev, pager, next" :total="total" @size-change="handleSizeChange"
:page-size.sync="queryForm.limit" :page-sizes="[10, 20, 50, 100]" class="page-border-container">
</el-pagination>
</div>
</template>
<script>
import { getUserList } from '@/api/permission/user';
export default {
name: 'userView',
data() {
return {
tableData: [],
queryForm: {
page: 1,
limit: 10,
username: '',
birthday: '',
},
total: 0,
loading: false,
};
},
created() {
this.getUserList();
},
methods: {
getUserList() {
this.loading = true;
getUserList(this.queryForm).then(res => {
if (res.data.code == 200) {
this.tableData = res.data.data;
this.total = res.data.total;
// this.$message.success("获取用户列表成功!");
} else {
this.$message.error("获取用户列表失败!");
}
}).finally(() => {
this.loading = false;
});
},
// 查询
onQuery() {
this.getUserList();
},
// 重置表单并查询
onReset() {
this.queryForm = {
page: 1,
limit: 10,
name: '',
birthday: '',
};
this.getUserList();
},
handleSizeChange(val) {
this.queryForm.limit = val;
this.getUserList();
},
handleEdit(index, row) {
console.log(index, row);
this.$message.success('编辑成功');
},
handleDelete(index, row) {
console.log(index, row);
this.$message.success('删除成功');
},
},
};
</script>
<style scoped>
.query-border-container {
border: 1px dashed #dcdcdc;
border-radius: 8px;
padding: 8px 16px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 12px;
}
.table-border-container {
border: 1px dashed #dcdcdc;
border-radius: 8px;
padding: 8px 16px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 12px;
}
.page-border-container {
border: 1px dashed #dcdcdc;
border-radius: 8px;
padding: 8px 16px 8px 16px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 1px;
}
.el-table .el-table__cell {
padding: 5px 0px !important;
}
.el-form-item {
margin-bottom: 2px !important;
}
</style>
2.同样的,js请求抽出来到user.js下,目录在src/api/permission/下,
javascript
import request from '@/utils/request';
/**
* 查询用户列表(分页)
* @param {Object} params - 请求参数,如 page, limit 等
*/
export function getUserList(params) {
return request({
url: '/user/getUserList',
method: 'post',
data : params,
});
}
- user.vue作为后续页面的参考页面,所以我把CSS部分抽出来到src/assets/css/global.css如下:
css
.query-border-container {
border: 1px dashed #dcdcdc;
border-radius: 8px;
padding: 8px 16px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 12px;
}
.table-border-container {
border: 1px dashed #dcdcdc;
border-radius: 8px;
padding: 8px 16px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 12px;
}
.page-border-container {
border: 1px dashed #dcdcdc;
border-radius: 8px;
padding: 8px 16px 8px 16px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 1px;
}
.el-table .el-table__cell {
padding: 5px 0px !important;
}
.el-form-item {
margin-bottom: 2px !important;
}
CSS作为全局页面的样式需要在main.js中挂载,添加如下代码:
javascript
import '@/assets/css/global.css';
2.index.vue
1.index.vue页面调整,主要是抽取js请求,调整布局和响应式菜单,如下:
html
<template>
<el-container class="home-container">
<!-- 左侧区域 -->
<el-aside class="left-section" :width="'12%'">
<!-- 左上部分:logo + 标题 -->
<div class="top-left">
<div class="logo-container">
<img src="../assets/logo.png" alt="logo">
</div>
<h1>我的管理系统</h1>
</div>
<!-- 左下部分:菜单 -->
<el-menu default-active="1" class="sidebar-menu" :collapse="isCollapse" :collapse-transition="false"
@open="handleOpen" @close="handleClose" background-color="#304156" text-color="#fff"
active-text-color="#ffd04b">
<el-submenu v-for="menu in menuList" :key="menu.id" :index="menu.id + ''">
<template #title>
<i :class="'el-icon-' + menu.icon"></i>
<span>{{ menu.menuName }}</span>
</template>
<el-menu-item v-for="child in menu.children" :key="child.id" :index="child.path"
@click="handleMenuClick(child)">
{{ child.menuName }}
</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<!-- 右侧区域 -->
<el-container class="right-section">
<!-- 右上部分:顶部导航 -->
<el-header class="top-right-header">
<div class="header-right">
<span>欢迎,Admin</span>
<el-button type="text" @click="logout">退出</el-button>
</div>
</el-header>
<!-- 右下部分:主内容区域 -->
<el-main class="main-content">
<router-view />
<user />
</el-main>
</el-container>
</el-container>
</template>
<script>
import user from './user.vue'
import { logout } from '@/api/login'
import { getMenuList } from '@/api/permission/menu'
export default {
name: 'userIndex',
components: { user },
data() {
return {
isCollapse: false, // 默认展开
menuList: [],// 菜单列表
};
},
created() {
this.getMenuList();
},
methods: {
getMenuList() {
getMenuList().then(res => {
console.log('res.data', res.data)
if (res.data.code === 200) {
this.menuList = res.data.data || [];
this.$message.success("获菜单列表c成功!");
} else {
this.$message.error("获菜单列表失败!");
}
});
},
logout() {
logout().then(res => {
if (res.code === 200) {
localStorage.removeItem('token');
this.$router.push('/login');
this.$message.success('退出成功');
} else {
this.$message.error('退出失败');
}
}).catch(() => {
this.$message.error('请求异常');
});
},
handleMenuClick(menuItem) {
this.$router.push(menuItem.path); // 跳转到对应路径
},
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
},
},
};
</script>
<style scoped>
.home-container {
height: 100vh;
}
/* 左侧整体样式 */
.left-section {
display: flex;
flex-direction: column;
background-color: #304156;
color: white;
padding: 10px;
width: 50px;
}
/* 左上角 logo 和标题 */
.top-left {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.logo-container {
margin-right: 10px;
margin-top: 5px;
}
.logo-container img {
height: 20px;
width: auto;
object-fit: contain;
}
.top-left h1 {
font-size: 18px;
margin: 0;
color: white;
}
/* 菜单样式 */
.sidebar-menu {
flex: 1;
border-right: none;
}
/* 右侧整体样式 */
.right-section {
display: flex;
flex-direction: column;
}
/* 右上角导航栏 */
.top-right-header {
display: flex;
justify-content: flex-end;
align-items: center;
background-color: #ffffff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 0 20px;
}
.header-right {
display: flex;
align-items: center;
}
/* 主内容区域 */
.main-content {
padding: 20px;
}
</style>
2.index.vue抽取的js在src/api/permission/下,menu.js如下:
javascript
import request from '@/utils/request';
/**
* 查询菜单列表
* @param {Object} userId {可选} - 用户ID
}
*/
export function getMenuList() {
return request({
url: '/menu/getMenuList',
method: 'get',
});
}
3.请求拦截
1.在src下新建utils目录,在utils下新建request.js,所以请求js都要导入request.js,在request.js中声明请求配置、请求拦截器,如下:
javascript
import axios from 'axios';
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API || '/api', // 使用环境变量或默认值
timeout: 5000,
});
// 请求拦截器:添加 token 到 header
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;
}
return config;
},
error => {
return Promise.reject(error);
}
);
export default service;
至此,前端布局、请求调整到此结束。
四、开发过程中的问题
1.code review
在调试过程中,不断的重启后端项目,导致token失效,请求都是401未授权访问。
解决方案:在request.js中定义响应拦截器,把遇到error = 401重新跳转到登录页。
javascript
//响应拦截器(可选启用)
service.interceptors.response.use(
response => {
return response;
},
error => {
if(error.response.data.error == 'Unauthorized'){
console.error('token已失效请重新登录');
localStorage.removeItem('token');
window.location.href = '/login';
}
console.error('网络异常:', error);
return Promise.reject(error.message);
}
);
五、附:源码
1.源码下载地址
https://gitee.com/wangaolin/user-demo.git
同学们有需要可以自行下载查看,此文章是dev-vue分支。
六、结语
此次开发+调整只是为了后续开发有个参照,下一篇文章具体开发首页和权限管理,有需要的同学可以关注我。
(注:接定制化开发前后端分离项目,私我)