1、准备工作

1)前端页面

  • 将html页面放入templates目录
  • 将css,js,img放入到static目录

在这里插入图片描述

2)实体类

  • Department
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
    private Integer id;
    private String departmentName;
}
  • Employee
@Data
@NoArgsConstructor
public class Employee {
    private Integer id;
    private String lastName;
    private String email;

    private Integer gender;

    private Department department;
    private Date birth;

    public Employee(Integer id, String lastName, String email, Integer gender, Department department) {
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = gender;
        this.department = department;
        this.birth = new Date();
    }
}

3)Dao类

模拟数据库

  • DepartmentDao
@Repository
public class DepartmentDao {

    //模拟数据库数据

    private static Map<Integer, Department> departments = null;

    static {
        //创建一个部门表
        departments = new HashMap<Integer, Department>();

        departments.put(101,new Department(101,"教学部"));
        departments.put(102,new Department(102,"市场部"));
        departments.put(103,new Department(103,"教研部"));
        departments.put(104,new Department(104,"运营部"));
        departments.put(105,new Department(105,"后勤部"));
    }

    /**
     * 获取所有部门
     * @return
     */
    public Collection<Department> getAllDepartment(){
        return departments.values();
    }

    /**
     * 根据id得到部门
     * @param id
     * @return
     */
    public Department getDepartmentById(Integer id){
        return departments.get(id);
    }
}
  • EmployeeDao
@Repository
public class EmployeeDao {
    //模拟数据库数据

    private static Map<Integer, Employee> employees = null;
    /**
     * 员工所属部门
     */
    @Autowired
    private DepartmentDao departmentDao;

    static {
        //创建一个员工表
        employees = new HashMap<Integer, Employee>();

        employees.put(1001,new Employee(1001,"AA","A123456@qq.com",1,new Department(101,"教学部")));
        employees.put(1002,new Employee(1002,"BB","B123456@qq.com",0,new Department(102,"市场部")));
        employees.put(1003,new Employee(1003,"CC","C123456@qq.com",1,new Department(103,"教研部")));
        employees.put(1004,new Employee(1004,"DD","D123456@qq.com",0,new Department(104,"运营部")));
        employees.put(1005,new Employee(1005,"EE","E123456@qq.com",1,new Department(105,"后勤部")));
    }

    //主键自增
    private static Integer ininId = 1006;

    /**
     * 增加一个员工
     */
    public void save(Employee employee) {
        if (employee.getId() == null) {
            employee.setId(ininId++);
        }
        employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getId()));

        employees.put(employee.getId(),employee);
    }

    /**
     * 查询全部员工信息
     * @return
     */
    public Collection<Employee> getAll() {
        return employees.values();
    }

    /**
     * 通过id查询员工
     * @param id
     * @return
     */
    public Employee getEmployeeById(Integer id) {
        return employees.get(id);
    }

    /**
     * 删除员工通过id
     * @param id
     */
    public void delete(Integer id) {
        employees.remove(id);
    }
}

4)目录结构

在这里插入图片描述

2、首页实现

需要引入模板引擎

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

1)方式一

Controller(不建议使用

@Controller
public class IndexController {
    @RequestMapping({"/","/index.htm","/index.html"})
    public String index(){
        return "index";
    }
}

2)方式二

自定义配置类

addViewControllers()

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.htm").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
    }
}

3)加载静态资源

  • 导入thymeleaf包
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  • 将所有页面的静态资源使用thymeleaf接管
<!-- css的导入 -->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<link th:href="@{/css/signin.css}" rel="stylesheet">

<!-- 图片的导入 -->
<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">

<!-- js导入 -->
<script type="text/javascript" th:src="@{/js/jquery-3.2.1.slim.min.js}"></script>
<script type="text/javascript" th:src="@{/js/popper.min.js}"></script>
<script type="text/javascript" th:src="@{/js/bootstrap.min.js}"></script>

<script type="text/javascript" th:src="@{/js/feather.min.js}"></script>

<script type="text/javascript" th:src="@{/js/Chart.min.js}"></script>

