【SpringBoot+Vue】x-admin管理系统跟做

news2024/12/23 18:10:24

技术栈

前端技术说明
Vue前端框架
Vuex全局状态管理框架
ElementUI前端UI框架
Axios前端HTTP框架
vue-element-admin项目脚手架
后端技术说明
SpringBoot容器+MVC框架
MyBatisORM框架
MyBatis-plusMyBatis增强工具
Redis非关系型数据库

数据库准备

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu`  (
  `menu_id` int(11) NOT NULL AUTO_INCREMENT,
  `component` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `path` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `redirect` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `parent_id` int(11) NULL DEFAULT NULL,
  `is_leaf` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `hidden` tinyint(1) NULL DEFAULT NULL,
  PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES (1, 'Layout', '/user', '/user/list', 'userManage', '用户管理', 'userManage', 0, 'N', 0);
INSERT INTO `sys_menu` VALUES (2, 'user/user', 'list', NULL, 'userList', '用户列表', 'userList', 1, 'Y', 0);
INSERT INTO `sys_menu` VALUES (3, 'user/role', 'role', NULL, 'roleList', '角色列表', 'role', 1, 'Y', 0);
INSERT INTO `sys_menu` VALUES (4, 'user/permission', 'permission', NULL, 'permissionList', '权限列表', 'permission', 1, 'Y', 0);

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `role_id` int(11) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `role_desc` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'admin', '超级管理员');
INSERT INTO `sys_role` VALUES (2, 'hr', '人事专员');
INSERT INTO `sys_role` VALUES (3, 'normal', '普通员工');

-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_id` int(11) NULL DEFAULT NULL,
  `menu_id` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `phone` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `status` int(1) NULL DEFAULT NULL,
  `avater` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `deleted` int(1) NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'admin1', '$2a$10$QSDIu2LNrHsj.YqC2rrwEOtTCcoZWUphwRbwe0VIKVkJXaMD2Qo8y', '123456', '12345456', 1, '', 0);

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NULL DEFAULT NULL,
  `role_id` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1, 1);

SET FOREIGN_KEY_CHECKS = 1;

后端框架装备

1.项目框架搭建

创建springboot 2.6项目

在这里插入图片描述

编辑pom.xml文件导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.cyfy</groupId>
    <artifactId>x-admin-serve</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>x-admin-serve</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.7.6</spring-boot.version>
    </properties>
    <dependencies>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <!--   mybatis-plus     -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!-- freemarker -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
         <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.7</version>
        </dependency>
        <!--    密码加密工具    -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core</artifactId>
            <version>3.1.0.RELEASE</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>com.cyfy.XAdminServeApplication</mainClass>
                    <skip>true</skip>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

编辑src/main/resources目录下的application.yml配置文件(没有就创建)

server:
  port:8080

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql:///my_demo
  redis:
    port: 6379
    host: localhost

logging:
  level:
    com.cyfy: debug
    
#  设置逻辑删除字段
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

在这里插入图片描述

运行项目。查看是否可正常启动

在这里插入图片描述

2.代码生成器

在src/test/java目录下,创建CodeGenerator.java文件,编写引用mybatis-plus代码生成器生成代码

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.Collections;

public class CodeGenerator {
    public static void main(String[] args) {
        String url = "jdbc:mysql:///my_demo";   // 数据库链接地址
        String username = "root";               // 数据库用户名
        String password = "123456";             // 数据库密码
        String moduleName = "sys";              // 父包模块
        String project_abs_path = "E:\\项目\\神盾管理系统\\x-admin\\x-admin-serve\\src\\main\\";  // 项目绝对路径
        String mapperLocation = project_abs_path + "resources\\mapper\\" + moduleName;   // xxxMapper.xml文件输出地址
        String tables = "sys_user,sys_role,sys_menu,sys_user_role,sys_role_menu";     // 需要生成代码的表
        FastAutoGenerator.create(url, username, password)
                .globalConfig(builder -> {
                    builder.author("cyfy") // 设置作者
                         // .enableSwagger() // 开启 swagger 模式
                            .outputDir(project_abs_path + "java"); // 指定输出目录
                })
                .packageConfig(builder ->
                        builder.parent("com.cyfy") // 设置父包名
                                .moduleName(moduleName) // 设置父包模块名
                                .pathInfo(Collections.singletonMap(OutputFile.xml, mapperLocation)) // 设置mapperXml生成路径
                )
                .strategyConfig(builder ->
                        builder.addInclude(tables) // 设置需要生成的表名
                                .addTablePrefix("sys_") // 设置过滤表前缀
                )
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();
    }
}

执行生成器代码后,项目新增对应代码文件

在这里插入图片描述

测试

编辑UserController.java文件,编写获取用户表数据接口

import com.cyfy.sys.entity.User;
import com.cyfy.sys.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private IUserService userService;

    @GetMapping("/all")
    public List<User> getAllUser(){
        List<User> list = userService.list();
        return list;
    }
}

修改项目启动器文件,增加@MapperScan注解

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.cyfy.*.mapper")
public class XAdminServeApplication {
    public static void main(String[] args) {
        SpringApplication.run(XAdminServeApplication.class, args);
    }
}

运行项目,访问接口是否正常

在这里插入图片描述

后端开发

1.公共响应类

在com/xxx目录下新建common/vo目录,并新建Result.java文件处理返回给前端的数据

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private Integer code;
    private String message;
    private T data;

    public static<T> Result<T> success(){
        return new Result<>(20000,"success",null);
    }

    public static<T> Result<T> success(T data){
        return new Result<>(20000,"success",data);
    }

    public static<T> Result<T> success(T data, String Message){
        return new Result<>(20000,Message,data);
    }

    public static<T> Result<T> success( String Message){
        return new Result<>(20000,Message,null);
    }

    public static<T> Result<T> fail(){
        return new Result<>(20001,"fail",null);
    }

    public static<T> Result<T> fail(Integer code){
        return new Result<>(code,"success",null);
    }

    public static<T> Result<T> fail(Integer code, String Message){
        return new Result<>(code,Message,null);
    }

    public static<T> Result<T> fail( String Message){
        return new Result<>(20001,Message,null);
    }
}

测试:修改getAllUser()方法

    @GetMapping("/all")
    public Result<List<User>> getAllUser(){
        List<User> list = userService.list();
        return Result.success(list,"查询成功");
    }

