SpringBoot+Mybatis+MySQL+Vue+ElementUI前后端分离版:整体布局、架构调整(二)

目录

一、前言

二、后端调整

1.实体类调整

2.菜单相关接口

3.用户相关接口

4.新增工具类

5.新增菜单树返回类

6.配置类、拦截器

三、前端调整

1.请求调整

2.页面布局、样式调整

1.user.vue

2.index.vue

3.请求拦截

四、开发过程中的问题

五、附:源码

1.源码下载地址

六、结语

一、前言

此文章在上次的基础上进行了部分调整,并根据用户体验(我自己)确认了页面整体布局和数据呈现,暂定就先这样,后续有需要或者有不协调的地方再调整。

此项目是在我上一个文章的后续开发, 需要的同学可以关注一下,文章链接如下: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,
  });
}
  1. 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分支。

六、结语

此次开发+调整只是为了后续开发有个参照,下一篇文章具体开发首页和权限管理,有需要的同学可以关注我。

(注:接定制化开发前后端分离项目,私我)