在这里插入图片描述

3、页面国际化

1)准备工作

在IDEA中统一设置properties的编码问题!UTF-8

2)编写配置文件

  • 在resources资源文件下新建一个i18n目录,存放国际化配置文件
  • 建立一个login.properties文件,还有一个login_zh_CN.properties;IDEA会自动识别国际化操作;文件夹变了!统一在资源包下

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3)文件配置生效探究

MessageSourceAutoConfiguration 自动配置类

@Configuration
// ...
public class MessageSourceAutoConfiguration {
   @Bean
    @ConfigurationProperties(
        prefix = "spring.messages"
    )
    public MessageSourceProperties messageSourceProperties() {
        return new MessageSourceProperties();
    }
    
    // 获取 properties 传递过来的值进行判断
    @Bean
    public MessageSource messageSource(MessageSourceProperties properties) {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        if (StringUtils.hasText(properties.getBasename())) {
            //  设置国际化文件的基础名(去掉语言国家代码)
            messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
        }

        //  ...
        return messageSource;
    }
    
    // ...
    protected static class ResourceBundleCondition extends SpringBootCondition {
        // ...

        public ConditionOutcome getMatchOutcome(...) {
            String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
            // ...
            return outcome;
        }
    }
}    

配置messages基础路径

spring:
  # 国际化:省略 _en_US、_zh_CN
  messages:
    basename: i18n.login

4)配置页面国际化值

message取值操作为:#{...}

<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>

<input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
<input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">

<input type="checkbox" value="remember-me" th:text="#{login.remember}">

<button class="btn btn-lg btn-primary btn-block" type="submit"  th:text="#{login.btn}">Sign in</button>

在这里插入图片描述

在这里插入图片描述

<input type="checkbox" value="remember-me"> [[#{login.remember}]]

在这里插入图片描述

5)配置国际化解析

根据按钮自动切换中文英文!

在Spring中有一个国际化的Locale (区域信息对象);里面有一个叫做LocaleResolver (获取区域信息对象)的解析器!

@Bean
@ConditionalOnMissingBean(
    name = {"localeResolver"}
)
public LocaleResolver localeResolver() {
    if (this.webProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.WebProperties.LocaleResolver.FIXED) {
        // 容器中没有就自己配,有的话就用用户配置的
        return new FixedLocaleResolver(this.webProperties.getLocale());
    } else {
        // 接收头国际化分解
        AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
        localeResolver.setDefaultLocale(this.webProperties.getLocale());
        return localeResolver;
    }
}

在这里插入图片描述

public Locale resolveLocale(HttpServletRequest request) {
    Locale defaultLocale = this.getDefaultLocale();
    // 默认的就是根据请求头带来的区域信息获取Locale进行国际化
    if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
        return defaultLocale;
    } else {
        Locale requestLocale = request.getLocale();
        List<Locale> supportedLocales = this.getSupportedLocales();
        if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
            Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
            if (supportedLocale != null) {
                return supportedLocale;
            } else {
                return defaultLocale != null ? defaultLocale : requestLocale;
            }
        } else {
            return requestLocale;
        }
    }
}

在这里插入图片描述

  • 自定义 LocaleResolver,在链接上携带区域信息!

前端html

<!-- 这里传入参数不需要使用 ?使用 (key=value)-->
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>

MyLocaleResolver

public class MyLocaleResolver implements LocaleResolver {
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        // 获取请求参数
        String lang = request.getParameter("l");
        // 如果请求中没有区域值,就用默认的
        Locale locale = Locale.getDefault();
        if(StringUtils.hasText(lang)){
            // 分割请求参数
            String[] split = lang.split("_");
            // 语言、地区
            locale = new Locale(split[0], split[1]);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}

注册 MyLocaleResolver

在 MyMvcConfig 中添加 @Bean 方法

方法名必须为:localeResolver() ,不要乱写

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    // ...

    // 注册bean
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }
}

在这里插入图片描述

4、登录页面

1)表单提交地址

th:action="@{/user/login}"

<form class="form-signin" th:action="@{/user/login}">