运行效果

在这里插入图片描述

2.Redis配置

在common目录下新建tools目录,并新建MyRedisConfig.java文件

package com.cyfy.common.tools;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.TimeZone;

@Configurable
public class MyRedisConfig {

    @Resource
    private RedisConnectionFactory factory;

    @Bean
    public RedisTemplate redisTemplate(){
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);

        // 设置键的序列化方法
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        // 设置字符串的序列化方法
        redisTemplate.setValueSerializer(serializer);

        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        om.setTimeZone(TimeZone.getDefault());
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
        om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);
        om.configure(MapperFeature.USE_ANNOTATIONS,false);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        serializer.setObjectMapper(om);
        return redisTemplate;
    }
}

3.跨域处理

在common/tools目录下,新建MyCorsConfig.java文件,用于处理跨域请求问题

package com.cyfy.common.tools;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * 跨域请求处理
 */
@Configuration
public class MyCorsConfig {
    @Bean
    public CorsFilter corsFilter(){
        // 1.添加CORS配置信息
        CorsConfiguration config = new CorsConfiguration();
        // 1.1. 允许的域,不要写*,否则cookie就无法使用
        config.addAllowedOrigin("http://localhost:8888"); // 这里填写请求的前端服务器
        // 1.2. 是否发送cookie信息
        config.setAllowCredentials(true);
        // 1.3. 允许的请求方式,常用方式有GET、POST
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("HEAD");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");
        // 1.4. 允许的头信息
        config.addAllowedHeader("*");

        // 2.添加映射路径,这里拦截一切请求
        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration("/**",config);

        // 3.返回新的CorsFilter
        return new CorsFilter(configSource);
    }
}

也可在控制器类上加@CrossOrigin注解处理,减少配置类

4.mybatis-plus拦截器

在common/tools目录下,新建MpConfig.java文件

package com.cyfy.common.tools;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MpConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

5.用户相关接口

编写UserController.java文件

package com.cyfy.sys.controller;

import com.cyfy.common.vo.Result;
import com.cyfy.sys.entity.User;
import com.cyfy.sys.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

/**
 *  前端控制器
 */
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private IUserService userService;

    /**
     * 用户登录接口
     */
    @PostMapping("/login")
    public Result<Map<String,Object>> login(@RequestBody User user){
        Map<String, Object> data = userService.login(user);

        if(data != null){
            return Result.success(data);
        }
        return Result.fail(20002,"用户名或密码错误");
    }

    /**
     * 用户信息查询接口
     */
    @GetMapping("/info")
    public Result<Map<String,Object>> getUserInfo(@RequestParam("token") String token){
        Map<String,Object> data = userService.getUserInfo(token);

        if(data != null){
            return Result.success(data);
        }
        return Result.fail(20003,"登录信息已失效,请重新登录");
    }

    /**
     * 用户注销接口
     */
    @PostMapping("/logout")
    public Result<?> logout(@RequestHeader("X-Token") String token){
        userService.logout(token);
        return Result.success();
    }

    /**
     * 获取用户列表接口
     */
    @GetMapping("/list")
    public Result<Map<String,Object>> getUserList(@RequestParam(value = "username",required = false) String username,
                                                  @RequestParam(value = "phone",required = false) String phone,
                                                  @RequestParam(value = "pageNo") Long pageNo,
                                                  @RequestParam(value = "pageSize") Long pageSize){
        Map<String, Object> data = userService.getUserList(username,phone,pageNo,pageSize);
        return Result.success(data);
    }

    @PostMapping("/add")
    public Result<?> addUser(@RequestBody User user){
        if(userService.addUser(user)) {
            return Result.success("新增用户成功");
        }
        return Result.fail("新增用户失败");
    }

    @PutMapping("/upd")
    public Result<?> updateUser(@RequestBody User user){
        if(userService.updateUser(user)) {
            return Result.success("修改用户成功");
        }
        return Result.fail("修改用户失败");
    }

    @GetMapping("/{id}")
    public Result<User> getUserById(@PathVariable("id")Integer id){
        User data = userService.getUserById(id);
        return Result.success(data);
    }

    @DeleteMapping("/{id}")
    public Result<User> deleteUserById(@PathVariable("id")Integer id){
        if(userService.deleteUserById(id)){
            return Result.success("删除用户成功");
        }
        return Result.fail("删除用户失败");
    }
}

编写IUserService.java文件

package com.cyfy.sys.service;

import com.cyfy.sys.entity.User;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.Map;

/**
 *  服务类
 */
public interface IUserService extends IService<User> {
    // 用户登录
    Map<String,Object> login(User user);
    // 根据token获取用户信息
    Map<String, Object> getUserInfo(String token);
    // 注销登录
    void logout(String token);
    // 获取用户列表
    Map<String, Object> getUserList(String username,String phone,Long pageNo,Long pageSize);
    // 添加用户
    boolean addUser(User user);
    // 修改用户
    boolean updateUser(User user);
    // 根据id查询用户
    User getUserById(Integer id);
    // 根据id删除指定用户
    boolean deleteUserById(Integer id);
}

编写UserServiceImpl.java文件

package com.cyfy.sys.service.impl;

import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cyfy.sys.entity.User;
import com.cyfy.sys.mapper.UserMapper;
import com.cyfy.sys.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 *  服务实现类
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private RedisTemplate redisTemplate;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Map<String, Object> login(User user) {
        // 根据用户名查询
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUsername,user.getUsername());
        // 根据username查询是否存在对应用户
        User loginUser = this.baseMapper.selectOne(wrapper);
        System.out.println(user.getPassword());
        System.out.println(loginUser.getPassword());

        // 结果不为空时,且加密密码正确:生成token,并将用户信息存入redis
        if(loginUser != null && passwordEncoder.matches(user.getPassword(),loginUser.getPassword())){
            // 这里暂时使用UUID,最终需要使用jwt
            String key = "user" + UUID.randomUUID();

            loginUser.setPassword(null);    // 将密码设置为空,不存入redis
            // 存入redis
            redisTemplate.opsForValue().set(key,loginUser,30, TimeUnit.MINUTES);

            // 返回处理好的token
            Map<String,Object> data = new HashMap<>();
            data.put("token",key);
            return data;
        }
        return null;
    }

    @Override
    public Map<String, Object> getUserInfo(String token) {
        // 根据Token从Redis中获取用户信息
        Object obj = redisTemplate.opsForValue().get(token);

        if(obj != null){
            // 将字符串转为User对象
            User loginUser = JSON.parseObject(JSON.toJSONString(obj),User.class);
            Map<String,Object> data = new HashMap<>();
            data.put("name",loginUser.getUsername());
            data.put("avatar",loginUser.getAvater());
            // 通过ID查询用户角色
            List<String> roleList = this.baseMapper.getRoleNameByUserId(loginUser.getId());
            data.put("roles",roleList);
            return data;
        }
        return null;
    }

    @Override
    public void logout(String token) {
        // 从redis中清除指定token
        redisTemplate.delete(token);
    }

    @Override
    public Map<String, Object> getUserList(String username,String phone,Long pageNo,Long pageSize) {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(StringUtils.hasLength(username),User::getUsername,username);
        wrapper.eq(StringUtils.hasLength(phone),User::getPhone,phone);

        // 分页处理
        Page<User> page = new Page<>(pageNo,pageSize);
        this.page(page,wrapper);
        Map<String,Object> data = new HashMap<>();
        data.put("total",page.getTotal());
        data.put("row",page.getRecords());
        return data;
    }

    @Override
    public boolean addUser(User user) {
        // 根据用户名查询
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUsername,user.getUsername());
        // 根据username查询是否存在对应用户
        User loginUser = this.baseMapper.selectOne(wrapper);
        // 如果用户名存在,返回false
        if(loginUser != null){
            return false;
        }
        //  加密密码
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        return this.save(user);
    }

    @Override
    public boolean updateUser(User user) {
        //  加密密码
        user.setPassword(null);
        return this.updateById(user);
    }

    @Override
    public User getUserById(Integer id) {
        User user = this.getById(id);
        user.setPassword(null);
        return user;
    }

    @Override
    public boolean deleteUserById(Integer id) {
        // 先查询用户是否存在
        if(this.getById(id) != null){
            return this.removeById(id);
        }
        return false;
    }
}

编写UserMapper.java文件

package com.cyfy.sys.mapper;

import com.cyfy.sys.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import java.util.List;

/**
 *  Mapper 接口
 */
public interface UserMapper extends BaseMapper<User> {
    List<String> getRoleNameByUserId(Integer id);
}

编写UserMapper.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="com.cyfy.sys.mapper.UserMapper">
    <select id="getRoleNameByUserId" parameterType="Integer" resultType="String">
        SELECT
            b.`role_name`
        FROM  sys_user_role a, sys_role b
        WHERE
            a.role_id = b.`role_id` AND a.user_id = #{userId}
    </select>
</mapper>

修改XAdminServeApplication.java文件,加个加密方法

package com.cyfy;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@SpringBootApplication
@MapperScan("com.cyfy.*.mapper")
public class XAdminServeApplication {

    public static void main(String[] args) {
        SpringApplication.run(XAdminServeApplication.class, args);
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

使用Postman测试接口(记得开启redis

在这里插入图片描述

前端框架准备

1.脚手架搭建

详见【Vue】vue-admin-template项目搭建,需保持node版本一致,可以使用nvm进行版本管理

2.使用VS Code打开项目

在这里插入图片描述

安装依赖:npm install --no-fund

在这里插入图片描述

运行项目:npm run dev

在这里插入图片描述

在这里插入图片描述

3.修改后端请求接口

修改.env.development文件

# just a flag
ENV = 'development'

# base api
VUE_APP_BASE_API = 'http://localhost:8080'

修改vue.config.js文件,注释使用模拟数据

// before: require('./mock/mock-server.js')  // 模拟数据

测试:修改src/api/user.js文件,更正请求url

import request from '@/utils/request'

export function login(data) {
  return request({
    url: '/user/login',
    method: 'post',
    data
  })
}

export function getInfo(token) {
  return request({
    url: '/user/info',
    method: 'get',
    params: { token }
  })
}

export function logout() {
  return request({
    url: '/user/logout',
    method: 'post'
  })
}

运行效果:可通过数据库的账号登录

在这里插入图片描述

前端开发

1.登录页修改

修改src/views/login目录下的index.vue文件(没啥修改内容,就是将英文内容改为中文,稍微调整一下布局)

<template>
  <div class="login-container">
    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">

      <div class="title-container">
        <h3 class="title">欢迎登录神盾局管理系统</h3>
      </div>

      <el-form-item prop="username">
        <span class="svg-container">
          <svg-icon icon-class="user" />
        </span>
        <el-input
          ref="username"
          v-model="loginForm.username"
          placeholder="用户名"
          name="username"
          type="text"
          tabindex="1"
          auto-complete="on"
        />
      </el-form-item>

      <el-form-item prop="password">
        <span class="svg-container">
          <svg-icon icon-class="password" />
        </span>
        <el-input
          :key="passwordType"
          ref="password"
          v-model="loginForm.password"
          :type="passwordType"
          placeholder="密码"
          name="password"
          tabindex="2"
          auto-complete="on"
          @keyup.enter.native="handleLogin"
        />
        <span class="show-pwd" @click="showPwd">
          <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
        </span>
      </el-form-item>

      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button>
    </el-form>
  </div>
</template>

<script>
import { validUsername } from '@/utils/validate'

export default {
  name: 'Login',
  data() {
    const validateUsername = (rule, value, callback) => {
      if (!validUsername(value)) {
        callback(new Error('请输入正确的用户名'))
      } else {
        callback()
      }
    }
    const validatePassword = (rule, value, callback) => {
      if (value.length < 6) {
        callback(new Error('密码不能少于6位'))
      } else {
        callback()
      }
    }
    return {
      loginForm: {
        username: '',
        password: ''
      },
      loginRules: {
        username: [{ required: true, trigger: 'blur', validator: validateUsername }],
        password: [{ required: true, trigger: 'blur', validator: validatePassword }]
      },
      loading: false,
      passwordType: 'password',
      redirect: undefined
    }
  },
  watch: {
    $route: {
      handler: function(route) {
        this.redirect = route.query && route.query.redirect
      },
      immediate: true
    }
  },
  methods: {
    showPwd() {
      if (this.passwordType === 'password') {
        this.passwordType = ''
      } else {
        this.passwordType = 'password'
      }
      this.$nextTick(() => {
        this.$refs.password.focus()
      })
    },
    handleLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.loading = true
          this.$store.dispatch('user/login', this.loginForm).then(() => {
            this.$router.push({ path: this.redirect || '/' })
            this.loading = false
          }).catch(() => {
            this.loading = false
          })
        } else {
          console.log('error submit!!')
          return false
        }
      })
    }
  }
}
</script>