2)LoginController

@Controller
public class LoginController {
    @RequestMapping("/user/login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Model model){
        if(StringUtils.hasText(username) && "123456".equals(password)){
            // 登录成功
            return "dashboard";
        }

        model.addAttribute("msg", "用户名或密码错误");
        return "index";
    }
}

3)错误提示

登录页显示错误提示: th:text="${msg}"

有消息时才提示:th:if="${not #strings.isEmpty(msg)}"

<p style="color:red;" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

在这里插入图片描述

4)地址栏泄露密码

在这里插入图片描述

解决方案

(1)提交方式改为:post

<form class="form-signin" th:action="@{/user/login}" method="post">

(2)映射跳转

登录成功后 redirect/main.html ,再由 main.html 映射到后台页面 dashboard.html

main.html 形式上是页面,实际上是作为中间跳转的一个桥梁(请求),在地址栏展示给用户,替代原来的用户名密码信息,并非一个真正页面

  • 登录成功后重定向到 main.html

修改 LoginController 的 login() 方法

// 登录成功
// return "dashboard";
return "redirect:/main.html";
  • main.htmldashboard.html

方式一:在 LoginController 中添加 main() 方法

@RequestMapping("/main.html")
public String main(){
    return "dashboard";
}

方式二:在 MyMvcConfig 中添加

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // ...
        registry.addViewController("/main.html").setViewName("dashboard");
    }
}    

在这里插入图片描述

5、登录拦截器

1)Controller 中添加 Session

@RequestMapping("/user/login")
public String login(@RequestParam("username") String username,
                    @RequestParam("password") String password,
                    Model model,
                    HttpSession session){
    if(StringUtils.hasText(username) && "123456".equals(password)){
        // 登录成功
        session.setAttribute("loginUser", username);
        return "redirect:/main.html";
    }

    model.addAttribute("msg", "用户名或密码错误");
    return "index";
}

2)自定义登录拦截器

public class LoginHandlerInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 检查session
        String loginUser = (String) request.getSession().getAttribute("loginUser");
        if(!StringUtils.hasText(loginUser)){
            // 无权限
            request.setAttribute("msg", "没有权限,请登录");
            // 请求转发到登录页
            request.getRequestDispatcher("/index.html").forward(request, response);
            // 拦截
            return false;
        }
        return true;
    }
}

3)添加拦截器

在 MyMvcConfig 中添加 addInterceptors()

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginHandlerInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/index.html","/index.htm","/","/user/login","/css/**","/js/**","/img/**");
    }
}    
  • addPathPatterns() 拦截路径
  • excludePathPatterns() 排除路径;静态资源的过滤,否则页面渲染效果会消失

4)前端显示登录用户

[[ ${session.loginUser} ]]

<!--顶部导航栏-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
    <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[ ${session.loginUser} ]]</a>
    <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
    <ul class="navbar-nav px-3">
        <li class="nav-item text-nowrap">
            <a class="nav-link" th:href="@{/user/logout}">注销</a>
        </li>
    </ul>
</nav>

在这里插入图片描述

6、员工列表展示

1)EmployeeController

@Controller
public class EmployeeController {
    @Autowired
    EmployeeDao employeeDao;

    @RequestMapping("/emps")
    public String list(Model model){
        Collection<Employee> employees = employeeDao.getAll();
        model.addAttribute("emps",employees);
        return "emp/list";
    }
}

2)修改侧边栏链接

侧边栏员工管理链接 th:href="@{/emps}"

<li class="nav-item">
    <a th:class="nav-lin" th:href="@{/emps}">
        <svg ...>
            ...
        </svg>
        员工管理
    </a>
</li>

3)提取公共页面

  • 顶部导航栏侧边栏 提取出来

    templates目录下面创建commons目录,在commons目录下面创建commons.html放公共代码,以供其它页面调用

    th:fragment="topbar" 指定供其它页面调用的键

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

    <!--顶部导航栏-->
    <nav class="..." th:fragment="topbar">
        ...
    </nav>

    <!--侧边栏-->
    <nav class="..." th:fragment="sidebar">
        ...
    </nav>

</html>
  • 调用公共页面

    th:insert="~{commons/commons::topbar}"

    th:replace="~{commons/commons::sidebar}"

    insertreplace 效果一样,一个插入,一个是替换;insert 会在外面多加一层div

<!--顶部导航栏-->
<div th:insert="~{commons/commons::topbar}"></div>

<div class="container-fluid">
    <div class="row">
        <!--侧边栏-->
        <div th:replace="~{commons/commons::sidebar}"></div>
		...
    </div>
</div>

4)侧边活动页高亮

  • 引用页面传参

    list.html (active='list.html')

    <!--侧边栏-->
    <div th:replace="~{commons/commons::sidebar(active='list.html')}"></div>
    

    dashboard.html (active='main.html')

    <!--侧边栏-->
    <!--传递参数给组件-->
    <div th:replace="~{commons/commons::sidebar(active='main.html')}"></div>
    
  • commons.html中接收参数并判断

    <li class="nav-item">
        <a th:class="${active=='main.html'?'nav-link active':'nav-link'}" th:href="@{/main.html}">
            ...
            首页
        </a>
    </li>
    <li class="nav-item">
        <a th:class="${active=='list.html'?'nav-link active':'nav-link'}" th:href="@{/emps}">
            ...
            员工管理
        </a>
    </li>
    

    th:class="${active=='main.html'?'nav-link active':'nav-link'}"

    三元运算符

    如果接收到的 active 等于 main.html,那么 class 的值就为 nav-link active,否则为 nav-link

5)员工信息循环展示

<tbody>
    <tr th:each="emp:${emps}">
        <td th:text="${emp.getId()}"></td>
        <td th:text="${emp.getLastName()}"></td>
        <td th:text="${emp.getEmail()}"></td>
        <td th:text="${emp.getGender()==0?'':''}"></td>
        <td th:text="${emp.department.getDepartmentName()}"></td>
        <td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td>
    </tr>
</tbody>

th:each="emp:${emps}" 遍历集合 empsemp 为当前元素

在这里插入图片描述

在这里插入图片描述

7、添加员工

1)添加按钮

位于 list.html 页面的员工列表上面

<h2><a class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a></h2>

2)跳转到添加页

EmployeeController

只接收 get 请求 @GetMapping

@GetMapping("/emp")
public String toAddPage(Model model){
    // 部门信息传给添加页面
    Collection<Department> departments = departmentDao.getAllDepartment();
    model.addAttribute("departments",departments);
    
    return "emp/add";
}

3)添加页

add.html

post 方式提交

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <form th:action="@{/emp}" method="post">
        <div class="form-group">
            <label>LastName</label>
            <input type="text" name="lastName" class="form-control" placeholder="姓名">
        </div>
        <div class="form-group">
            <label>Email</label>
            <input type="email" name="email" class="form-control" placeholder="xxx@xxx.xxx">
        </div>
        <div class="form-group">
            <label>Gender</label><br>
            <div class="form-check form-check-inline">
                <input class="form-check-input" type="radio" name="gender" value="1">
                <label class="form-check-label"></label>
            </div>
            <div class="form-check form-check-inline">
                <input class="form-check-input" type="radio" name="gender" value="0">
                <label class="form-check-label"></label>
            </div>
        </div>
        <div class="form-group">
            <label>Department</label>
            <!-- 提交的是id,所以name为department.id,提交后可以自动绑定 -->
            <select class="form-control" name="department.id">
                <!-- 遍历部门信息:显示名称,值为id -->
                <option th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}">1</option>
            </select>
        </div>
        <div class="form-group">
            <label>Birth</label>
            <input type="text" name="birth" class="form-control"  placeholder="2020-07-25">
        </div>
        <button type="submit" class="btn btn-primary">添加</button>
    </form>
</main>

注意:下拉框提交的时候应提交一个属性,因为其在controller接收的是一个Employee,否则不能自动绑定,会报错

4)添加员工

只接收 post 请求

参数为 employee 对象;要求提交表单的每一项name值与对象的字段对应

@PostMapping("/emp")
public String add(Employee employee){
    // 添加员工
    employeeDao.save(employee);
    // 跳转到员工列表页
    return "redirect:/emps";
}

5)自定义日期格式

系统默认日期格式

/**
* Date format to use, for example 'dd/MM/yyyy'.
*/
private String date;