<style lang="scss">
/* 修复input 背景不协调 和光标变色 */
/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */

$bg:#283443;
$light_gray:#fff;
$cursor: #fff;

@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
  .login-container .el-input input {
    color: $cursor;
  }
}

/* reset element-ui css */
.login-container {
  .el-input {
    display: inline-block;
    height: 47px;
    width: 85%;

    input {
      background: transparent;
      border: 0px;
      -webkit-appearance: none;
      border-radius: 0px;
      padding: 12px 5px 12px 15px;
      color: $light_gray;
      height: 47px;
      caret-color: $cursor;

      &:-webkit-autofill {
        box-shadow: 0 0 0px 1000px $bg inset !important;
        -webkit-text-fill-color: $cursor !important;
      }
    }
  }

  .el-form-item {
    border: 1px solid rgba(255, 255, 255, 0.1);
    background: rgba(0, 0, 0, 0.1);
    border-radius: 5px;
    color: #454545;
  }
}
</style>

<style lang="scss" scoped>
$bg:#2d3a4b;
$dark_gray:#889aa4;
$light_gray:#eee;

.login-container {
  min-height: 100%;
  width: 100%;
  background-color: $bg;
  overflow: hidden;

  display: flex;                    // 布局类型:弹性布局
  align-items: center;              // 调整元素在侧轴的对齐方式:居中
  .login-form {
    position: relative;
    width: 520px;
    max-width: 100%;
    padding: 35px 50px 20px;        // 内边距:上 中 下
    margin: 0 auto;                 // 外边距
    overflow: hidden;
    background-color: #283443;  // 设置背景色
    border-radius: 8px;             // 设置边框圆角
    opacity: 0.9;                   // 设置透明度
  }

  .tips {
    font-size: 14px;
    color: #fff;
    margin-bottom: 10px;

    span {
      &:first-of-type {
        margin-right: 16px;
      }
    }
  }

  .svg-container {
    padding: 6px 5px 6px 15px;
    color: $dark_gray;
    vertical-align: middle;
    width: 30px;
    display: inline-block;
  }

  .title-container {
    position: relative;

    .title {
      font-size: 26px;
      color: $light_gray;
      margin: 0px auto 40px auto;
      text-align: center;
      font-weight: bold;
    }
  }

  .show-pwd {
    position: absolute;
    right: 10px;
    top: 7px;
    font-size: 16px;
    color: $dark_gray;
    cursor: pointer;
    user-select: none;
  }
}
</style>

运行效果

在这里插入图片描述

2.菜单初始化

在src/views目录下创建sys模块目录、test模块目录(充数,暂不实现)

在sys模块目录下创建user/index.vuerole/index.vue两个组件文件,test模块目录下创建test1.vuetest2.vuetest3.vue

在这里插入图片描述

修改src/router/index.js文件,修改路由配置

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'

export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },

  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },

  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [{
      path: 'dashboard',
      name: 'Dashboard',
      component: () => import('@/views/dashboard/index'),
      meta: { title: '首页', icon: 'dashboard' }
    }]
  },

  {
    path: '/sys',
    component: Layout,
    redirect: '/sys/user',
    name: 'sysMange',
    meta: { title: '系统管理', icon: 'el-icon-s-help' },
    children: [
      {
        path: 'user',
        name: 'User',
        component: () => import('@/views/sys/user/index'),
        meta: { title: '用户管理', icon: 'table' }
      },
      {
        path: 'role',
        name: 'Role',
        component: () => import('@/views/sys/role/index'),
        meta: { title: '角色管理', icon: 'tree' }
      }
    ]
  },

  {
    path: '/test',
    component: Layout,
    redirect: '/test/test1',
    name: 'test',
    meta: { title: '测试模块', icon: 'form' },
    children: [
      {
        path: 'test1',
        name: 'Test1',
        component: () => import('@/views/test/test1'),
        meta: { title: '功能点一', icon: 'form' }
      },
      {
        path: 'test2',
        name: 'Test2',
        component: () => import('@/views/test/test2'),
        meta: { title: '功能点二', icon: 'form' }
      },
      {
        path: 'test3',
        name: 'Test3',
        component: () => import('@/views/test/test3'),
        meta: { title: '功能点三', icon: 'form' }
      },
    ]
  },

  // 404 page must be placed at the end !!!
  { path: '*', redirect: '/404', hidden: true }
]

const createRouter = () => new Router({
  // mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})

const router = createRouter()

// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}

export default router

运行效果

在这里插入图片描述

3.标签栏导航

3.1.复制对应文件到对应目录

@/layout/components/TagsView

@/store/modules/tagsView.js

@/store/modules/permission.js

3.2.修改@/layoutcomponents/AppMain.vue文件

<template>
  <section class="app-main">
    <transition name="fade-transform" mode="out-in">
      <!-- 加入标签栏组件  -->
      <keep-alive :include="cachedViews">
        <router-view :key="key" />
      </keep-alive>
    </transition>
  </section>
</template>

<script>
export default {
  name: 'AppMain',
  computed: {
    key() {
      return this.$route.path
    },
    cachedViews(){
      return this.$store.state.tagsView.cachedViews
    }
  }
}
</script>

3.3.修改@store/getters.js文件

const getters = {
  sidebar: state => state.app.sidebar,
  device: state => state.app.device,
  token: state => state.user.token,
  avatar: state => state.user.avatar,
  name: state => state.user.name,

  visitedViews: state => state.tagsView.visitedViews,
  cachedViews: state => state.tagsView.cachedViews,
  permission_routes: state => state.permission.routes,
}

export default getters

3.4.修改@layout/components/index.js文件

export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar'
export { default as AppMain } from './AppMain'
export { default as TagsView } from './TagsView/index.vue'

3.5.修改@store/index.js文件

import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import app from './modules/app'
import settings from './modules/settings'
import user from './modules/user'
import tagsView from './modules/tagsView'
import permission from './modules/permission'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    app,
    settings,
    user,
    tagsView,
    permission
  },
  getters
})