/**
* Time format to use, for example 'HH:mm:ss'.
*/
private String time;

/**
* Date-time format to use, for example 'yyyy-MM-dd HH:mm:ss'.
*/
private String dateTime;
  • 自定义格式
spring:
  # 日期格式
  mvc:
    format:
      date: yyyy-MM-dd

在这里插入图片描述

在这里插入图片描述

8、修改员工

1)修改按钮

<a class="btn btn-sm btn-primary" th:href="@{/update/}+${emp.getId()}">编辑</a>

@{/update/}+${emp.getId()} => /update/1003 Restful风格

2)跳转到修改页

@GetMapping("update/{id}")
public String toUpdate(
    @PathVariable("id") Integer id,Model model){
    // 员工信息
    Employee employee = employeeDao.getEmployeeById(id);
    model.addAttribute("emp", employee);

    // 部门信息
    Collection<Department> departments = departmentDao.getAllDepartment();
    model.addAttribute("departments",departments);

    return "emp/update";
}

3)修改页

update.html

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <form th:action="@{/updateEmp}" method="post">
        <input type="hidden" name="id" th:value="${emp.getId()}">
        <div class="form-group">
            <label>LastName</label>
            <input type="text" name="lastName" class="form-control" placeholder="姓名" th:value="${emp.getLastName()}">
        </div>
        <div class="form-group">
            <label>Email</label>
            <input type="email" name="email" class="form-control" placeholder="xxx@xxx.xxx" th:value="${emp.getEmail()}">
        </div>
        <div class="form-group">
            <label>Gender</label><br>
            <div class="form-check form-check-inline">
                <input class="form-check-input" type="radio" name="gender" value="1" th:checked="${emp.getGender()==1}">
                <label class="form-check-label"></label>
            </div>
            <div class="form-check form-check-inline">
                <input class="form-check-input" type="radio" name="gender" value="0" th:checked="${emp.getGender()==1}">
                <label class="form-check-label"></label>
            </div>
        </div>
        <div class="form-group">
            <label>Department</label>
            <select class="form-control" name="department.id">
                <option th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"  th:selected="${dept.getId()==emp.getDepartment().getId()}">1</option>
            </select>
        </div>
        <div class="form-group">
            <label>Birth</label>
            <input type="text" name="birth" class="form-control"  placeholder="2020-07-25"  th:value="${#dates.format(emp.getBirth(),'yyyy-MM-dd')}">
        </div>
        <button type="submit" class="btn btn-primary">修改</button>
    </form>
</main>
  • <input type="hidden" name="id" th:value="${emp.getId()}"> 隐藏域(员工id)

  • th:checked="${emp.getGender()==1}" 当员工性别值为1时 checked 有效

    th:selected="${dept.getId()==emp.getDepartment().getId()}"

4)修改员工

@PostMapping("updateEmp")
public String update(Employee employee){
    employeeDao.save(employee);
    return "redirect:/emps";
}

9、删除员工

1)删除按钮

<a class="btn btn-sm btn-danger" th:href="@{/del/}+${emp.getId()}">删除</a>

2)删除员工

@GetMapping("/del/{id}")
public String del(@PathVariable("id") Integer id){
    employeeDao.delete(id);
    return "redirect:/emps";
}

10、404页

404.html 页面放入到 templates 目录下面的 error 目录中

其它页面类似;如:500等

在这里插入图片描述

11、注销

把登录时添加的session值设为null

  • 链接
<a class="nav-link" th:href="@{/user/logout}">注销</a>
  • Controller
@RequestMapping("/user/logout")
public String logout(HttpSession session){
    session.setAttribute("loginUser",null);
    return "index";
}

资源下载

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