export default store

3.6.Affix固钉

修改@/router/index.js文件,在需要固定的标签页中添加affix: true属性

  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [{
      path: 'dashboard',
      name: 'Dashboard',
      component: () => import('@/views/dashboard/index'),
      meta: { title: '首页', icon: 'dashboard' ,affix: true }
    }]
  },

运行效果

在这里插入图片描述

4.用户管理页实现

编写views/sys/user/index.vue文件

<template>
  <div>
    <!-- 搜索栏 -->
    <el-card class="search">
      <el-row>
        <el-col :span="20">
          <el-input v-model="searchModel.username" placeholder="用户名" clearable />
          <el-input v-model="searchModel.phone" placeholder="电话" clearable />
          <el-button @click="getUserList" type="primary" round icon="el-icon-search">查询</el-button>
        </el-col>
        <el-col :span="4" align="right">
          <el-button @click="openUserVisible(null)" type="primary" icon="el-icon-plus" circle></el-button>
        </el-col>
      </el-row>
    </el-card>
    <!-- 结果列表 -->
    <el-card>
      <el-table :data="userList" stripe style="width:100%">
        <el-table-column label="#" width="80">
          <template slot-scope="scope">
            {{ (searchModel.pageNo - 1) * searchModel.pageSize + scope.$index + 1 }}
          </template>
        </el-table-column>
        <el-table-column prop="id" label="用户ID" width="80" />
        <el-table-column prop="username" label="用户名" width="180" />
        <el-table-column prop="phone" label="电话" width="180" />
        <el-table-column prop="status" label="状态" width="180" >
          <template slot-scope="scope">
            <el-tag v-if="scope.row.status === 1">正常</el-tag>
            <el-tag v-else type="danger">禁用</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="email" label="电子邮件" />
        <el-table-column label="操作" width="180">
          <template slot-scope="scope">
            <el-button @click="openUserVisible(scope.row.id)" type="primary" icon="el-icon-edit" size="mini" circle />
            <el-button @click="deleteUser(scope.row)" type="danger" icon="el-icon-delete" size="mini" circle />
          </template>
        </el-table-column>
      </el-table>
    </el-card>

    <!-- 分页组件 -->
    <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
      :current-page="searchModel.pageNo" :page-sizes="[5, 10, 20, 50]" :page-size="searchModel.pageSize"
      layout="total, sizes, prev, pager, next, jumper" :total="total">
    </el-pagination>

    <!-- 用户信息编辑对话框 -->
    <el-dialog @close="clearUserForm" :title="title" :visible.sync="userFormVisible">
      <el-form :model="userForm" ref="userForm" :rules="rules">
        <el-form-item label="用户名" prop="username" :label-width="formLabelWidth">
          <el-input v-model="userForm.username" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item v-if="userForm.id == null || userForm.id === undefined" label="登录密码" prop="password" :label-width="formLabelWidth">
          <el-input type="password" v-model="userForm.password" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="联系电话" :label-width="formLabelWidth">
          <el-input v-model="userForm.phone" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="状态" :label-width="formLabelWidth">
          <el-switch
            v-model="userForm.status"
            :active-value="1"
            :inactive-value="0"
          ></el-switch>
        </el-form-item>
        <el-form-item label="电子邮件" prop="email" :label-width="formLabelWidth">
          <el-input v-model="userForm.email" autocomplete="off"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="userFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="saveUserFrom">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import userApi from '@/api/userManage'
export default {
  components: {
  },
  data() {
    // 自定义验证规则
    var checkEmail = (rule, value, callback)=>{
      var reg = /^[a-zA-Z0-9]+([-_.][a-zA-Z0-9]+)*@[a-zA-Z0-9]+([-_.][a-zA-Z0-9]+)*\.[a-z]{2,}$/
      if(!reg.test(value)){
        return callback(new Error('电子邮箱格式错误'));
      }
      callback();
    }
    return {
      total: 0,
      searchModel: {
        username: "",
        phone: "",
        pageNo: 1,
        pageSize: 10,
      },
      userList: [],
      title:"",
      userFormVisible: false,
      userForm:{},
      formLabelWidth: '130px',
      rules:{
        username:[
          {required:true, message: '请输入用户名', trigger: 'blur'},
          {min:5,max: 12, message: '长度在5到12个字符', trigger: 'blur'},
        ],
        password:[
          {required:true, message: '请输入初始登录密码', trigger: 'blur'},
          {min:6,max: 16, message: '长度在6到16个字符', trigger: 'blur'},
        ],
        email:[
          {required:true, message: '请输入电子邮箱', trigger: 'blur'},
          {validator:checkEmail, trigger: 'blur'},
        ],
      }
    }
  },
  methods: {
    // 删除用户
    deleteUser(user){
      this.$confirm(`你确认删除用户${user.username}?`, '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          userApi.deleteUserById(user.id).then(response => {
            this.$message({
              type: 'success',
              message: response.message
            });
            this.getUserList();
          })
          
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除'
          });          
        });
    },
    // 添加用户
    saveUserFrom(){
      // 触发表单验证
      this.$refs.userForm.validate((valid)=>{
        if(valid){
          // 提交请求给后台
          userApi.saveUser(this.userForm).then(response =>{
            // 成功提示
            this.$message({
              message: response.message,
              type: 'success'
            });
            // 关闭对话框
            this.userFormVisible = false;
            // 刷新表格
            this.getUserList();
          })
        }else{
            this.$message({
              message: "数据提交错误",
              type: 'error'
            });
          return false;
        }
      })
      
    },
    handleSizeChange(pageSize) {
      this.searchModel.pageSize = pageSize;
      this.getUserList();
    },
    handleCurrentChange(pageNo) {
      this.searchModel.pageSize = pageNo;
      this.getUserList();
    },
    getUserList() {
      userApi.getUserList(this.searchModel).then(response => {
        this.userList = response.data.row;
        this.total = response.data.total;
      });
    },
    openUserVisible(id) {
      if(id === null){
        this.title = "新增用户";
      }else{
        this.title = "修改用户";
        // 根据id查询用户
        userApi.getUserById(id).then(response =>{
          this.userForm = response.data
        })
      }
      this.userFormVisible = true;
    },
    clearUserForm(){
      this.userForm = {}
      this.$refs.userForm.clearValidate();
    }
  },
  created() {
    this.getUserList();
  }
}
</script>

<style scoped>
.search .el-input {
  width: 200px;
  margin-right: 10px;
}
.el-dialog .el-input{
  width: 85%;
}
</style>

在src/api目录下新建userManage.js文件

import request from '@/utils/request'

export default{
  getUserList(searchModel){
    return request({
      url: '/user/list',
      method: 'get',
      params :{
        pageNo: searchModel.pageNo,
        pageSize: searchModel.pageSize,
        username: searchModel.username,
        phone: searchModel.phone,
      }
    })
  },
  addUser(user){
    return request({
      url: '/user/add',
      method: 'post',
      data:user
    })
  },
  saveUser(user){
    if(user.id === null || user.id === undefined){
      return this.addUser(user)
    }else{
      return this.updateUser(user);
    }
  },
  updateUser(user){
    return request({
      url: '/user/upd',
      method: 'put',
      data: user
    })
  },
  getUserById(id){
    return request({
      url: `/user/${id}`,
      method: 'get'
    })
  },
  deleteUserById(id){
    return request({
      url: `/user/${id}`,
      method: 'delete'
    })
  },
}

5.用户管理页组件优化

当前页面所有内容都集中在index.vue中,代码多看起码繁杂,不适合后续拓展及优化,所有需内容按模块拆分成组件文件

在src/utils目录下新建eventbus.js文件,实现组件之间数据交互的方法

import Vue from "vue"
const EventBus = new Vue();
Object.defineProperties(Vue.prototype,{
    $bus:{
        get(){
            return EventBus;
        }
    }
})

main.js文件中引入eventbus

// 引入eventbus
import "./utils/eventbus"

在src/views/sys/user目录下新建components目录,用于存放当前页面的组件文件

新建SearchBar.vue文件,用于存放搜索框部分

<template>
    <!-- 搜索栏 -->
    <el-card class="search">
        <el-row>
            <el-col :span="20">
                <el-input v-model="searchContext.username" placeholder="用户名" clearable />
                <el-input v-model="searchContext.phone" placeholder="电话" clearable />
                <el-button @click="searchUser" type="primary" round icon="el-icon-search">查询</el-button>
            </el-col>
            <el-col :span="4" align="right">
                <el-button @click="openUserVisible" type="primary" icon="el-icon-plus" circle></el-button>
            </el-col>
        </el-row>
    </el-card>
</template>

<script>
export default {
    data(){
        return {
            searchContext: {
                username: "",
                phone: "",
            },
        }
    },
    methods:{
        searchUser(){
            this.$bus.$emit("searchUser",this.searchContext)
        },
        openUserVisible(){
            this.$bus.$emit("openUserVisible",null)
        }
    }
}
</script>

<style scoped>
.search .el-input {
  width: 200px;
  margin-right: 10px;
}
</style>

新建ResultList.vue文件,用于存放结果展示部分

<template>
  <!-- 结果列表 -->
  <el-card>
    <el-table :data="userList" stripe style="width:100%">
      <el-table-column label="#" width="80">
        <template slot-scope="scope">
          {{ (searchModel.pageNo - 1) * searchModel.pageSize + scope.$index + 1 }}
        </template>
      </el-table-column>
      <el-table-column prop="id" label="用户ID" width="80" />
      <el-table-column prop="username" label="用户名" width="180" />
      <el-table-column prop="phone" label="电话" width="180" />
      <el-table-column prop="status" label="状态" width="180">
        <template slot-scope="scope">
          <el-tag v-if="scope.row.status === 1">正常</el-tag>
          <el-tag v-else type="danger">禁用</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="email" label="电子邮件" />
      <el-table-column label="操作" width="180">
        <template slot-scope="scope">
          <el-button @click="openUserVisible(scope.row.id)" type="primary" icon="el-icon-edit" size="mini" circle />
          <el-button @click="deleteUser(scope.row)" type="danger" icon="el-icon-delete" size="mini" circle />
        </template>
      </el-table-column>
    </el-table>
  </el-card>
</template>

<script>
import userApi from '@/api/userManage'
export default {
  data() {
    return {
      userList: [],
      searchModel: {
        pageNo: 1,
        pageSize: 10,
      },
    }
  },
  methods: {
    // 删除用户
    deleteUser(user) {
      this.$confirm(`你确认删除用户${user.username}?`, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        userApi.deleteUserById(user.id).then(response => {
          this.$message({
            type: 'success',
            message: response.message
          });
          this.getUserList();
        })

      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        });
      });
    },
    // 获取数据
    getUserList() {
      userApi.getUserList(this.searchModel).then(response => {
        this.userList = response.data.row;
        this.$bus.$emit("getTotal",response.data.total)
      });
    },
    // 修改用户
    openUserVisible(id){
      console.log(1111111111111);
      this.$bus.$emit("openUserVisible",id)
    }
  },
  mounted(){
    // 查询数据
    this.$bus.$on("searchUser",searchContext =>{
      this.searchModel.username = searchContext.username;
      this.searchModel.phone = searchContext.phone;
      this.getUserList();
    })
    // 更新页面
    this.$bus.$on("refresh",() =>{
      this.getUserList();
    })
    // 分页
    this.$bus.$on("page_turning",searchModel =>{
      this.searchModel.pageNo = searchModel.pageNo;
      this.searchModel.pageSize = searchModel.pageSize;
      this.getUserList();
    })
  },
  created() {
    this.getUserList();
  }
}
</script>

新建ResultPage.vue文件,用于存放分页部分

<template>
    <!-- 分页组件 -->
    <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
        :current-page="searchModel.pageNo" :page-sizes="[5, 10, 20, 50]" :page-size="searchModel.pageSize"
        layout="total, sizes, prev, pager, next, jumper" :total="total">
    </el-pagination>
</template>

<script>
export default {
    data() {
        return {
            total: 0,
            searchModel: {
                pageNo: 1,
                pageSize: 10,
            }
        }
    },
    methods: {
        handleSizeChange(pageSize) {
            this.searchModel.pageSize = pageSize;
            this.$bus.$emit("page_turning",this.searchModel)
        },
        handleCurrentChange(pageNo) {
            this.searchModel.pageNo = pageNo;
            this.$bus.$emit("page_turning",this.searchModel)
        },
    },
    mounted(){
        this.$bus.$on("getTotal",total =>{
            this.total = total
        })
    }
}
</script>

新建UserDialog.vue文件,用于存放用户信息编辑对话框部分

<template>
    <div>
        <!-- 用户信息编辑对话框 -->
        <el-dialog @close="clearUserForm" :title="title" :visible.sync="userFormVisible">
            <el-form :model="userForm" ref="userForm" :rules="rules">
                <el-form-item label="用户名" prop="username" :label-width="formLabelWidth">
                    <el-input v-model="userForm.username" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item v-if="userForm.id == null || userForm.id === undefined" label="登录密码" prop="password"
                    :label-width="formLabelWidth">
                    <el-input type="password" v-model="userForm.password" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item label="联系电话" :label-width="formLabelWidth">
                    <el-input v-model="userForm.phone" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item label="状态" :label-width="formLabelWidth">
                    <el-switch v-model="userForm.status" :active-value="1" :inactive-value="0"></el-switch>
                </el-form-item>
                <el-form-item label="电子邮件" prop="email" :label-width="formLabelWidth">
                    <el-input v-model="userForm.email" autocomplete="off"></el-input>
                </el-form-item>
            </el-form>
            <div slot="footer" class="dialog-footer">
                <el-button @click="userFormVisible = false">取 消</el-button>
                <el-button type="primary" @click="saveUserFrom">确 定</el-button>
            </div>
        </el-dialog>
    </div>
</template>

<script>
import userApi from '@/api/userManage'
export default {
    data() {
        // 自定义验证规则
        var checkEmail = (rule, value, callback) => {
            var reg = /^[a-zA-Z0-9]+([-_.][a-zA-Z0-9]+)*@[a-zA-Z0-9]+([-_.][a-zA-Z0-9]+)*\.[a-z]{2,}$/
            if (!reg.test(value)) {
                return callback(new Error('电子邮箱格式错误'));
            }
            callback();
        }
        return {
            title: "",
            userFormVisible: false,
            userForm: {},
            formLabelWidth: '130px',
            rules: {
                username: [
                    { required: true, message: '请输入用户名', trigger: 'blur' },
                    { min: 5, max: 12, message: '长度在5到12个字符', trigger: 'blur' },
                ],
                password: [
                    { required: true, message: '请输入初始登录密码', trigger: 'blur' },
                    { min: 6, max: 16, message: '长度在6到16个字符', trigger: 'blur' },
                ],
                email: [
                    { required: true, message: '请输入电子邮箱', trigger: 'blur' },
                    { validator: checkEmail, trigger: 'blur' },
                ],
            }
        }
    },
    methods: {
        // 添加或修改用户
        saveUserFrom() {
            // 触发表单验证
            this.$refs.userForm.validate((valid) => {
                if (valid) {
                    // 提交请求给后台
                    userApi.saveUser(this.userForm).then(response => {
                        // 成功提示
                        this.$message({
                            message: response.message,
                            type: 'success'
                        });
                        // 关闭对话框
                        this.userFormVisible = false;
                        // 刷新表格
                        this.$bus.$emit("refresh")
                    })
                } else {
                    this.$message({
                        message: "数据提交错误",
                        type: 'error'
                    });
                    return false;
                }
            })
        },
        // 根据是否存在id显示弹出信息
        openUserVisible(id) {
            if (id === null) {
                this.title = "新增用户";
            } else {
                this.title = "修改用户";
                // 根据id查询用户
                userApi.getUserById(id).then(response => {
                    this.userForm = response.data
                })
            }
            this.userFormVisible = true;
        },
        // 清除弹窗内容
        clearUserForm() {
            this.userForm = {}
            this.$refs.userForm.clearValidate();
        }
    },
    mounted(){
        this.$bus.$on("openUserVisible",id =>{
            console.log(22222222);
            this.openUserVisible(id)
        })
    }
}
</script>

<style scoped>
.el-dialog .el-input{
  width: 85%;
}
</style>

修改user/index.vue文件

<template>
  <div>
    <SearchBar/>
    <ResultList/>
    <ResultPage/>
    <UserDialog/>
  </div>
</template>

<script>
import SearchBar from './components/SearchBar'
import ResultList from './components/ResultList'
import ResultPage from './components/ResultPage'
import UserDialog from './components/UserDialog'
export default {
  components: {
    SearchBar,ResultList,ResultPage,UserDialog
  }
}
</script>

运行效果正常

在这里插入图片描述

报错处理

1.使用TagsView组件控制台报错

报错截图

在这里插入图片描述

报错原因

未将TagsView所依赖的组件permission组件注册到store中,导致TagsView组件在找permission.routes时没找到

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2252475.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Docker】Docker配置远程访问

配置Docker的远程访问&#xff0c;你需要按照以下步骤进行操作&#xff1a; 1. 在Docker宿主机上配置Docker守护进程监听TCP端口 Docker守护进程默认只监听UNIX套接字&#xff0c;要实现远程访问&#xff0c;需要修改配置以监听TCP端口。 ‌方法一&#xff1a;修改Docker服务…

LeetCode hot100(自用背诵、部分题目、非最优解)

点击题目可以跳转到LeetCode 哈希 两数之和 public int[] twoSum(int[] nums, int target) {int lengthnums.length;int[] ans new int[2];for (int i 0; i <length-1 ; i) {for (int j i1; j < length; j) {if(nums[i]nums[j]target){ans[0]i;ans[1]j;}}}return an…

Android -- 简易音乐播放器

Android – 简易音乐播放器 播放器功能&#xff1a;* 1. 播放模式&#xff1a;单曲、列表循环、列表随机&#xff1b;* 2. 后台播放&#xff08;单例模式&#xff09;&#xff1b;* 3. 多位置同步状态回调&#xff1b;处理模块&#xff1a;* 1. 提取文件信息&#xff1a;音频文…

基础Web安全|SQL注入

基础Web安全 URI Uniform Resource Identifier&#xff0c;统一资源标识符&#xff0c;用来唯一的标识一个资源。 URL Uniform Resource Locator&#xff0c;统一资源定位器&#xff0c;一种具体的URI&#xff0c;可以标识一个资源&#xff0c;并且指明了如何定位这个资源…

ESG研究报告白皮书与ESG治理报告合集(2020-2023年)

一.资料范围&#xff1a;&#xff08;1&#xff09;ESG白皮书及指南;&#xff08;2&#xff09;ESG研究报告,&#xff08;3&#xff09;ESG治理报告分析&#xff08;4&#xff09;上市公司ESG报告&#xff08;知名企业&#xff09; 二、资料用途&#xff1a;可以分析研究企业E…

WPF指示灯的实现方式

WPF指示灯的实现方式 样式 XAML <Button x:Name"Btn1" Width"25" Height"25" Grid.Row"0" Grid.Column"1" Margin"10 5 5 5 "><Button.Template><ControlTemplate TargetType"Button"…

初识QT第一天

思维导图 利用Qt尝试做出原神登陆界面 import sys from PyQt6.QtGui import QIcon, QPixmap, QMovie from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QLineEdit# 封装原神窗口类 class Genshin(QWidget):# 构造函数def __init__(self):# 初始化父类…

【Linux】线程池设计 + 策略模式

&#x1f308; 个人主页&#xff1a;Zfox_ &#x1f525; 系列专栏&#xff1a;Linux 目录 一&#xff1a;&#x1f525; 线程池 1-1 ⽇志与策略模式1-2 线程池设计1-3 线程安全的单例模式1-3-1 什么是单例模式1-3-2 单例模式的特点1-3-3 饿汉实现⽅式和懒汉实现⽅式1-3-4 饿汉…

vim插件管理器vim-plug替代vim-bundle

文章目录 vim-plug与vim-bundle对比vim-plug安装vim-plug管理安装插件相关文章 vim-plug与vim-bundle对比 vim-plug 和 vim-bundle 都是 Vim 的插件管理器&#xff0c;但它们有一些关键的区别。以下是两者的主要对比&#xff1a; 易用性和简洁性 vim-plug: 易用性: vim-plug …

107.【C语言】数据结构之二叉树求总节点和第K层节点的个数

目录 1.求二叉树总的节点的个数 1.容易想到的方法 代码 缺陷 思考:能否在TreeSize函数内定义静态变量解决size的问题呢? 其他写法 运行结果 2.最好的方法:分而治之 代码 运行结果 2.求二叉树第K层节点的个数 错误代码 运行结果 修正 运行结果 其他写法 1.求二…

【代码随想录day48】【C++复健】739. 每日温度;496.下一个更大元素 I;503.下一个更大元素II

739. 每日温度 一顿操作猛如虎&#xff0c;一看击败5%。一眼顶针&#xff0c;鉴定为在存栈的时候把值和下标一起存了&#xff0c;所以导致了问题。 class Solution { public:vector<int> dailyTemperatures(vector<int>& temperatures) {stack<vector<…

vscode + conda + qt联合开发

安装vscode 安装conda 清华大学开源软件镜像(Anaconda下载)_清华大学镜像-CSDN博客 conda create新建一个环境&#xff0c;激活这个环境&#xff0c;然后安装pyside6 pip install pyside6 -i https://pypi.tuna.tsinghua.edu.cn/simple 安装成功后输入 pip list查看是否安装…

第十一篇 绘图matplotlib.pyplot的使用

文章目录 摘要安装方法入门案例使用plt绘图使用ax绘图plt.figure参数plot参数案例一 绘制红色实心的点状图案例二 绘制红色的破折线图案例三 绘制两条线颜色总结设置标题、轴名称、图例使用plt实现绘图使用ax实现绘图legend()中loc设置刻度plt自定义刻度ax自定义刻度plt.title …

Unity-Particle System属性介绍(一)基本属性

什么是ParticleSystem 粒子系统是Unity中用于模拟大量粒子的行为的组件。每个粒子都有一个生命周期&#xff0c;包括出生、运动、颜色变化、大小变化和死亡等。粒子系统可以用来创建烟雾、火焰、水、雨、雪、尘埃、闪电和其他各种视觉效果。 开始 在项目文件下创建一个Vfx文件…

计算机的错误计算(一百七十二)

摘要 探讨 MATLAB 对于算式 的计算误差。 例1. 在 MATLAB 中计算 的值。 直接贴图吧&#xff1a; 这样&#xff0c;MATLAB 的输出中只有3位正确数字&#xff0c;有效数字的错误率为 (16-3)/16 81.25% . 因为16位的正确输出为 0.2971242332737277e-18&#xff08;ISReals…

Flink四大基石之CheckPoint(检查点) 的使用详解

目录 一、Checkpoint 剖析 State 与 Checkpoint 概念区分 设置 Checkpoint 实战 执行代码所需的服务与遇到的问题 二、重启策略解读 重启策略意义 代码示例与效果展示 三、SavePoint 与 Checkpoint 异同 操作步骤详解 四、总结 在大数据流式处理领域&#xff0c;Ap…

S4 UPA of AA :新资产会计概览

通用并行会计&#xff08;Universal Parallel Accounting&#xff09;可以支持每个独立的分类账与其他模块集成&#xff0c;UPA主要是为了支持平行评估、多货币类型、财务合并、多准则财务报告的复杂业务需求 在ML层面UPA允许根据不同的分类账规则对物料进行评估&#xff0c;并…

Vue3学习宝典

1.ref函数调用的方式生成响应式数据&#xff0c;可以传复杂和简单数据类型 <script setup> // reactive接收一个对象类型的数据 import { reactive } from vue;// ref用函数调用的方式生成响应式数据&#xff0c;可以传复杂和简单数据类型 import { ref } from vue // 简…

Linux——基础命令(2) 文件内容操作

目录 ​编辑 文件内容操作 1.Vim &#xff08;1&#xff09;移动光标 &#xff08;2&#xff09;复制 &#xff08;3&#xff09;剪切 &#xff08;4&#xff09;删除 &#xff08;5&#xff09;粘贴 &#xff08;6&#xff09;替换,撤销,查找 &#xff08;7&#xff…

openwrt利用nftables在校园网环境下开启nat6 (ipv6 nat)

年初写过一篇openwrt在校园网环境下开启ipv6 nat的文章&#xff0c;利用ip6tables控制ipv6的流量。然而从OpenWrt22版本开始&#xff0c;系统内置的防火墙变为nftables&#xff0c;因此配置方法有所改变。本文主要参考了OpenWRT使用nftables实现IPv6 NAT 这篇文章。 友情提示 …