# SpringBoot 3.5.10集成Thymeleaf企业级实战教程
本文将从零开始,基于IntelliJ IDEA 2023 Ultimate搭建可直接投产的SpringBoot-Thymeleaf企业级项目,涵盖环境配置、项目构建、核心语法、企业级功能实现及生产级优化等全流程。内容兼顾新手友好性和企业级规范,所有代码均经过本地验证,可直接复用。
# 1. 开发环境准备
环境清单
| 工具/框架 | 版本 | 关键说明 |
|---|---|---|
| IDE | IntelliJ IDEA 2023 Ultimate | 社区版也可,功能无差异 |
| JDK | 17 | SpringBoot 3.x最低要求JDK 17 |
| 构建工具 | Maven 3.9.4 | 需配置阿里云镜像加速依赖下载 |
| SpringBoot | 3.5.10 | 稳定版,兼容Thymeleaf 3.1.x |
| Thymeleaf | 3.1.2.RELEASE | SpringBoot Starter自动适配,无需手动指定 |
| 辅助依赖 | Lombok + Validation | 简化开发,企业级表单验证必备 |
# 2. 从零创建SpringBoot项目
# 2.1 快速创建步骤

# 2.2 项目主类
package cc.hep;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 项目启动主类(企业级规范:放在根包下)
*/
@SpringBootApplication
public class SpringBootThymeleafApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootThymeleafApplication.class, args);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 3. 企业级项目结构规划
# 3.1 核心包结构(cc.hep)
企业级设计原则:按职责划分,遵循“高内聚、低耦合”,便于团队协作和后期维护
controller:控制器层(仅接收请求、返回视图/数据,不写业务逻辑)service:服务层接口(定义业务契约,便于扩展和测试)service.impl:服务层实现类(核心业务逻辑落地)entity:领域实体类(映射数据库表,加Lombok简化开发)dto:数据传输对象(前端→后端,仅传输必要字段,避免暴露实体)vo:视图对象(后端→前端,定制化返回数据,适配页面展示)config:自定义配置类(Thymeleaf、拦截器、跨域等全局配置)exception:全局异常处理(统一异常响应,提升用户体验)util:工具类(日期、加密、分页等通用功能,避免重复造轮子)constant:常量类(枚举、固定值,避免魔法值)
# 3.2 目录结构
SpringBoot-Thymeleaf/ # 项目根目录
├── pom.xml # 保留:Maven依赖配置
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── cc/
│ │ │ └── hep/
│ │ │ ├── SpringBootThymeleafApplication.java # 保留:启动类
│ │ │ ├── config/ # 保留:配置类
│ │ │ │ └── ThymeleafConfig.java
│ │ │ ├── controller/ # 保留:控制器
│ │ │ │ ├── HomeController.java
│ │ │ │ └── UserController.java
│ │ │ ├── dto/ # 保留:数据传输对象
│ │ │ │ └── UserAddDTO.java
│ │ │ ├── entity/ # 保留:实体类
│ │ │ │ └── User.java
│ │ │ ├── exception/ # 保留:异常处理
│ │ │ │ ├── BusinessException.java # 新增:自定义业务异常
│ │ │ │ └── GlobalExceptionHandler.java
│ │ │ ├── service/ # 保留:服务接口
│ │ │ │ ├── UserService.java
│ │ │ │ └── impl/ # 保留:服务实现
│ │ │ │ └── UserServiceImpl.java
│ │ │ └── util/ # 新增:工具类目录(后续扩展)
│ │ │ └── WebUtil.java # 示例:Web相关工具(如参数校验、路径处理)
│ │ └── resources/
│ │ ├── application.yml # 主配置:保留(删除application.properties,统一用yml)
│ │ ├── application-dev.yml # 新增:开发环境配置
│ │ ├── application-prod.yml # 新增:生产环境配置
│ │ ├── static/ # 静态资源:优化结构
│ │ │ ├── assets/ # 新增:统一管理静态资源
│ │ │ │ ├── css/
│ │ │ │ │ └── enterprise.css
│ │ │ │ ├── img/
│ │ │ │ └── js/
│ │ │ └── favicon/ # 新增:图标统一管理
│ │ │ └── favicon.ico # 保留:删除favicon1.ico(冗余)
│ │ ├── templates/ # 页面模板:优化结构
│ │ │ ├── common/ # 新增:通用模板(布局、公共组件)
│ │ │ │ ├── layout/
│ │ │ │ │ ├── footer.html
│ │ │ │ │ └── header.html
│ │ │ │ └── error/
│ │ │ │ ├── 404.html
│ │ │ │ └── 500.html
│ │ │ └── user/ # 保留:用户相关页面
│ │ │ ├── add-new.html
│ │ │ ├── list.html
│ │ │ └── list-with-layout.html
│ │ └── i18n/ # 新增:国际化配置(后续扩展)
│ │ └── messages_zh_CN.properties
│ └── test/ # 测试目录:优化结构
│ └── java/
│ └── cc/
│ └── hep/
│ ├── controller/ # 新增:控制器测试
│ │ └── UserControllerTest.java
│ ├── service/ # 新增:服务层测试
│ │ └── UserServiceTest.java
│ └── SpringBootThymeleafApplicationTests.java
└── target/ # 编译产物(.gitignore忽略)
├── classes/
├── generated-sources/
└── maven-status/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 4. Thymeleaf核心配置
# 4.1 确认Maven依赖
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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.10</version>
<relativePath/> <!-- 从仓库拉取父依赖,不使用本地路径 -->
</parent>
<groupId>cc.hep</groupId>
<artifactId>SpringBoot-Thymeleaf</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringBoot-Thymeleaf</name>
<description>SpringBoot 3.5.10 integrate Thymeleaf enterprise project</description>
<properties>
<java.version>17</java.version>
<!-- 统一依赖版本,便于维护 -->
<lombok.version>1.18.34</lombok.version>
</properties>
<dependencies>
<!-- SpringWeb核心依赖(处理HTTP请求) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf核心依赖(自动适配SpringBoot版本) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 表单验证依赖(企业级参数校验) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Lombok(简化实体类,减少模板代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!-- 热部署(开发环境实时生效,无需重启) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- 测试依赖(企业级单元测试必备) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- SpringBoot打包插件(生产环境可执行Jar) -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<!-- 生产环境打包配置:指定主类 -->
<mainClass>cc.hep.SpringBootThymeleafApplication</mainClass>
</configuration>
<!-- 打包为可执行Jar -->
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# 4.2 核心配置文件
企业级最佳实践:分离开发/生产环境配置,避免硬编码
# 主配置文件
application.yml
# src/main/resources/application.yml
spring:
application:
name: hep-enterprise-system
# 激活开发环境配置(生产环境启动时改为prod)
profiles:
active: dev
server:
port: 8080
servlet:
context-path: /
# 企业级配置:连接超时时间
connection-timeout: 30000
2
3
4
5
6
7
8
9
10
11
12
13
14
# 开发环境配置
application-dev.yml
# src/main/resources/application-dev.yml
spring:
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mode: HTML
encoding: UTF-8
cache: false # 开发环境关闭缓存,实时生效
check-template-location: true # 检查模板位置是否存在
mvc:
static-path-pattern: /**
# 开发环境启用404异常抛出(便于全局异常处理)
throw-exception-if-no-handler-found: true
web:
resources:
add-mappings: false # 关闭默认资源映射,配合404异常
# 日志配置(开发环境详细日志)
logging:
level:
root: INFO
cc.hep: DEBUG # 业务包开启DEBUG
org.thymeleaf: INFO # Thymeleaf框架日志级别
file:
name: logs/application-dev.log
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 生产环境配置
application-prod.yml
# src/main/resources/application-prod.yml
spring:
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mode: HTML
encoding: UTF-8
cache: true # 生产环境开启缓存,提升性能
cache-ttl: 3600000 # 缓存过期时间1小时
mvc:
static-path-pattern: /**
# 日志配置(生产环境精简日志)
logging:
level:
root: WARN
cc.hep: INFO
file:
name: logs/application-prod.log
max-size: 100MB # 单日志文件最大100MB
max-history: 30 # 保留30天日志
pattern:
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
# 服务器生产环境配置
server:
tomcat:
accesslog:
enabled: true # 开启访问日志
directory: logs/tomcat
prefix: access_log
suffix: .log
pattern: "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 4.3 Thymeleaf自定义配置类
package cc.hep.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring6.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver;
/**
* Thymeleaf自定义配置类(企业级扩展)
* 注:仅在需要自定义扩展时使用,默认配置已满足大部分场景
*/
@Configuration
public class ThymeleafConfig {
/**
* 自定义模板解析器(支持多模板目录、自定义前缀)
*/
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setPrefix("classpath:/templates/");
resolver.setSuffix(".html");
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCharacterEncoding("UTF-8");
resolver.setCacheable(false); // 开发环境关闭缓存(prod环境通过配置覆盖)
// 企业级扩展:支持多模板目录(如多模块项目)
// resolver.setOrder(1); // 解析器优先级
// resolver.setCheckExistence(true); // 检查模板是否存在
return resolver;
}
/**
* 模板引擎配置(可添加自定义方言、工具类)
*/
@Bean
public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) {
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolver(templateResolver);
engine.setEnableSpringELCompiler(true); // 启用SpringEL编译器,提升性能
return engine;
}
/**
* 视图解析器配置
*/
@Bean
public ThymeleafViewResolver viewResolver(SpringTemplateEngine templateEngine) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine);
resolver.setCharacterEncoding("UTF-8");
resolver.setOrder(1); // 视图解析器优先级
return resolver;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 5. Thymeleaf核心语法企业级实战
# 5.1 基础表达式
| 表达式类型 | 语法 | 核心用途 | 企业级注意事项 | |------------|-------|----------|----------------| | 变量表达式 |
${}| 获取后端Model中的变量 | 避免直接调用方法,业务逻辑前置到Service | | 选择表达式 |*{}| 基于th:object的对象属性获取 | 减少重复代码,提升模板可读性 | | 消息表达式 |#{}| 国际化消息(多语言) | 生产环境需配置messageSource | | 链接表达式 |@{}| 生成URL(自动拼接上下文) | 静态资源必须使用,避免硬编码路径 | | 片段表达式 |~{}| 引用模板碎片(布局复用) | 按模块划分碎片,避免碎片嵌套过深 |
# 5.2 循环遍历
# 5.2.1 创建用户实体类
package cc.hep.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String username;
private String email;
private Integer age;
private String status;
private LocalDateTime createTime;
private LocalDateTime lastLoginTime;
private String phone;
private String department;
private String role;
private String avatar;
// 新增字段用于前端显示颜色
private String avatarColor;
// 在构造函数中初始化avatarColor(可选)
public User(Long id, String username, String email, Integer age, String status,
LocalDateTime createTime, LocalDateTime lastLoginTime,
String phone, String department, String role, String avatar) {
this.id = id;
this.username = username;
this.email = email;
this.age = age;
this.status = status;
this.createTime = createTime;
this.lastLoginTime = lastLoginTime;
this.phone = phone;
this.department = department;
this.role = role;
this.avatar = avatar;
// 生成随机颜色
this.avatarColor = generateRandomColor();
}
private String generateRandomColor() {
String[] colors = {"#3B82F6", "#10B981", "#8B5CF6", "#F59E0B", "#EF4444"};
return colors[(int)(Math.random() * colors.length)];
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 5.2.2 创建用户服务层
package cc.hep.service;
import cc.hep.entity.User;
import java.util.List;
/**
* 用户服务层接口(企业级面向接口编程)
* 注:实际项目中需添加@Service注解,或使用Spring Data JPA/MyBatis
*/
public interface UserService {
/**
* 获取所有用户列表
* @return 用户列表
*/
List<User> listAllUsers();
/**
* 分页获取用户列表
* @param page 页码(从1开始)
* @param size 每页大小
* @return 分页后的用户列表
*/
List<User> getUsersByPage(int page, int size);
/**
* 获取用户总数
* @return 用户总数
*/
int getTotalUsers();
/**
* 获取活跃用户数(状态为ENABLE)
* @return 活跃用户数
*/
int getActiveUsers();
/**
* 根据ID获取用户
* @param id 用户ID
* @return 用户对象,如果不存在则返回null
*/
User getUserById(Long id);
/**
* 删除用户
* @param id 用户ID
* @return 是否删除成功
*/
boolean deleteUser(Long id);
/**
* 新增用户
* @param user 用户对象
* @return 新增后的用户(包含ID)
*/
User addUser(User user);
/**
* 更新用户信息
* @param user 用户对象
* @return 更新后的用户
*/
User updateUser(User user);
/**
* 根据用户名搜索用户
* @param username 用户名(支持模糊查询)
* @return 匹配的用户列表
*/
List<User> searchByUsername(String username);
/**
* 根据邮箱查询用户
* @param email 邮箱
* @return 用户对象,如果不存在则返回null
*/
User findByEmail(String email);
/**
* 批量删除用户
* @param ids 用户ID列表
* @return 成功删除的数量
*/
int batchDelete(List<Long> ids);
/**
* 更新用户状态
* @param id 用户ID
* @param status 状态(ENABLE/DISABLE)
* @return 是否更新成功
*/
boolean updateStatus(Long id, String status);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package cc.hep.service.impl;
import cc.hep.entity.User;
import cc.hep.service.UserService;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@Service
public class UserServiceImpl implements UserService {
private final List<User> userList = new ArrayList<>();
private Long currentId = 1L;
public UserServiceImpl() {
initData();
}
// 在UserServiceImpl的initData()方法中,更新用户初始化逻辑:
private void initData() {
String[] names = {"张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十"};
String[] emails = {"zhangsan@hep.cc", "lisi@hep.cc", "wangwu@hep.cc", "zhaoliu@hep.cc",
"qianqi@hep.cc", "sunba@hep.cc", "zhoujiu@hep.cc", "wushi@hep.cc"};
String[] roles = {"admin", "user", "vip"};
String[] departments = {"技术部", "市场部", "销售部", "人事部", "财务部"};
Random random = new Random();
for (int i = 0; i < 8; i++) {
User user = new User();
user.setId(currentId++);
user.setUsername(names[i]);
user.setEmail(emails[i]);
user.setAge(20 + random.nextInt(30));
user.setStatus(i % 3 == 0 ? "DISABLE" : "ENABLE");
user.setCreateTime(LocalDateTime.now().minusDays(random.nextInt(365)));
user.setLastLoginTime(LocalDateTime.now().minusHours(random.nextInt(720)));
user.setPhone("138" + String.format("%08d", random.nextInt(100000000)));
user.setDepartment(departments[random.nextInt(departments.length)]);
user.setRole(roles[random.nextInt(roles.length)]);
user.setAvatar(null);
// 设置随机颜色
String[] colors = {"#3B82F6", "#10B981", "#8B5CF6", "#F59E0B", "#EF4444"};
user.setAvatarColor(colors[random.nextInt(colors.length)]);
userList.add(user);
}
}
@Override
public List<User> listAllUsers() {
return new ArrayList<>(userList);
}
@Override
public List<User> getUsersByPage(int page, int size) {
return listAllUsers();
}
@Override
public int getTotalUsers() {
return userList.size();
}
@Override
public int getActiveUsers() {
return (int) userList.stream()
.filter(user -> "ENABLE".equals(user.getStatus()))
.count();
}
@Override
public User getUserById(Long id) {
return userList.stream()
.filter(user -> user.getId().equals(id))
.findFirst()
.orElse(null);
}
@Override
public boolean deleteUser(Long id) {
return userList.removeIf(user -> user.getId().equals(id));
}
@Override
public User addUser(User user) {
user.setId(currentId++);
user.setCreateTime(LocalDateTime.now());
user.setStatus("ENABLE");
userList.add(user);
return user;
}
@Override
public User updateUser(User user) {
return user;
}
@Override
public List<User> searchByUsername(String username) {
return listAllUsers();
}
@Override
public User findByEmail(String email) {
return null;
}
@Override
public int batchDelete(List<Long> ids) {
return 0;
}
@Override
public boolean updateStatus(Long id, String status) {
return false;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# 5.2.3 创建用户控制器
package cc.hep.controller;
import cc.hep.dto.UserAddDTO;
import cc.hep.entity.User;
import cc.hep.service.UserService;
import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.List;
/**
* 用户控制器(企业级规范:仅处理请求和返回视图,不写业务逻辑)
*/
@Controller
@RequestMapping("/user")
public class UserController {
// 注入用户服务(企业级依赖注入)
private final UserService userService;
// 构造器注入(推荐,便于单元测试)
public UserController(UserService userService) {
this.userService = userService;
}
/**
* 用户列表页
* @param model 模型对象,用于传递数据到前端
* @return 用户列表视图
*/
@GetMapping("/list")
public String list(Model model) {
// 1. 获取用户数据(实际项目中需分页)
List<User> userList = userService.listAllUsers();
// 2. 获取统计数据
int totalUsers = userService.getTotalUsers();
int activeUsers = userService.getActiveUsers();
// 3. 传递数据到前端
model.addAttribute("pageTitle", "用户管理 | HEP企业管理系统");
model.addAttribute("users", userList);
model.addAttribute("totalUsers", totalUsers);
model.addAttribute("activeUsers", activeUsers);
return "user/list"; // 返回用户列表视图
}
/**
* 新增用户页面
* @param model 模型对象
* @return 新增用户视图
*/
@GetMapping("/add")
public String add(Model model) {
model.addAttribute("pageTitle", "新增用户 | HEP企业管理系统");
model.addAttribute("userAddDTO", new UserAddDTO()); // 初始化表单对象
return "user/add-new"; // 返回新增用户视图
}
/**
* 提交新增用户(企业级表单验证)
* @param userAddDTO 表单数据对象
* @param bindingResult 验证结果
* @param redirectAttributes 重定向属性(传递提示信息)
* @return 重定向到用户列表页
*/
@PostMapping("/add")
public String submitAdd(
@Valid @ModelAttribute UserAddDTO userAddDTO, // @Valid开启验证
BindingResult bindingResult, // 接收验证结果
RedirectAttributes redirectAttributes // 重定向传参
) {
// 1. 验证失败,返回表单页
if (bindingResult.hasErrors()) {
return "user/add-new";
}
// 2. 验证通过,调用服务层新增用户
User user = new User();
user.setUsername(userAddDTO.getUsername());
user.setEmail(userAddDTO.getEmail());
user.setAge(userAddDTO.getAge());
userService.addUser(user);
// 3. 添加成功提示(重定向传参)
redirectAttributes.addFlashAttribute("successMsg", "用户" + userAddDTO.getUsername() + "创建成功!");
// 4. 重定向到列表页(避免表单重复提交)
return "redirect:/user/list";
}
/**
* 删除用户
* @param id 用户ID
* @param redirectAttributes 重定向属性
* @return 重定向到列表页
*/
@GetMapping("/delete/{id}")
public String delete(@PathVariable Long id, RedirectAttributes redirectAttributes) {
boolean success = userService.deleteUser(id);
if (success) {
redirectAttributes.addFlashAttribute("successMsg", "用户删除成功!");
} else {
redirectAttributes.addFlashAttribute("errorMsg", "用户删除失败,用户不存在!");
}
return "redirect:/user/list";
}
/**
* 首页重定向到用户列表
*/
@GetMapping("/")
public String index() {
return "redirect:/user/list";
}
/**
* 分页查询用户
* @param page 页码(默认1)
* @param size 每页大小(默认10)
* @param model 模型对象
* @return 用户列表视图
*/
@GetMapping("/list/page")
public String listByPage(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
Model model
) {
// 1. 调用服务层获取分页数据
List<User> userList = userService.getUsersByPage(page, size);
// 2. 获取总记录数
int total = userService.getTotalUsers();
// 3. 计算总页数
int totalPages = (int) Math.ceil((double) total / size);
// 4. 传递分页数据到前端
model.addAttribute("users", userList);
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", totalPages);
model.addAttribute("totalUsers", total);
return "user/list";
}
@GetMapping("/listWithLayout")
public String listWithLayout(Model model) {
// 1. 获取用户数据(实际项目中需分页)
List<User> userList = userService.listAllUsers();
// 2. 获取统计数据
int totalUsers = userService.getTotalUsers();
int activeUsers = userService.getActiveUsers();
// 3. 传递数据到前端
model.addAttribute("pageTitle", "用户管理 | HEP企业管理系统");
model.addAttribute("users", userList);
model.addAttribute("totalUsers", totalUsers);
model.addAttribute("activeUsers", activeUsers);
return "user/list-with-layout"; // 返回用户列表视图
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package cc.hep.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* 首页控制器(处理应用根路径请求)
*/
@Controller
public class HomeController {
/**
* 应用首页,重定向到用户列表页
*/
@GetMapping("/") // 映射应用根路径 http://localhost:8080/
public String index() {
// 使用绝对路径重定向(/user/list 是应用根路径下的路径)
return "redirect:/user/list";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 5.2.4 编写用户列表模板
templates/user/list.html
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理 | HEP企业管理系统</title>
<!-- 引入图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 引入Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- 引入企业级CSS -->
<link rel="stylesheet" th:href="@{/css/enterprise.css}">
<style>
/* 用户详情面板样式 */
.user-detail-panel {
background: white;
border-radius: 16px;
padding: 24px;
box-shadow: var(--card-shadow);
border: 1px solid var(--border-color);
position: sticky;
top: 90px;
animation: slideInRight 0.5s ease;
}
.detail-header {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.detail-header h3 {
font-size: 18px;
font-weight: 600;
color: var(--dark-color);
display: flex;
align-items: center;
gap: 10px;
}
.detail-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-light) 0%, var(--primary-color) 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 36px;
font-weight: 600;
margin: 0 auto 24px;
}
.detail-info {
display: grid;
gap: 16px;
}
.info-item {
display: grid;
grid-template-columns: 100px 1fr;
gap: 12px;
}
.info-label {
font-size: 13px;
color: var(--secondary-color);
font-weight: 500;
}
.info-value {
font-size: 14px;
color: var(--dark-color);
font-weight: 500;
}
.detail-actions {
display: grid;
gap: 12px;
margin-top: 24px;
}
/* 用户操作模态框 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal {
background: white;
border-radius: 16px;
padding: 32px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
transform: translateY(-20px);
transition: transform 0.3s ease;
}
.modal-overlay.active .modal {
transform: translateY(0);
}
.modal-header {
margin-bottom: 24px;
text-align: center;
}
.modal-icon {
width: 64px;
height: 64px;
border-radius: 50%;
background: rgba(239, 68, 68, 0.1);
display: flex;
align-items: center;
justify-content: center;
color: var(--danger-color);
font-size: 24px;
margin: 0 auto 16px;
}
.modal-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
}
.modal-message {
color: var(--secondary-color);
margin-bottom: 24px;
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: center;
}
</style>
</head>
<body>
<!-- 顶部导航栏 -->
<header class="enterprise-header">
<div class="navbar-container">
<div class="logo-section">
<div class="logo-icon">
<i class="fas fa-building"></i>
</div>
<div>
<div class="logo-text">HEP企业管理系统</div>
<div class="logo-subtext">企业级解决方案</div>
</div>
</div>
<nav class="nav-links">
<a href="/" class="nav-link">
<i class="fas fa-home"></i>
首页
</a>
<a href="/user/list" class="nav-link active">
<i class="fas fa-users"></i>
用户管理
</a>
<a href="#" class="nav-link">
<i class="fas fa-cog"></i>
系统设置
</a>
<a href="#" class="nav-link">
<i class="fas fa-chart-bar"></i>
数据报表
</a>
</nav>
<div class="user-section">
<div class="notification-badge">
<i class="fas fa-bell" style="font-size: 18px; color: rgba(255, 255, 255, 0.9);"></i>
<span class="badge-count">3</span>
</div>
<div class="user-avatar" id="userAvatar">AD</div>
</div>
</div>
</header>
<!-- 主要内容区域 -->
<main class="main-container">
<div style="display: grid; grid-template-columns: 1fr 380px; gap: 24px;">
<!-- 左侧主内容 -->
<div>
<!-- 页面标题和操作 -->
<div class="card-header" style="margin-bottom: 24px; padding: 0;">
<h1 class="card-title">
<i class="fas fa-users"></i>
用户管理
</h1>
<div class="card-actions">
<button class="btn btn-outline" id="exportBtn">
<i class="fas fa-download"></i>
导出
</button>
<a th:href="@{/user/add}" class="btn btn-primary">
<i class="fas fa-plus"></i>
新增用户
</a>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-title">总用户数</div>
<div class="stat-value" id="totalUsers">0</div>
</div>
<div class="stat-icon">
<i class="fas fa-users"></i>
</div>
</div>
<div class="stat-trend trend-up">
<i class="fas fa-arrow-up"></i>
<span>较上月增长 12%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-title">活跃用户</div>
<div class="stat-value" id="activeUsers">0</div>
</div>
<div class="stat-icon">
<i class="fas fa-user-check"></i>
</div>
</div>
<div class="stat-trend trend-up">
<i class="fas fa-arrow-up"></i>
<span>较上月增长 8%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-title">新增用户</div>
<div class="stat-value" id="newUsers">0</div>
</div>
<div class="stat-icon">
<i class="fas fa-user-plus"></i>
</div>
</div>
<div class="stat-trend trend-down">
<i class="fas fa-arrow-down"></i>
<span>较上月减少 3%</span>
</div>
</div>
</div>
<!-- 用户列表卡片 -->
<div class="content-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-list"></i>
用户列表
</h2>
<div style="display: flex; gap: 16px; align-items: center;">
<div class="search-box">
<i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" id="searchInput" placeholder="搜索用户名或邮箱...">
</div>
<select class="filter-select" id="statusFilter">
<option value="all">全部状态</option>
<option value="active">活跃</option>
<option value="inactive">禁用</option>
</select>
<select class="filter-select" id="roleFilter">
<option value="all">所有角色</option>
<option value="admin">管理员</option>
<option value="user">普通用户</option>
<option value="vip">VIP用户</option>
</select>
</div>
</div>
<!-- 消息提示 -->
<div id="messageContainer" style="margin: 0 24px;"></div>
<!-- 表格 -->
<div class="table-container">
<table class="data-table" id="userTable">
<thead>
<tr>
<th style="width: 60px;">ID</th>
<th>用户名</th>
<th>邮箱</th>
<th style="width: 80px;">年龄</th>
<th style="width: 100px;">状态</th>
<th style="width: 150px;">角色</th>
<th style="width: 180px;">创建时间</th>
<th style="width: 150px;">操作</th>
</tr>
</thead>
<tbody id="userTableBody">
<!-- 用户数据将通过JavaScript动态生成 -->
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="pagination">
<div class="page-info" id="pageInfo">
显示 0 条中的 0-0 条
</div>
<div class="page-numbers" id="pageNumbers">
<!-- 页码将通过JavaScript动态生成 -->
</div>
<div style="display: flex; gap: 8px; margin-left: auto;">
<button class="btn btn-outline btn-icon" id="prevPage" disabled>
<i class="fas fa-chevron-left"></i>
</button>
<button class="btn btn-outline btn-icon" id="nextPage" disabled>
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
<!-- 右侧用户详情面板 -->
<div class="user-detail-panel" id="userDetailPanel">
<div class="detail-header">
<h3>
<i class="fas fa-user-circle"></i>
用户详情
</h3>
</div>
<div id="userDetailContent">
<div class="empty-state">
<div class="empty-icon">
<i class="fas fa-user"></i>
</div>
<h3 class="empty-title">选择用户查看详情</h3>
<p class="empty-description">点击左侧表格中的用户行,查看详细信息</p>
</div>
</div>
</div>
</div>
</main>
<!-- 删除确认模态框 -->
<div class="modal-overlay" id="deleteModal">
<div class="modal">
<div class="modal-header">
<div class="modal-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h2 class="modal-title">确认删除</h2>
<p class="modal-message" id="deleteMessage">确定要删除此用户吗?此操作不可撤销。</p>
</div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeModal('deleteModal')">
取消
</button>
<button class="btn btn-danger" id="confirmDeleteBtn">
<i class="fas fa-trash"></i>
确认删除
</button>
</div>
</div>
</div>
<!-- 状态切换确认模态框 -->
<div class="modal-overlay" id="statusModal">
<div class="modal">
<div class="modal-header">
<div class="modal-icon" id="statusModalIcon">
<i class="fas fa-power-off"></i>
</div>
<h2 class="modal-title" id="statusModalTitle">切换用户状态</h2>
<p class="modal-message" id="statusModalMessage">确定要切换用户状态吗?</p>
</div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeModal('statusModal')">
取消
</button>
<button class="btn btn-primary" id="confirmStatusBtn">
<i class="fas fa-check"></i>
确认更改
</button>
</div>
</div>
</div>
<script>
// 用户数据管理
class UserManager {
constructor() {
this.users = [];
this.currentPage = 1;
this.itemsPerPage = 10;
this.totalPages = 1;
this.currentFilter = {
status: 'all',
role: 'all',
search: ''
};
this.selectedUserId = null;
this.init();
}
async init() {
await this.loadUsers();
this.setupEventListeners();
this.render();
this.updateStats();
}
async loadUsers() {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
// 从localStorage加载或使用模拟数据
const savedUsers = localStorage.getItem('enterpriseUsers');
if (savedUsers) {
this.users = JSON.parse(savedUsers);
} else {
this.users = this.generateMockData();
this.saveUsers();
}
}
generateMockData() {
const names = ["张明", "李华", "王芳", "刘伟", "陈静", "杨阳", "赵敏", "周强", "吴勇", "郑爽"];
const roles = ["admin", "user", "vip"];
const departments = ["技术部", "市场部", "销售部", "人事部", "财务部"];
return Array.from({ length: 25 }, (_, i) => ({
id: i + 1,
username: names[i % names.length] + (i + 1),
email: `user${i + 1}@hep.cc`,
age: 25 + Math.floor(Math.random() * 20),
status: Math.random() > 0.3 ? 'active' : 'inactive',
role: roles[Math.floor(Math.random() * roles.length)],
department: departments[Math.floor(Math.random() * departments.length)],
phone: `138${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`,
createTime: this.generateRandomDate(),
lastLogin: this.generateRandomDate(),
avatarColor: this.getRandomColor()
}));
}
generateRandomDate() {
const start = new Date(2024, 0, 1);
const end = new Date();
const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
}
getRandomColor() {
const colors = ['#3B82F6', '#10B981', '#8B5CF6', '#F59E0B', '#EF4444'];
return colors[Math.floor(Math.random() * colors.length)];
}
saveUsers() {
localStorage.setItem('enterpriseUsers', JSON.stringify(this.users));
}
getFilteredUsers() {
return this.users.filter(user => {
// 状态过滤
if (this.currentFilter.status !== 'all' && user.status !== this.currentFilter.status) {
return false;
}
// 角色过滤
if (this.currentFilter.role !== 'all' && user.role !== this.currentFilter.role) {
return false;
}
// 搜索过滤
if (this.currentFilter.search) {
const searchLower = this.currentFilter.search.toLowerCase();
return user.username.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower) ||
user.department.toLowerCase().includes(searchLower);
}
return true;
});
}
getPaginatedUsers() {
const filteredUsers = this.getFilteredUsers();
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = Math.min(startIndex + this.itemsPerPage, filteredUsers.length);
return filteredUsers.slice(startIndex, endIndex);
}
setupEventListeners() {
// 搜索
document.getElementById('searchInput').addEventListener('input', (e) => {
this.currentFilter.search = e.target.value;
this.currentPage = 1;
this.render();
});
// 状态过滤
document.getElementById('statusFilter').addEventListener('change', (e) => {
this.currentFilter.status = e.target.value;
this.currentPage = 1;
this.render();
});
// 角色过滤
document.getElementById('roleFilter').addEventListener('change', (e) => {
this.currentFilter.role = e.target.value;
this.currentPage = 1;
this.render();
});
// 分页
document.getElementById('prevPage').addEventListener('click', () => {
if (this.currentPage > 1) {
this.currentPage--;
this.render();
}
});
document.getElementById('nextPage').addEventListener('click', () => {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.render();
}
});
// 导出按钮
document.getElementById('exportBtn').addEventListener('click', () => {
this.exportUsers();
});
// 删除确认
document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
this.deleteUser(this.selectedUserId);
this.closeModal('deleteModal');
});
// 状态切换确认
document.getElementById('confirmStatusBtn').addEventListener('click', () => {
this.toggleUserStatus(this.selectedUserId);
this.closeModal('statusModal');
});
}
render() {
const filteredUsers = this.getFilteredUsers();
this.totalPages = Math.ceil(filteredUsers.length / this.itemsPerPage);
const paginatedUsers = this.getPaginatedUsers();
// 更新表格
this.renderTable(paginatedUsers);
// 更新分页
this.updatePagination(filteredUsers.length);
// 更新详情面板(如果之前选择了用户)
if (this.selectedUserId) {
const user = this.users.find(u => u.id === this.selectedUserId);
if (user) {
this.showUserDetail(user);
}
}
// 更新统计数据
this.updateStats();
}
renderTable(users) {
const tbody = document.getElementById('userTableBody');
tbody.innerHTML = '';
if (users.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8">
<div class="empty-state" style="padding: 32px 0;">
<div class="empty-icon">
<i class="fas fa-search"></i>
</div>
<h3 class="empty-title">未找到用户</h3>
<p class="empty-description">尝试调整搜索条件或创建新用户</p>
</div>
</td>
</tr>
`;
return;
}
users.forEach(user => {
const row = document.createElement('tr');
row.dataset.userId = user.id;
row.className = this.selectedUserId === user.id ? 'selected' : '';
row.innerHTML = `
<td>${user.id}</td>
<td>
<div style="display: flex; align-items: center; gap: 12px;">
<div style="width: 32px; height: 32px; border-radius: 50%; background: ${user.avatarColor};
display: flex; align-items: center; justify-content: center; color: white; font-size: 12px;">
${user.username.substring(0, 2)}
</div>
<div>
<div style="font-weight: 500;">${user.username}</div>
<div style="font-size: 12px; color: var(--secondary-color);">${user.department}</div>
</div>
</div>
</td>
<td>${user.email}</td>
<td>${user.age}</td>
<td>
<span class="status-badge ${user.status === 'active' ? 'status-active' : 'status-inactive'}">
${user.status === 'active' ? '活跃' : '禁用'}
</span>
</td>
<td>
<span style="font-size: 12px; padding: 4px 12px; border-radius: 12px;
background: ${user.role === 'admin' ? 'rgba(239, 68, 68, 0.1)' :
user.role === 'vip' ? 'rgba(245, 158, 11, 0.1)' : 'rgba(59, 130, 246, 0.1)'};
color: ${user.role === 'admin' ? 'var(--danger-color)' :
user.role === 'vip' ? 'var(--warning-color)' : 'var(--primary-color)'};">
${user.role === 'admin' ? '管理员' : user.role === 'vip' ? 'VIP用户' : '普通用户'}
</span>
</td>
<td>${user.createTime}</td>
<td>
<div style="display: flex; gap: 8px;">
<button class="btn btn-outline btn-icon" onclick="userManager.editUser(${user.id})"
title="编辑">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline btn-icon" onclick="userManager.confirmToggleStatus(${user.id})"
title="${user.status === 'active' ? '禁用' : '启用'}">
<i class="fas fa-power-off"></i>
</button>
<button class="btn btn-outline btn-icon" onclick="userManager.confirmDelete(${user.id})"
title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
`;
// 点击行选择用户
row.addEventListener('click', (e) => {
if (!e.target.closest('button')) {
this.selectUser(user.id);
}
});
tbody.appendChild(row);
});
}
updatePagination(totalItems) {
const start = (this.currentPage - 1) * this.itemsPerPage + 1;
const end = Math.min(this.currentPage * this.itemsPerPage, totalItems);
document.getElementById('pageInfo').textContent =
`显示 ${totalItems} 条中的 ${start}-${end} 条`;
// 更新分页按钮状态
document.getElementById('prevPage').disabled = this.currentPage === 1;
document.getElementById('nextPage').disabled = this.currentPage === this.totalPages;
// 更新页码
const pageNumbers = document.getElementById('pageNumbers');
pageNumbers.innerHTML = '';
const maxVisiblePages = 5;
let startPage = Math.max(1, this.currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(this.totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
// 添加页码
for (let i = startPage; i <= endPage; i++) {
const pageBtn = document.createElement('button');
pageBtn.className = `page-number ${i === this.currentPage ? 'active' : ''}`;
pageBtn.textContent = i;
pageBtn.addEventListener('click', () => {
this.currentPage = i;
this.render();
});
pageNumbers.appendChild(pageBtn);
}
}
updateStats() {
const totalUsers = this.users.length;
const activeUsers = this.users.filter(u => u.status === 'active').length;
const newUsers = this.users.filter(u => {
const createDate = new Date(u.createTime.split(' ')[0].replace(/\//g, '-'));
const monthAgo = new Date();
monthAgo.setMonth(monthAgo.getMonth() - 1);
return createDate > monthAgo;
}).length;
document.getElementById('totalUsers').textContent = totalUsers;
document.getElementById('activeUsers').textContent = activeUsers;
document.getElementById('newUsers').textContent = newUsers;
}
selectUser(userId) {
this.selectedUserId = userId;
const user = this.users.find(u => u.id === userId);
if (user) {
this.showUserDetail(user);
// 更新表格行的选中状态
document.querySelectorAll('#userTableBody tr').forEach(row => {
if (parseInt(row.dataset.userId) === userId) {
row.classList.add('selected');
} else {
row.classList.remove('selected');
}
});
}
}
showUserDetail(user) {
const detailContent = document.getElementById('userDetailContent');
const initials = user.username.substring(0, 2);
detailContent.innerHTML = `
<div class="detail-avatar" style="background: ${user.avatarColor};">
${initials}
</div>
<div class="detail-info">
<div class="info-item">
<div class="info-label">用户名</div>
<div class="info-value">${user.username}</div>
</div>
<div class="info-item">
<div class="info-label">邮箱</div>
<div class="info-value">${user.email}</div>
</div>
<div class="info-item">
<div class="info-label">手机号</div>
<div class="info-value">${user.phone}</div>
</div>
<div class="info-item">
<div class="info-label">年龄</div>
<div class="info-value">${user.age} 岁</div>
</div>
<div class="info-item">
<div class="info-label">部门</div>
<div class="info-value">${user.department}</div>
</div>
<div class="info-item">
<div class="info-label">角色</div>
<div class="info-value">
<span style="font-size: 12px; padding: 4px 12px; border-radius: 12px;
background: ${user.role === 'admin' ? 'rgba(239, 68, 68, 0.1)' :
user.role === 'vip' ? 'rgba(245, 158, 11, 0.1)' : 'rgba(59, 130, 246, 0.1)'};
color: ${user.role === 'admin' ? 'var(--danger-color)' :
user.role === 'vip' ? 'var(--warning-color)' : 'var(--primary-color)'};">
${user.role === 'admin' ? '管理员' : user.role === 'vip' ? 'VIP用户' : '普通用户'}
</span>
</div>
</div>
<div class="info-item">
<div class="info-label">状态</div>
<div class="info-value">
<span class="status-badge ${user.status === 'active' ? 'status-active' : 'status-inactive'}">
${user.status === 'active' ? '活跃' : '禁用'}
</span>
</div>
</div>
<div class="info-item">
<div class="info-label">创建时间</div>
<div class="info-value">${user.createTime}</div>
</div>
<div class="info-item">
<div class="info-label">最后登录</div>
<div class="info-value">${user.lastLogin || '从未登录'}</div>
</div>
</div>
<div class="detail-actions">
<button class="btn btn-primary" onclick="userManager.editUser(${user.id})">
<i class="fas fa-edit"></i>
编辑用户
</button>
<button class="btn ${user.status === 'active' ? 'btn-warning' : 'btn-success'}"
onclick="userManager.confirmToggleStatus(${user.id})">
<i class="fas fa-power-off"></i>
${user.status === 'active' ? '禁用用户' : '启用用户'}
</button>
</div>
`;
}
editUser(userId) {
// 在实际应用中,这里会跳转到编辑页面
this.showMessage(`正在编辑用户 ${userId}...`, 'info');
}
confirmDelete(userId) {
const user = this.users.find(u => u.id === userId);
if (user) {
this.selectedUserId = userId;
document.getElementById('deleteMessage').textContent =
`确定要删除用户 "${user.username}" 吗?此操作不可撤销。`;
this.openModal('deleteModal');
}
}
confirmToggleStatus(userId) {
const user = this.users.find(u => u.id === userId);
if (user) {
this.selectedUserId = userId;
const newStatus = user.status === 'active' ? '禁用' : '启用';
document.getElementById('statusModalIcon').innerHTML =
user.status === 'active' ? '<i class="fas fa-ban"></i>' : '<i class="fas fa-check-circle"></i>';
document.getElementById('statusModalTitle').textContent = `${newStatus}用户`;
document.getElementById('statusModalMessage').textContent =
`确定要将用户 "${user.username}" ${newStatus}吗?`;
this.openModal('statusModal');
}
}
deleteUser(userId) {
const userIndex = this.users.findIndex(u => u.id === userId);
if (userIndex !== -1) {
const userName = this.users[userIndex].username;
this.users.splice(userIndex, 1);
this.saveUsers();
if (this.selectedUserId === userId) {
this.selectedUserId = null;
document.getElementById('userDetailContent').innerHTML = `
<div class="empty-state">
<div class="empty-icon">
<i class="fas fa-user"></i>
</div>
<h3 class="empty-title">选择用户查看详情</h3>
<p class="empty-description">点击左侧表格中的用户行,查看详细信息</p>
</div>
`;
}
this.render();
this.showMessage(`用户 "${userName}" 已成功删除`, 'success');
}
}
toggleUserStatus(userId) {
const user = this.users.find(u => u.id === userId);
if (user) {
user.status = user.status === 'active' ? 'inactive' : 'active';
this.saveUsers();
this.render();
const action = user.status === 'active' ? '启用' : '禁用';
this.showMessage(`用户 "${user.username}" 已成功${action}`, 'success');
}
}
exportUsers() {
const data = this.getFilteredUsers();
const csv = this.convertToCSV(data);
this.downloadCSV(csv, 'users_export.csv');
this.showMessage(`已导出 ${data.length} 条用户数据`, 'success');
}
convertToCSV(data) {
const headers = ['ID', '用户名', '邮箱', '年龄', '状态', '角色', '部门', '手机号', '创建时间'];
const rows = data.map(user => [
user.id,
user.username,
user.email,
user.age,
user.status === 'active' ? '活跃' : '禁用',
user.role === 'admin' ? '管理员' : user.role === 'vip' ? 'VIP用户' : '普通用户',
user.department,
user.phone,
user.createTime
]);
return [headers, ...rows].map(row =>
row.map(cell => `"${cell}"`).join(',')
).join('\n');
}
downloadCSV(csv, filename) {
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
showMessage(text, type = 'success') {
const messageContainer = document.getElementById('messageContainer');
const message = document.createElement('div');
message.className = `message message-${type}`;
message.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
<span>${text}</span>
`;
messageContainer.innerHTML = '';
messageContainer.appendChild(message);
// 3秒后自动隐藏
setTimeout(() => {
message.style.opacity = '0';
message.style.transform = 'translateY(-10px)';
setTimeout(() => {
if (message.parentNode) {
message.parentNode.removeChild(message);
}
}, 300);
}, 3000);
}
openModal(modalId) {
document.getElementById(modalId).classList.add('active');
}
closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
}
// 全局函数供HTML调用
function closeModal(modalId) {
userManager.closeModal(modalId);
}
// 初始化应用
let userManager;
document.addEventListener('DOMContentLoaded', () => {
userManager = new UserManager();
// 检查URL参数
const urlParams = new URLSearchParams(window.location.search);
const newUser = urlParams.get('newUser');
if (newUser) {
userManager.showMessage(`用户 "${newUser}" 创建成功!`, 'success');
// 清除URL参数
window.history.replaceState({}, document.title, window.location.pathname);
}
});
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
list-with-layout.html
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title th:text="${pageTitle}">用户管理 | HEP企业管理系统</title>
<!-- 引入图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 引入Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- 引入企业级CSS -->
<link rel="stylesheet" th:href="@{/css/enterprise.css}">
<style>
/* 基础样式重置 - 确保页面无默认边距 */
html, body {
margin: 0;
padding: 0;
width: 100%;
box-sizing: border-box;
}
/* 主内容容器样式 */
.main-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
box-sizing: border-box;
}
/* footer容器样式 - 确保占满页面宽度 */
.footer-wrapper {
width: 100%;
margin: 0 auto;
padding: 0;
box-sizing: border-box;
display: block;
}
/* ========== 整合的Footer样式 - 核心居中逻辑 ========== */
.hep-footer {
width: 100%; /* 占满整个浏览器宽度 */
background: linear-gradient(135deg, #1e3a8a 0%, #1e293b 100%);
color: #f8fafc;
padding: 32px 24px;
margin-top: 48px;
border-top: 4px solid #3b82f6;
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15);
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
box-sizing: border-box;
}
.footer-content {
max-width: 1200px; /* 和主内容宽度一致 */
margin: 0 auto; /* 水平居中 */
width: 100%;
box-sizing: border-box;
}
.footer-main-section {
display: flex;
flex-direction: column;
align-items: center; /* 内部元素水平居中 */
justify-content: center; /* 内部元素垂直居中 */
text-align: center;
width: 100%;
gap: 24px;
}
.page-info-container {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.page-info {
font-size: 14px;
color: #cbd5e1;
padding: 12px 28px;
background: rgba(255, 255, 255, 0.06);
border-radius: 12px;
border: 1px solid rgba(59, 130, 246, 0.25);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.page-info:hover {
background: rgba(255, 255, 255, 0.09);
border-color: rgba(59, 130, 246, 0.4);
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.page-info strong {
color: #3b82f6;
font-weight: 600;
font-size: 15px;
}
.footer-bottom {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
}
/* 版权区域 - 强制居中+美化 */
.copyright-container {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.copyright {
font-size: 15px;
color: #94a3b8;
font-weight: 400;
letter-spacing: 0.4px;
padding: 12px 24px;
background: rgba(255, 255, 255, 0.04);
border-radius: 16px;
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
border: 1px solid rgba(59, 130, 246, 0.15);
}
.copyright-icon {
color: #3b82f6;
font-size: 16px;
font-weight: 600;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
}
.copyright-text {
color: #e2e8f0;
font-weight: 500;
}
.copyright-separator {
color: #64748b;
opacity: 0.6;
margin: 0 4px;
}
.copyright-desc {
font-size: 13px;
color: #94a3b8;
opacity: 0.9;
}
.footer-links {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
width: 100%;
}
.footer-link {
color: #f1f5f9;
text-decoration: none;
font-size: 15px;
padding: 8px 18px;
border-radius: 8px;
transition: all 0.3s ease;
font-weight: 500;
position: relative;
}
.footer-link:hover {
color: #3b82f6;
background: rgba(59, 130, 246, 0.12);
transform: translateY(-2px);
}
.footer-link::after {
content: "";
position: absolute;
bottom: 4px;
left: 50%;
width: 0;
height: 1px;
background: #3b82f6;
transition: width 0.3s ease, left 0.3s ease;
}
.footer-link:hover::after {
width: 80%;
left: 10%;
}
.separator {
color: #64748b;
font-size: 14px;
opacity: 0.7;
}
/* 响应式适配 */
@media (max-width: 768px) {
.hep-footer {
padding: 24px 16px;
}
.footer-main-section {
gap: 20px;
}
.footer-bottom {
gap: 14px;
}
.footer-links {
flex-direction: row;
gap: 12px;
}
.separator {
display: none;
}
.footer-link {
padding: 6px 14px;
font-size: 14px;
}
.page-info {
font-size: 13px;
padding: 10px 20px;
}
.copyright {
font-size: 14px;
padding: 10px 20px;
gap: 6px;
}
.copyright-desc {
font-size: 12px;
}
}
@media (max-width: 480px) {
.hep-footer {
padding: 20px 12px;
}
.footer-links {
flex-direction: column;
gap: 10px;
}
.footer-link {
display: block;
text-align: center;
width: 100%;
max-width: 180px;
}
.page-info {
padding: 8px 16px;
}
.copyright {
flex-direction: column;
gap: 4px;
}
.copyright-separator {
display: none;
}
}
/* ========== 用户详情面板样式 ========== */
.user-detail-panel {
background: white;
border-radius: 16px;
padding: 24px;
box-shadow: var(--card-shadow);
border: 1px solid var(--border-color);
position: sticky;
top: 90px;
animation: slideInRight 0.5s ease;
}
.detail-header {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.detail-header h3 {
font-size: 18px;
font-weight: 600;
color: var(--dark-color);
display: flex;
align-items: center;
gap: 10px;
}
.detail-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-light) 0%, var(--primary-color) 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 36px;
font-weight: 600;
margin: 0 auto 24px;
}
.detail-info {
display: grid;
gap: 16px;
}
.info-item {
display: grid;
grid-template-columns: 100px 1fr;
gap: 12px;
}
.info-label {
font-size: 13px;
color: var(--secondary-color);
font-weight: 500;
}
.info-value {
font-size: 14px;
color: var(--dark-color);
font-weight: 500;
}
.detail-actions {
display: grid;
gap: 12px;
margin-top: 24px;
}
/* ========== 用户操作模态框 ========== */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal {
background: white;
border-radius: 16px;
padding: 32px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
transform: translateY(-20px);
transition: transform 0.3s ease;
}
.modal-overlay.active .modal {
transform: translateY(0);
}
.modal-header {
margin-bottom: 24px;
text-align: center;
}
.modal-icon {
width: 64px;
height: 64px;
border-radius: 50%;
background: rgba(239, 68, 68, 0.1);
display: flex;
align-items: center;
justify-content: center;
color: var(--danger-color);
font-size: 24px;
margin: 0 auto 16px;
}
.modal-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
}
.modal-message {
color: var(--secondary-color);
margin-bottom: 24px;
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: center;
}
</style>
</head>
<body>
<!-- 引用企业级头部碎片 -->
<div th:replace="~{layout/header :: header}"></div>
<!-- 主要内容区域 -->
<main class="main-container">
<div style="display: grid; grid-template-columns: 1fr 380px; gap: 24px;">
<!-- 左侧主内容 -->
<div>
<!-- 页面标题和操作 -->
<div class="card-header" style="margin-bottom: 24px; padding: 0;">
<h1 class="card-title">
<i class="fas fa-users"></i>
用户管理
</h1>
<div class="card-actions">
<button class="btn btn-outline" id="exportBtn">
<i class="fas fa-download"></i>
导出
</button>
<a th:href="@{/user/add}" class="btn btn-primary">
<i class="fas fa-plus"></i>
新增用户
</a>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-title">总用户数</div>
<div class="stat-value" th:text="${totalUsers}">0</div>
</div>
<div class="stat-icon">
<i class="fas fa-users"></i>
</div>
</div>
<div class="stat-trend trend-up">
<i class="fas fa-arrow-up"></i>
<span>较上月增长 12%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-title">活跃用户</div>
<div class="stat-value" th:text="${activeUsers}">0</div>
</div>
<div class="stat-icon">
<i class="fas fa-user-check"></i>
</div>
</div>
<div class="stat-trend trend-up">
<i class="fas fa-arrow-up"></i>
<span>较上月增长 8%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-title">新增用户</div>
<div class="stat-value" id="newUsers">0</div>
</div>
<div class="stat-icon">
<i class="fas fa-user-plus"></i>
</div>
</div>
<div class="stat-trend trend-down">
<i class="fas fa-arrow-down"></i>
<span>较上月减少 3%</span>
</div>
</div>
</div>
<!-- 用户列表卡片 -->
<div class="content-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-list"></i>
用户列表
</h2>
<div style="display: flex; gap: 16px; align-items: center;">
<div class="search-box">
<i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" id="searchInput" placeholder="搜索用户名或邮箱...">
</div>
<select class="filter-select" id="statusFilter">
<option value="all">全部状态</option>
<option value="active">活跃</option>
<option value="inactive">禁用</option>
</select>
<select class="filter-select" id="roleFilter">
<option value="all">所有角色</option>
<option value="admin">管理员</option>
<option value="user">普通用户</option>
<option value="vip">VIP用户</option>
</select>
</div>
</div>
<!-- 消息提示 -->
<div id="messageContainer" style="margin: 0 24px;"></div>
<!-- 表格 -->
<div class="table-container">
<table class="data-table" id="userTable">
<thead>
<tr>
<th style="width: 60px;">ID</th>
<th>用户名</th>
<th>邮箱</th>
<th style="width: 80px;">年龄</th>
<th style="width: 100px;">状态</th>
<th style="width: 150px;">角色</th>
<th style="width: 180px;">创建时间</th>
<th style="width: 150px;">操作</th>
</tr>
</thead>
<tbody id="userTableBody">
<!-- 用户数据将通过JavaScript动态生成 -->
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="pagination">
<div class="page-info" id="pageInfo">
显示 0 条中的 0-0 条
</div>
<div class="page-numbers" id="pageNumbers">
<!-- 页码将通过JavaScript动态生成 -->
</div>
<div style="display: flex; gap: 8px; margin-left: auto;">
<button class="btn btn-outline btn-icon" id="prevPage" disabled>
<i class="fas fa-chevron-left"></i>
</button>
<button class="btn btn-outline btn-icon" id="nextPage" disabled>
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
<!-- 右侧用户详情面板 -->
<div class="user-detail-panel" id="userDetailPanel">
<div class="detail-header">
<h3>
<i class="fas fa-user-circle"></i>
用户详情
</h3>
</div>
<div id="userDetailContent">
<div class="empty-state">
<div class="empty-icon">
<i class="fas fa-user"></i>
</div>
<h3 class="empty-title">选择用户查看详情</h3>
<p class="empty-description">点击左侧表格中的用户行,查看详细信息</p>
</div>
</div>
</div>
</div>
</main>
<!-- 直接嵌入Footer - 不再引用外部文件 -->
<div class="footer-wrapper">
<footer class="hep-footer">
<div class="footer-content">
<div class="footer-main-section">
<div class="page-info-container">
<div class="page-info">
显示 <strong>26</strong> 条中的 <strong>1-10</strong> 条
</div>
</div>
<div class="footer-bottom">
<div class="copyright-container">
<div class="copyright">
<span class="copyright-icon">©</span>
<span class="copyright-text">2026 HEP企业管理系统 V2.0</span>
<span class="copyright-separator">|</span>
<span class="copyright-desc">保留所有权利 · 安全合规 · 高效管理</span>
</div>
</div>
<div class="footer-links">
<a href="#" class="footer-link">技术支持</a>
<span class="separator">|</span>
<a href="#" class="footer-link">联系我们</a>
<span class="separator">|</span>
<a href="#" class="footer-link">隐私政策</a>
</div>
</div>
</div>
</div>
</footer>
</div>
<!-- 删除确认模态框 -->
<div class="modal-overlay" id="deleteModal">
<div class="modal">
<div class="modal-header">
<div class="modal-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h2 class="modal-title">确认删除</h2>
<p class="modal-message" id="deleteMessage">确定要删除此用户吗?此操作不可撤销。</p>
</div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeModal('deleteModal')">
取消
</button>
<button class="btn btn-danger" id="confirmDeleteBtn">
<i class="fas fa-trash"></i>
确认删除
</button>
</div>
</div>
</div>
<!-- 状态切换确认模态框 -->
<div class="modal-overlay" id="statusModal">
<div class="modal">
<div class="modal-header">
<div class="modal-icon" id="statusModalIcon">
<i class="fas fa-power-off"></i>
</div>
<h2 class="modal-title" id="statusModalTitle">切换用户状态</h2>
<p class="modal-message" id="statusModalMessage">确定要切换用户状态吗?</p>
</div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeModal('statusModal')">
取消
</button>
<button class="btn btn-primary" id="confirmStatusBtn">
<i class="fas fa-check"></i>
确认更改
</button>
</div>
</div>
</div>
<script>
// 用户数据管理
class UserManager {
constructor() {
this.users = [];
this.currentPage = 1;
this.itemsPerPage = 10;
this.totalPages = 1;
this.currentFilter = {
status: 'all',
role: 'all',
search: ''
};
this.selectedUserId = null;
this.init();
}
async init() {
await this.loadUsers();
this.setupEventListeners();
this.render();
this.updateStats();
}
async loadUsers() {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
// 从localStorage加载或使用模拟数据
const savedUsers = localStorage.getItem('enterpriseUsers');
if (savedUsers) {
this.users = JSON.parse(savedUsers);
} else {
this.users = this.generateMockData();
this.saveUsers();
}
}
generateMockData() {
const names = ["张明", "李华", "王芳", "刘伟", "陈静", "杨阳", "赵敏", "周强", "吴勇", "郑爽"];
const roles = ["admin", "user", "vip"];
const departments = ["技术部", "市场部", "销售部", "人事部", "财务部"];
return Array.from({ length: 25 }, (_, i) => ({
id: i + 1,
username: names[i % names.length] + (i + 1),
email: `user${i + 1}@hep.cc`,
age: 25 + Math.floor(Math.random() * 20),
status: Math.random() > 0.3 ? 'active' : 'inactive',
role: roles[Math.floor(Math.random() * roles.length)],
department: departments[Math.floor(Math.random() * departments.length)],
phone: `138${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`,
createTime: this.generateRandomDate(),
lastLogin: this.generateRandomDate(),
avatarColor: this.getRandomColor()
}));
}
generateRandomDate() {
const start = new Date(2024, 0, 1);
const end = new Date();
const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
}
getRandomColor() {
const colors = ['#3B82F6', '#10B981', '#8B5CF6', '#F59E0B', '#EF4444'];
return colors[Math.floor(Math.random() * colors.length)];
}
saveUsers() {
localStorage.setItem('enterpriseUsers', JSON.stringify(this.users));
}
getFilteredUsers() {
return this.users.filter(user => {
// 状态过滤
if (this.currentFilter.status !== 'all' && user.status !== this.currentFilter.status) {
return false;
}
// 角色过滤
if (this.currentFilter.role !== 'all' && user.role !== this.currentFilter.role) {
return false;
}
// 搜索过滤
if (this.currentFilter.search) {
const searchLower = this.currentFilter.search.toLowerCase();
return user.username.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower) ||
user.department.toLowerCase().includes(searchLower);
}
return true;
});
}
getPaginatedUsers() {
const filteredUsers = this.getFilteredUsers();
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = Math.min(startIndex + this.itemsPerPage, filteredUsers.length);
return filteredUsers.slice(startIndex, endIndex);
}
setupEventListeners() {
// 搜索
document.getElementById('searchInput').addEventListener('input', (e) => {
this.currentFilter.search = e.target.value;
this.currentPage = 1;
this.render();
});
// 状态过滤
document.getElementById('statusFilter').addEventListener('change', (e) => {
this.currentFilter.status = e.target.value;
this.currentPage = 1;
this.render();
});
// 角色过滤
document.getElementById('roleFilter').addEventListener('change', (e) => {
this.currentFilter.role = e.target.value;
this.currentPage = 1;
this.render();
});
// 分页
document.getElementById('prevPage').addEventListener('click', () => {
if (this.currentPage > 1) {
this.currentPage--;
this.render();
}
});
document.getElementById('nextPage').addEventListener('click', () => {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.render();
}
});
// 导出按钮
document.getElementById('exportBtn').addEventListener('click', () => {
this.exportUsers();
});
// 删除确认
document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
this.deleteUser(this.selectedUserId);
this.closeModal('deleteModal');
});
// 状态切换确认
document.getElementById('confirmStatusBtn').addEventListener('click', () => {
this.toggleUserStatus(this.selectedUserId);
this.closeModal('statusModal');
});
}
render() {
const filteredUsers = this.getFilteredUsers();
this.totalPages = Math.ceil(filteredUsers.length / this.itemsPerPage);
const paginatedUsers = this.getPaginatedUsers();
// 更新表格
this.renderTable(paginatedUsers);
// 更新分页
this.updatePagination(filteredUsers.length);
// 更新详情面板(如果之前选择了用户)
if (this.selectedUserId) {
const user = this.users.find(u => u.id === this.selectedUserId);
if (user) {
this.showUserDetail(user);
}
}
// 更新统计数据
this.updateStats();
}
renderTable(users) {
const tbody = document.getElementById('userTableBody');
tbody.innerHTML = '';
if (users.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8">
<div class="empty-state" style="padding: 32px 0;">
<div class="empty-icon">
<i class="fas fa-search"></i>
</div>
<h3 class="empty-title">未找到用户</h3>
<p class="empty-description">尝试调整搜索条件或创建新用户</p>
</div>
</td>
</tr>
`;
return;
}
users.forEach(user => {
const row = document.createElement('tr');
row.dataset.userId = user.id;
row.className = this.selectedUserId === user.id ? 'selected' : '';
row.innerHTML = `
<td>${user.id}</td>
<td>
<div style="display: flex; align-items: center; gap: 12px;">
<div style="width: 32px; height: 32px; border-radius: 50%; background: ${user.avatarColor};
display: flex; align-items: center; justify-content: center; color: white; font-size: 12px;">
${user.username.substring(0, 2)}
</div>
<div>
<div style="font-weight: 500;">${user.username}</div>
<div style="font-size: 12px; color: var(--secondary-color);">${user.department}</div>
</div>
</div>
</td>
<td>${user.email}</td>
<td>${user.age}</td>
<td>
<span class="status-badge ${user.status === 'active' ? 'status-active' : 'status-inactive'}">
${user.status === 'active' ? '活跃' : '禁用'}
</span>
</td>
<td>
<span style="font-size: 12px; padding: 4px 12px; border-radius: 12px;
background: ${user.role === 'admin' ? 'rgba(239, 68, 68, 0.1)' :
user.role === 'vip' ? 'rgba(245, 158, 11, 0.1)' : 'rgba(59, 130, 246, 0.1)'};
color: ${user.role === 'admin' ? 'var(--danger-color)' :
user.role === 'vip' ? 'var(--warning-color)' : 'var(--primary-color)'};">
${user.role === 'admin' ? '管理员' : user.role === 'vip' ? 'VIP用户' : '普通用户'}
</span>
</td>
<td>${user.createTime}</td>
<td>
<div style="display: flex; gap: 8px;">
<button class="btn btn-outline btn-icon" onclick="userManager.editUser(${user.id})"
title="编辑">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline btn-icon" onclick="userManager.confirmToggleStatus(${user.id})"
title="${user.status === 'active' ? '禁用' : '启用'}">
<i class="fas fa-power-off"></i>
</button>
<button class="btn btn-outline btn-icon" onclick="userManager.confirmDelete(${user.id})"
title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
`;
// 点击行选择用户
row.addEventListener('click', (e) => {
if (!e.target.closest('button')) {
this.selectUser(user.id);
}
});
tbody.appendChild(row);
});
}
updatePagination(totalItems) {
const start = (this.currentPage - 1) * this.itemsPerPage + 1;
const end = Math.min(this.currentPage * this.itemsPerPage, totalItems);
document.getElementById('pageInfo').textContent =
`显示 ${totalItems} 条中的 ${start}-${end} 条`;
// 更新分页按钮状态
document.getElementById('prevPage').disabled = this.currentPage === 1;
document.getElementById('nextPage').disabled = this.currentPage === this.totalPages;
// 更新页码
const pageNumbers = document.getElementById('pageNumbers');
pageNumbers.innerHTML = '';
const maxVisiblePages = 5;
let startPage = Math.max(1, this.currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(this.totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
// 添加页码
for (let i = startPage; i <= endPage; i++) {
const pageBtn = document.createElement('button');
pageBtn.className = `page-number ${i === this.currentPage ? 'active' : ''}`;
pageBtn.textContent = i;
pageBtn.addEventListener('click', () => {
this.currentPage = i;
this.render();
});
pageNumbers.appendChild(pageBtn);
}
}
updateStats() {
const totalUsers = this.users.length;
const activeUsers = this.users.filter(u => u.status === 'active').length;
const newUsers = this.users.filter(u => {
const createDate = new Date(u.createTime.split(' ')[0].replace(/\//g, '-'));
const monthAgo = new Date();
monthAgo.setMonth(monthAgo.getMonth() - 1);
return createDate > monthAgo;
}).length;
document.getElementById('totalUsers').textContent = totalUsers;
document.getElementById('activeUsers').textContent = activeUsers;
document.getElementById('newUsers').textContent = newUsers;
}
selectUser(userId) {
this.selectedUserId = userId;
const user = this.users.find(u => u.id === userId);
if (user) {
this.showUserDetail(user);
// 更新表格行的选中状态
document.querySelectorAll('#userTableBody tr').forEach(row => {
if (parseInt(row.dataset.userId) === userId) {
row.classList.add('selected');
} else {
row.classList.remove('selected');
}
});
}
}
showUserDetail(user) {
const detailContent = document.getElementById('userDetailContent');
const initials = user.username.substring(0, 2);
detailContent.innerHTML = `
<div class="detail-avatar" style="background: ${user.avatarColor};">
${initials}
</div>
<div class="detail-info">
<div class="info-item">
<div class="info-label">用户名</div>
<div class="info-value">${user.username}</div>
</div>
<div class="info-item">
<div class="info-label">邮箱</div>
<div class="info-value">${user.email}</div>
</div>
<div class="info-item">
<div class="info-label">手机号</div>
<div class="info-value">${user.phone}</div>
</div>
<div class="info-item">
<div class="info-label">年龄</div>
<div class="info-value">${user.age} 岁</div>
</div>
<div class="info-item">
<div class="info-label">部门</div>
<div class="info-value">${user.department}</div>
</div>
<div class="info-item">
<div class="info-label">角色</div>
<div class="info-value">
<span style="font-size: 12px; padding: 4px 12px; border-radius: 12px;
background: ${user.role === 'admin' ? 'rgba(239, 68, 68, 0.1)' :
user.role === 'vip' ? 'rgba(245, 158, 11, 0.1)' : 'rgba(59, 130, 246, 0.1)'};
color: ${user.role === 'admin' ? 'var(--danger-color)' :
user.role === 'vip' ? 'var(--warning-color)' : 'var(--primary-color)'};">
${user.role === 'admin' ? '管理员' : user.role === 'vip' ? 'VIP用户' : '普通用户'}
</span>
</div>
</div>
<div class="info-item">
<div class="info-label">状态</div>
<div class="info-value">
<span class="status-badge ${user.status === 'active' ? 'status-active' : 'status-inactive'}">
${user.status === 'active' ? '活跃' : '禁用'}
</span>
</div>
</div>
<div class="info-item">
<div class="info-label">创建时间</div>
<div class="info-value">${user.createTime}</div>
</div>
<div class="info-item">
<div class="info-label">最后登录</div>
<div class="info-value">${user.lastLogin || '从未登录'}</div>
</div>
</div>
<div class="detail-actions">
<button class="btn btn-primary" onclick="userManager.editUser(${user.id})">
<i class="fas fa-edit"></i>
编辑用户
</button>
<button class="btn ${user.status === 'active' ? 'btn-warning' : 'btn-success'}"
onclick="userManager.confirmToggleStatus(${user.id})">
<i class="fas fa-power-off"></i>
${user.status === 'active' ? '禁用用户' : '启用用户'}
</button>
</div>
`;
}
editUser(userId) {
// 在实际应用中,这里会跳转到编辑页面
this.showMessage(`正在编辑用户 ${userId}...`, 'info');
}
confirmDelete(userId) {
const user = this.users.find(u => u.id === userId);
if (user) {
this.selectedUserId = userId;
document.getElementById('deleteMessage').textContent =
`确定要删除用户 "${user.username}" 吗?此操作不可撤销。`;
this.openModal('deleteModal');
}
}
confirmToggleStatus(userId) {
const user = this.users.find(u => u.id === userId);
if (user) {
this.selectedUserId = userId;
const newStatus = user.status === 'active' ? '禁用' : '启用';
document.getElementById('statusModalIcon').innerHTML =
user.status === 'active' ? '<i class="fas fa-ban"></i>' : '<i class="fas fa-check-circle"></i>';
document.getElementById('statusModalTitle').textContent = `${newStatus}用户`;
document.getElementById('statusModalMessage').textContent =
`确定要将用户 "${user.username}" ${newStatus}吗?`;
this.openModal('statusModal');
}
}
deleteUser(userId) {
const userIndex = this.users.findIndex(u => u.id === userId);
if (userIndex !== -1) {
const userName = this.users[userIndex].username;
this.users.splice(userIndex, 1);
this.saveUsers();
if (this.selectedUserId === userId) {
this.selectedUserId = null;
document.getElementById('userDetailContent').innerHTML = `
<div class="empty-state">
<div class="empty-icon">
<i class="fas fa-user"></i>
</div>
<h3 class="empty-title">选择用户查看详情</h3>
<p class="empty-description">点击左侧表格中的用户行,查看详细信息</p>
</div>
`;
}
this.render();
this.showMessage(`用户 "${userName}" 已成功删除`, 'success');
}
}
toggleUserStatus(userId) {
const user = this.users.find(u => u.id === userId);
if (user) {
user.status = user.status === 'active' ? 'inactive' : 'active';
this.saveUsers();
this.render();
const action = user.status === 'active' ? '启用' : '禁用';
this.showMessage(`用户 "${user.username}" 已成功${action}`, 'success');
}
}
exportUsers() {
const data = this.getFilteredUsers();
const csv = this.convertToCSV(data);
this.downloadCSV(csv, 'users_export.csv');
this.showMessage(`已导出 ${data.length} 条用户数据`, 'success');
}
convertToCSV(data) {
const headers = ['ID', '用户名', '邮箱', '年龄', '状态', '角色', '部门', '手机号', '创建时间'];
const rows = data.map(user => [
user.id,
user.username,
user.email,
user.age,
user.status === 'active' ? '活跃' : '禁用',
user.role === 'admin' ? '管理员' : user.role === 'vip' ? 'VIP用户' : '普通用户',
user.department,
user.phone,
user.createTime
]);
return [headers, ...rows].map(row =>
row.map(cell => `"${cell}"`).join(',')
).join('\n');
}
downloadCSV(csv, filename) {
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
showMessage(text, type = 'success') {
const messageContainer = document.getElementById('messageContainer');
const message = document.createElement('div');
message.className = `message message-${type}`;
message.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
<span>${text}</span>
`;
messageContainer.innerHTML = '';
messageContainer.appendChild(message);
// 3秒后自动隐藏
setTimeout(() => {
message.style.opacity = '0';
message.style.transform = 'translateY(-10px)';
setTimeout(() => {
if (message.parentNode) {
message.parentNode.removeChild(message);
}
}, 300);
}, 3000);
}
openModal(modalId) {
document.getElementById(modalId).classList.add('active');
}
closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
}
// 全局函数供HTML调用
function closeModal(modalId) {
userManager.closeModal(modalId);
}
// 初始化应用
let userManager;
document.addEventListener('DOMContentLoaded', () => {
userManager = new UserManager();
// 检查URL参数
const urlParams = new URLSearchParams(window.location.search);
const newUser = urlParams.get('newUser');
if (newUser) {
userManager.showMessage(`用户 "${newUser}" 创建成功!`, 'success');
// 清除URL参数
window.history.replaceState({}, document.title, window.location.pathname);
}
});
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
# 5.3 表单处理与数据绑定
UserAddDTO
package cc.hep.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Min;
import lombok.Data;
/**
* 用户新增DTO(数据传输对象,企业级表单验证)
* 注:DTO仅包含表单需要的字段,避免暴露实体类所有字段
*/
@Data
public class UserAddDTO {
@NotBlank(message = "用户名不能为空") // 非空验证
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确") // 邮箱格式验证
private String email;
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄不能小于18岁") // 最小值验证
private Integer age;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
templates/user/add-new.html
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>新增用户 | HEP企业管理系统</title>
<!-- 引入图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 引入Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- 引入企业级CSS -->
<link rel="stylesheet" th:href="@{/css/enterprise.css}">
<style>
/* 步骤指示器 */
.steps-container {
background: white;
border-radius: 16px;
padding: 32px;
box-shadow: var(--card-shadow);
border: 1px solid var(--border-color);
margin-bottom: 24px;
}
.steps {
display: flex;
justify-content: space-between;
position: relative;
margin-bottom: 24px;
}
.step {
position: relative;
z-index: 2;
text-align: center;
flex: 1;
}
.step-circle {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--light-color);
border: 2px solid var(--border-color);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 12px;
font-weight: 600;
color: var(--secondary-color);
transition: all var(--transition-speed);
}
.step.active .step-circle {
background: var(--primary-color);
border-color: var(--primary-color);
color: white;
transform: scale(1.1);
}
.step.completed .step-circle {
background: var(--success-color);
border-color: var(--success-color);
color: white;
}
.step-title {
font-size: 14px;
font-weight: 500;
color: var(--secondary-color);
margin-bottom: 4px;
}
.step.active .step-title {
color: var(--primary-color);
font-weight: 600;
}
.step-description {
font-size: 12px;
color: var(--secondary-color);
}
.steps-line {
position: absolute;
top: 20px;
left: 50px;
right: 50px;
height: 2px;
background: var(--border-color);
z-index: 1;
}
.steps-progress {
position: absolute;
top: 20px;
left: 50px;
height: 2px;
background: var(--primary-color);
z-index: 1;
transition: width 0.3s ease;
}
/* 表单步骤 */
.form-step {
display: none;
animation: fadeIn 0.5s ease;
}
.form-step.active {
display: block;
}
.step-content {
background: white;
border-radius: 16px;
padding: 32px;
box-shadow: var(--card-shadow);
border: 1px solid var(--border-color);
}
.step-header {
margin-bottom: 32px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.step-header h3 {
font-size: 18px;
font-weight: 600;
color: var(--dark-color);
display: flex;
align-items: center;
gap: 10px;
}
.step-header p {
color: var(--secondary-color);
margin-top: 8px;
}
/* 表单网格布局 */
.form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
}
}
/* 角色选择卡片 */
.role-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin: 24px 0;
}
.role-card {
border: 2px solid var(--border-color);
border-radius: 12px;
padding: 24px;
cursor: pointer;
transition: all var(--transition-speed);
background: white;
text-align: center;
}
.role-card:hover {
border-color: var(--primary-light);
transform: translateY(-2px);
box-shadow: var(--card-shadow);
}
.role-card.selected {
border-color: var(--primary-color);
background: rgba(59, 130, 246, 0.05);
}
.role-icon {
width: 60px;
height: 60px;
border-radius: 12px;
background: rgba(59, 130, 246, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px;
color: var(--primary-color);
font-size: 24px;
}
.role-card.selected .role-icon {
background: var(--primary-color);
color: white;
}
.role-card:nth-child(2) .role-icon {
background: rgba(245, 158, 11, 0.1);
color: var(--warning-color);
}
.role-card:nth-child(2).selected .role-icon {
background: var(--warning-color);
color: white;
}
.role-card:nth-child(3) .role-icon {
background: rgba(239, 68, 68, 0.1);
color: var(--danger-color);
}
.role-card:nth-child(3).selected .role-icon {
background: var(--danger-color);
color: white;
}
.role-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
}
.role-description {
font-size: 14px;
color: var(--secondary-color);
line-height: 1.5;
}
/* 权限选择 */
.permissions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin: 24px 0;
}
.permission-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border: 1px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
transition: all var(--transition-speed);
}
.permission-item:hover {
border-color: var(--primary-light);
background: rgba(59, 130, 246, 0.05);
}
.permission-checkbox {
width: 20px;
height: 20px;
border: 2px solid var(--border-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-speed);
}
.permission-item.selected .permission-checkbox {
background: var(--primary-color);
border-color: var(--primary-color);
}
.permission-item.selected .permission-checkbox::after {
content: '✓';
color: white;
font-size: 12px;
font-weight: bold;
}
.permission-label {
flex: 1;
font-size: 14px;
font-weight: 500;
}
.permission-description {
font-size: 12px;
color: var(--secondary-color);
margin-top: 4px;
}
/* 确认信息 */
.confirmation-info {
background: var(--light-color);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.info-item {
margin-bottom: 16px;
}
.info-label {
font-size: 13px;
color: var(--secondary-color);
margin-bottom: 4px;
}
.info-value {
font-size: 14px;
font-weight: 500;
color: var(--dark-color);
}
/* 表单底部操作 */
.form-actions {
display: flex;
justify-content: space-between;
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid var(--border-color);
}
/* 成功提示 */
.success-message {
text-align: center;
padding: 48px;
}
.success-icon {
width: 80px;
height: 80px;
border-radius: 50%;
background: rgba(16, 185, 129, 0.1);
display: flex;
align-items: center;
justify-content: center;
color: var(--success-color);
font-size: 32px;
margin: 0 auto 24px;
}
/* 加载状态 */
.btn.loading {
position: relative;
color: transparent !important;
}
.btn.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
</style>
</head>
<body>
<!-- 顶部导航栏 -->
<header class="enterprise-header">
<div class="navbar-container">
<div class="logo-section">
<div class="logo-icon">
<i class="fas fa-building"></i>
</div>
<div>
<div class="logo-text">HEP企业管理系统</div>
<div class="logo-subtext">企业级解决方案</div>
</div>
</div>
<nav class="nav-links">
<a href="/" class="nav-link">
<i class="fas fa-home"></i>
首页
</a>
<a href="/user/list" class="nav-link">
<i class="fas fa-users"></i>
用户管理
</a>
<a href="#" class="nav-link">
<i class="fas fa-cog"></i>
系统设置
</a>
</nav>
<div class="user-section">
<div class="notification-badge">
<i class="fas fa-bell" style="font-size: 18px; color: rgba(255, 255, 255, 0.9);"></i>
<span class="badge-count">3</span>
</div>
<div class="user-avatar">AD</div>
</div>
</div>
</header>
<!-- 主要内容区域 -->
<main class="main-container">
<!-- 返回按钮 -->
<div style="margin-bottom: 24px;">
<a href="/user/list" class="btn btn-outline">
<i class="fas fa-arrow-left"></i>
返回用户列表
</a>
</div>
<!-- 步骤指示器 -->
<div class="steps-container">
<div class="steps">
<div class="steps-line"></div>
<div class="steps-progress" id="stepsProgress"></div>
<div class="step active" id="step1">
<div class="step-circle">1</div>
<div class="step-title">基本信息</div>
<div class="step-description">填写用户基本信息</div>
</div>
<div class="step" id="step2">
<div class="step-circle">2</div>
<div class="step-title">权限设置</div>
<div class="step-description">设置角色和权限</div>
</div>
<div class="step" id="step3">
<div class="step-circle">3</div>
<div class="step-title">确认信息</div>
<div class="step-description">确认并提交</div>
</div>
</div>
<div class="step-info">
<h2>新增用户</h2>
<p style="color: var(--secondary-color); margin-top: 8px;">
请按照步骤填写用户信息,所有标有 * 的字段为必填项
</p>
</div>
</div>
<!-- 消息提示 -->
<div id="messageContainer"></div>
<!-- 表单内容 -->
<form id="userForm">
<!-- 步骤1:基本信息 -->
<div class="form-step active" id="formStep1">
<div class="step-content">
<div class="step-header">
<h3><i class="fas fa-user"></i> 基本信息</h3>
<p>填写用户的基本信息,包括姓名、联系方式等</p>
</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label required" for="username">用户名</label>
<input type="text" class="form-control" id="username"
placeholder="请输入用户名" required>
<div class="error-message" id="usernameError">用户名不能为空,至少2个字符</div>
</div>
<div class="form-group">
<label class="form-label required" for="email">邮箱地址</label>
<input type="email" class="form-control" id="email"
placeholder="请输入邮箱地址" required>
<div class="error-message" id="emailError">请输入有效的邮箱地址</div>
</div>
<div class="form-group">
<label class="form-label required" for="phone">手机号</label>
<input type="tel" class="form-control" id="phone"
placeholder="请输入手机号" required>
<div class="error-message" id="phoneError">请输入有效的手机号</div>
</div>
<div class="form-group">
<label class="form-label" for="age">年龄</label>
<input type="number" class="form-control" id="age"
min="18" max="100" placeholder="请输入年龄">
<div class="error-message" id="ageError">年龄必须在18-100之间</div>
</div>
<div class="form-group">
<label class="form-label required" for="department">部门</label>
<select class="form-control" id="department" required>
<option value="">请选择部门</option>
<option value="技术部">技术部</option>
<option value="市场部">市场部</option>
<option value="销售部">销售部</option>
<option value="人事部">人事部</option>
<option value="财务部">财务部</option>
<option value="研发部">研发部</option>
</select>
<div class="error-message" id="departmentError">请选择部门</div>
</div>
<div class="form-group">
<label class="form-label" for="position">职位</label>
<input type="text" class="form-control" id="position"
placeholder="请输入职位">
</div>
</div>
<div class="form-actions">
<div></div> <!-- 占位 -->
<button type="button" class="btn btn-primary" onclick="nextStep(2)">
下一步 <i class="fas fa-arrow-right"></i>
</button>
</div>
</div>
</div>
<!-- 步骤2:权限设置 -->
<div class="form-step" id="formStep2">
<div class="step-content">
<div class="step-header">
<h3><i class="fas fa-lock"></i> 权限设置</h3>
<p>为用户分配角色和系统权限</p>
</div>
<div class="form-group">
<label class="form-label required">用户角色</label>
<div class="role-grid" id="roleSelection">
<div class="role-card" data-role="user" onclick="selectRole('user')">
<div class="role-icon">
<i class="fas fa-user"></i>
</div>
<div class="role-title">普通用户</div>
<div class="role-description">
基本的查看和操作权限,适用于大多数员工
</div>
</div>
<div class="role-card" data-role="vip" onclick="selectRole('vip')">
<div class="role-icon">
<i class="fas fa-crown"></i>
</div>
<div class="role-title">VIP用户</div>
<div class="role-description">
高级功能访问权限,适用于部门主管
</div>
</div>
<div class="role-card" data-role="admin" onclick="selectRole('admin')">
<div class="role-icon">
<i class="fas fa-shield-alt"></i>
</div>
<div class="role-title">管理员</div>
<div class="role-description">
系统管理权限,适用于IT管理人员
</div>
</div>
</div>
<input type="hidden" id="selectedRole" value="user">
<div class="error-message" id="roleError">请选择用户角色</div>
</div>
<div class="form-group">
<label class="form-label">系统权限</label>
<div class="permissions-grid" id="permissionsSelection">
<div class="permission-item" data-permission="read" onclick="togglePermission('read')">
<div class="permission-checkbox"></div>
<div>
<div class="permission-label">数据查看</div>
<div class="permission-description">查看系统数据和报表</div>
</div>
</div>
<div class="permission-item" data-permission="write" onclick="togglePermission('write')">
<div class="permission-checkbox"></div>
<div>
<div class="permission-label">数据编辑</div>
<div class="permission-description">编辑和更新数据</div>
</div>
</div>
<div class="permission-item" data-permission="delete" onclick="togglePermission('delete')">
<div class="permission-checkbox"></div>
<div>
<div class="permission-label">数据删除</div>
<div class="permission-description">删除数据和记录</div>
</div>
</div>
<div class="permission-item" data-permission="export" onclick="togglePermission('export')">
<div class="permission-checkbox"></div>
<div>
<div class="permission-label">数据导出</div>
<div class="permission-description">导出数据为文件</div>
</div>
</div>
<div class="permission-item" data-permission="manage" onclick="togglePermission('manage')">
<div class="permission-checkbox"></div>
<div>
<div class="permission-label">用户管理</div>
<div class="permission-description">管理系统用户</div>
</div>
</div>
<div class="permission-item" data-permission="config" onclick="togglePermission('config')">
<div class="permission-checkbox"></div>
<div>
<div class="permission-label">系统配置</div>
<div class="permission-description">修改系统设置</div>
</div>
</div>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-outline" onclick="prevStep(1)">
<i class="fas fa-arrow-left"></i> 上一步
</button>
<button type="button" class="btn btn-primary" onclick="nextStep(3)">
下一步 <i class="fas fa-arrow-right"></i>
</button>
</div>
</div>
</div>
<!-- 步骤3:确认信息 -->
<div class="form-step" id="formStep3">
<div class="step-content">
<div class="step-header">
<h3><i class="fas fa-check-circle"></i> 确认信息</h3>
<p>请确认填写的信息,确认无误后提交</p>
</div>
<div class="confirmation-info">
<div class="info-grid">
<div class="info-item">
<div class="info-label">用户名</div>
<div class="info-value" id="confirmUsername">--</div>
</div>
<div class="info-item">
<div class="info-label">邮箱</div>
<div class="info-value" id="confirmEmail">--</div>
</div>
<div class="info-item">
<div class="info-label">手机号</div>
<div class="info-value" id="confirmPhone">--</div>
</div>
<div class="info-item">
<div class="info-label">年龄</div>
<div class="info-value" id="confirmAge">--</div>
</div>
<div class="info-item">
<div class="info-label">部门</div>
<div class="info-value" id="confirmDepartment">--</div>
</div>
<div class="info-item">
<div class="info-label">职位</div>
<div class="info-value" id="confirmPosition">--</div>
</div>
<div class="info-item">
<div class="info-label">角色</div>
<div class="info-value" id="confirmRole">--</div>
</div>
<div class="info-item">
<div class="info-label">权限</div>
<div class="info-value" id="confirmPermissions">--</div>
</div>
</div>
</div>
<div class="message message-info">
<i class="fas fa-info-circle"></i>
<span>请仔细核对以上信息,确认无误后点击"提交"按钮创建用户。</span>
</div>
<div class="form-actions">
<button type="button" class="btn btn-outline" onclick="prevStep(2)">
<i class="fas fa-arrow-left"></i> 上一步
</button>
<button type="button" class="btn btn-success" id="submitBtn" onclick="submitForm()">
<i class="fas fa-check"></i> 提交创建
</button>
</div>
</div>
</div>
<!-- 步骤4:创建成功 -->
<div class="form-step" id="formStep4">
<div class="step-content">
<div class="success-message">
<div class="success-icon">
<i class="fas fa-check"></i>
</div>
<h2 style="margin-bottom: 8px;">用户创建成功!</h2>
<p style="color: var(--secondary-color); margin-bottom: 32px;">
新用户已成功添加到系统,您可以选择以下操作:
</p>
<div style="display: flex; gap: 12px; justify-content: center;">
<a href="/user/list" class="btn btn-primary">
<i class="fas fa-users"></i> 查看用户列表
</a>
<button type="button" class="btn btn-outline" onclick="resetForm()">
<i class="fas fa-plus"></i> 继续新增
</button>
</div>
</div>
</div>
</div>
</form>
</main>
<script>
class UserFormManager {
constructor() {
this.currentStep = 1;
this.formData = {
username: '',
email: '',
phone: '',
age: '',
department: '',
position: '',
role: 'user',
permissions: ['read']
};
this.init();
}
init() {
this.updateStepIndicator();
this.setupEventListeners();
this.loadDraft();
}
setupEventListeners() {
// 表单输入监听
document.getElementById('username').addEventListener('input', (e) => {
this.formData.username = e.target.value;
this.validateField('username');
this.saveDraft();
});
document.getElementById('email').addEventListener('input', (e) => {
this.formData.email = e.target.value;
this.validateField('email');
this.saveDraft();
});
document.getElementById('phone').addEventListener('input', (e) => {
this.formData.phone = e.target.value;
this.validateField('phone');
this.saveDraft();
});
document.getElementById('age').addEventListener('input', (e) => {
this.formData.age = e.target.value;
this.validateField('age');
this.saveDraft();
});
document.getElementById('department').addEventListener('change', (e) => {
this.formData.department = e.target.value;
this.validateField('department');
this.saveDraft();
});
document.getElementById('position').addEventListener('input', (e) => {
this.formData.position = e.target.value;
this.saveDraft();
});
// 默认选择普通用户角色
this.selectRole('user');
}
saveDraft() {
localStorage.setItem('userFormDraft', JSON.stringify(this.formData));
}
loadDraft() {
const draft = localStorage.getItem('userFormDraft');
if (draft) {
try {
this.formData = JSON.parse(draft);
// 填充表单字段
document.getElementById('username').value = this.formData.username || '';
document.getElementById('email').value = this.formData.email || '';
document.getElementById('phone').value = this.formData.phone || '';
document.getElementById('age').value = this.formData.age || '';
document.getElementById('department').value = this.formData.department || '';
document.getElementById('position').value = this.formData.position || '';
// 恢复角色选择
this.selectRole(this.formData.role);
// 恢复权限选择
this.formData.permissions.forEach(permission => {
this.togglePermission(permission, false);
});
this.showMessage('已恢复上次保存的草稿', 'info');
} catch (e) {
console.error('加载草稿失败:', e);
}
}
}
clearDraft() {
localStorage.removeItem('userFormDraft');
}
nextStep(step) {
if (!this.validateCurrentStep()) {
return;
}
// 隐藏当前步骤
document.getElementById(`formStep${this.currentStep}`).classList.remove('active');
// 更新当前步骤
this.currentStep = step;
// 显示下一步
document.getElementById(`formStep${this.currentStep}`).classList.add('active');
// 更新步骤指示器
this.updateStepIndicator();
// 如果是确认步骤,更新确认信息
if (this.currentStep === 3) {
this.updateConfirmationInfo();
}
}
prevStep(step) {
// 隐藏当前步骤
document.getElementById(`formStep${this.currentStep}`).classList.remove('active');
// 更新当前步骤
this.currentStep = step;
// 显示上一步
document.getElementById(`formStep${this.currentStep}`).classList.add('active');
// 更新步骤指示器
this.updateStepIndicator();
}
updateStepIndicator() {
// 重置所有步骤
document.querySelectorAll('.step').forEach(step => {
step.classList.remove('active', 'completed');
});
// 更新步骤进度条
const progress = document.getElementById('stepsProgress');
const progressWidth = ((this.currentStep - 1) / 2) * 100;
progress.style.width = `${progressWidth}%`;
// 更新步骤状态
for (let i = 1; i <= 3; i++) {
const stepElement = document.getElementById(`step${i}`);
if (i < this.currentStep) {
stepElement.classList.add('completed');
} else if (i === this.currentStep) {
stepElement.classList.add('active');
}
}
}
validateCurrentStep() {
switch (this.currentStep) {
case 1:
return this.validateStep1();
case 2:
return this.validateStep2();
default:
return true;
}
}
validateStep1() {
let isValid = true;
// 验证用户名
if (!this.validateField('username')) isValid = false;
// 验证邮箱
if (!this.validateField('email')) isValid = false;
// 验证手机号
if (!this.validateField('phone')) isValid = false;
// 验证年龄(如果填写)
if (this.formData.age && !this.validateField('age')) isValid = false;
// 验证部门
if (!this.validateField('department')) isValid = false;
return isValid;
}
validateStep2() {
// 步骤2没有必填项,总是返回true
return true;
}
validateField(field) {
const value = this.formData[field];
let isValid = true;
let errorMessage = '';
switch (field) {
case 'username':
if (!value || value.trim().length < 2) {
isValid = false;
errorMessage = '用户名不能为空,至少2个字符';
}
break;
case 'email':
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!value || !emailRegex.test(value)) {
isValid = false;
errorMessage = '请输入有效的邮箱地址';
}
break;
case 'phone':
const phoneRegex = /^1[3-9]\d{9}$/;
if (!value || !phoneRegex.test(value)) {
isValid = false;
errorMessage = '请输入有效的手机号';
}
break;
case 'age':
if (value) {
const ageNum = parseInt(value);
if (isNaN(ageNum) || ageNum < 18 || ageNum > 100) {
isValid = false;
errorMessage = '年龄必须在18-100之间';
}
}
break;
case 'department':
if (!value) {
isValid = false;
errorMessage = '请选择部门';
}
break;
}
// 更新UI状态
const input = document.getElementById(field);
const error = document.getElementById(`${field}Error`);
if (input) {
if (!isValid) {
input.classList.add('error');
if (error) error.style.display = 'block';
} else {
input.classList.remove('error');
if (error) error.style.display = 'none';
}
}
return isValid;
}
selectRole(role) {
this.formData.role = role;
document.getElementById('selectedRole').value = role;
// 更新UI
document.querySelectorAll('.role-card').forEach(card => {
card.classList.remove('selected');
if (card.dataset.role === role) {
card.classList.add('selected');
}
});
// 根据角色自动设置权限
this.setDefaultPermissions(role);
this.saveDraft();
}
setDefaultPermissions(role) {
// 清空当前权限
this.formData.permissions = ['read'];
// 根据角色设置默认权限
switch (role) {
case 'user':
this.formData.permissions = ['read'];
break;
case 'vip':
this.formData.permissions = ['read', 'write', 'export'];
break;
case 'admin':
this.formData.permissions = ['read', 'write', 'delete', 'export', 'manage', 'config'];
break;
}
// 更新UI
document.querySelectorAll('.permission-item').forEach(item => {
const permission = item.dataset.permission;
if (this.formData.permissions.includes(permission)) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
});
}
togglePermission(permission, save = true) {
const index = this.formData.permissions.indexOf(permission);
if (index === -1) {
this.formData.permissions.push(permission);
} else {
this.formData.permissions.splice(index, 1);
}
// 更新UI
const item = document.querySelector(`.permission-item[data-permission="${permission}"]`);
if (item) {
item.classList.toggle('selected');
}
if (save) {
this.saveDraft();
}
}
updateConfirmationInfo() {
// 更新确认信息显示
document.getElementById('confirmUsername').textContent = this.formData.username || '--';
document.getElementById('confirmEmail').textContent = this.formData.email || '--';
document.getElementById('confirmPhone').textContent = this.formData.phone || '--';
document.getElementById('confirmAge').textContent = this.formData.age ? `${this.formData.age} 岁` : '--';
document.getElementById('confirmDepartment').textContent = this.formData.department || '--';
document.getElementById('confirmPosition').textContent = this.formData.position || '--';
// 角色显示
const roleNames = {
'user': '普通用户',
'vip': 'VIP用户',
'admin': '管理员'
};
document.getElementById('confirmRole').textContent = roleNames[this.formData.role] || '--';
// 权限显示
const permissionNames = {
'read': '数据查看',
'write': '数据编辑',
'delete': '数据删除',
'export': '数据导出',
'manage': '用户管理',
'config': '系统配置'
};
const permissionText = this.formData.permissions
.map(p => permissionNames[p])
.filter(p => p)
.join(', ');
document.getElementById('confirmPermissions').textContent = permissionText || '--';
}
async submitForm() {
// 验证所有步骤
if (!this.validateStep1() || !this.validateStep2()) {
this.showMessage('请先完善所有必填信息', 'error');
return;
}
const submitBtn = document.getElementById('submitBtn');
const originalText = submitBtn.innerHTML;
// 显示加载状态
submitBtn.classList.add('loading');
submitBtn.disabled = true;
try {
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 1500));
// 获取现有用户列表
let users = JSON.parse(localStorage.getItem('enterpriseUsers')) || [];
// 生成新用户ID
const newId = users.length > 0 ? Math.max(...users.map(u => u.id)) + 1 : 1;
// 创建新用户对象
const newUser = {
id: newId,
username: this.formData.username,
email: this.formData.email,
phone: this.formData.phone,
age: this.formData.age ? parseInt(this.formData.age) : 25,
status: 'active',
role: this.formData.role,
department: this.formData.department,
position: this.formData.position || '',
createTime: new Date().toLocaleDateString('zh-CN') + ' ' +
new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
}),
lastLogin: '',
avatarColor: this.getRandomColor(),
permissions: this.formData.permissions
};
// 添加到用户列表
users.push(newUser);
// 保存到localStorage
localStorage.setItem('enterpriseUsers', JSON.stringify(users));
// 清除草稿
this.clearDraft();
// 进入成功步骤
this.currentStep = 4;
document.getElementById(`formStep3`).classList.remove('active');
document.getElementById(`formStep4`).classList.add('active');
this.updateStepIndicator();
this.showMessage(`用户 "${newUser.username}" 创建成功!`, 'success');
} catch (error) {
console.error('提交失败:', error);
this.showMessage('提交失败,请稍后重试', 'error');
} finally {
// 恢复按钮状态
submitBtn.classList.remove('loading');
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
}
getRandomColor() {
const colors = ['#3B82F6', '#10B981', '#8B5CF6', '#F59E0B', '#EF4444'];
return colors[Math.floor(Math.random() * colors.length)];
}
resetForm() {
// 重置表单数据
this.formData = {
username: '',
email: '',
phone: '',
age: '',
department: '',
position: '',
role: 'user',
permissions: ['read']
};
// 重置表单字段
document.getElementById('username').value = '';
document.getElementById('email').value = '';
document.getElementById('phone').value = '';
document.getElementById('age').value = '';
document.getElementById('department').value = '';
document.getElementById('position').value = '';
// 重置角色选择
this.selectRole('user');
// 重置步骤
document.getElementById(`formStep4`).classList.remove('active');
document.getElementById(`formStep1`).classList.add('active');
this.currentStep = 1;
this.updateStepIndicator();
// 清除草稿
this.clearDraft();
this.showMessage('表单已重置,可以开始创建新用户', 'info');
}
showMessage(text, type = 'success') {
const messageContainer = document.getElementById('messageContainer');
const message = document.createElement('div');
message.className = `message message-${type}`;
message.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
<span>${text}</span>
`;
messageContainer.innerHTML = '';
messageContainer.appendChild(message);
// 3秒后自动隐藏
setTimeout(() => {
message.style.opacity = '0';
message.style.transform = 'translateY(-10px)';
setTimeout(() => {
if (message.parentNode) {
message.parentNode.removeChild(message);
}
}, 300);
}, 3000);
}
}
// 全局函数供HTML调用
let formManager;
function nextStep(step) {
formManager.nextStep(step);
}
function prevStep(step) {
formManager.prevStep(step);
}
function selectRole(role) {
formManager.selectRole(role);
}
function togglePermission(permission) {
formManager.togglePermission(permission);
}
function submitForm() {
formManager.submitForm();
}
function resetForm() {
formManager.resetForm();
}
// 初始化表单管理器
document.addEventListener('DOMContentLoaded', () => {
formManager = new UserFormManager();
});
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
# 5.4 模板布局与碎片(企业级通用布局)
# 5.4.1 创建通用头部碎片
templates/layout/header.html
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Header</title>
</head>
<body>
<!-- 头部片段定义(企业级通用头部) -->
<header class="enterprise-header" th:fragment="header">
<div class="navbar-container">
<div class="logo-section">
<div class="logo-icon">
<i class="fas fa-building"></i>
</div>
<div>
<div class="logo-text">HEP企业管理系统</div>
<div class="logo-subtext">企业级解决方案</div>
</div>
</div>
<nav class="nav-links">
<a href="/" class="nav-link">
<i class="fas fa-home"></i>
首页
</a>
<a href="/user/list" class="nav-link active">
<i class="fas fa-users"></i>
用户管理
</a>
<a href="#" class="nav-link">
<i class="fas fa-cog"></i>
系统设置
</a>
<a href="#" class="nav-link">
<i class="fas fa-chart-bar"></i>
数据报表
</a>
</nav>
<div class="user-section">
<div class="notification-badge">
<i class="fas fa-bell" style="font-size: 18px; color: rgba(255, 255, 255, 0.9);"></i>
<span class="badge-count">3</span>
</div>
<div class="user-avatar" id="userAvatar">AD</div>
</div>
</div>
</header>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 5.4.2 创建通用底部碎片
templates/layout/footer.html
<!-- footer.html 完整代码 -->
<footer class="hep-footer">
<div class="footer-content">
<div class="footer-main-section">
<div class="page-info-container">
<div class="page-info">
显示 <strong>26</strong> 条中的 <strong>1-10</strong> 条
</div>
</div>
<div class="footer-bottom">
<div class="copyright-container">
<div class="copyright">
<span class="copyright-icon">©</span>
<span class="copyright-text">2026 HEP企业管理系统 V2.0</span>
<span class="copyright-separator">|</span>
<span class="copyright-desc">保留所有权利 · 安全合规 · 高效管理</span>
</div>
</div>
<div class="footer-links">
<a href="#" class="footer-link">技术支持</a>
<span class="separator">|</span>
<a href="#" class="footer-link">联系我们</a>
<span class="separator">|</span>
<a href="#" class="footer-link">隐私政策</a>
</div>
</div>
</div>
</div>
</footer>
<style>
/* 确保footer占满页面宽度 */
.hep-footer {
width: 100%; /* 关键:占满整个浏览器宽度 */
background: linear-gradient(135deg, #1e3a8a 0%, #1e293b 100%);
color: #f8fafc;
padding: 32px 24px;
margin-top: 48px;
border-top: 4px solid #3b82f6;
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15);
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
box-sizing: border-box;
}
/* 让footer内容和主页面内容宽度一致并居中 */
.footer-content {
max-width: 1200px; /* 和主页面.main-container宽度保持一致 */
margin: 0 auto; /* 水平居中 */
width: 100%;
box-sizing: border-box;
}
.footer-main-section {
display: flex;
flex-direction: column;
align-items: center; /* 内部元素水平居中 */
justify-content: center; /* 内部元素垂直居中 */
text-align: center;
width: 100%;
gap: 24px;
}
.page-info-container {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.page-info {
font-size: 14px;
color: #cbd5e1;
padding: 12px 28px;
background: rgba(255, 255, 255, 0.06);
border-radius: 12px;
border: 1px solid rgba(59, 130, 246, 0.25);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
/* 版权区域强制居中 */
.copyright-container {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.copyright {
font-size: 15px;
color: #94a3b8;
font-weight: 400;
letter-spacing: 0.4px;
padding: 12px 24px;
background: rgba(255, 255, 255, 0.04);
border-radius: 16px;
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
border: 1px solid rgba(59, 130, 246, 0.15);
}
.copyright-icon {
color: #3b82f6;
font-size: 16px;
font-weight: 600;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
}
.copyright-text {
color: #e2e8f0;
font-weight: 500;
}
.copyright-separator {
color: #64748b;
opacity: 0.6;
margin: 0 4px;
}
.copyright-desc {
font-size: 13px;
color: #94a3b8;
opacity: 0.9;
}
.footer-links {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
width: 100%;
}
.footer-link {
color: #f1f5f9;
text-decoration: none;
font-size: 15px;
padding: 8px 18px;
border-radius: 8px;
transition: all 0.3s ease;
font-weight: 500;
position: relative;
}
.footer-link:hover {
color: #3b82f6;
background: rgba(59, 130, 246, 0.12);
transform: translateY(-2px);
}
.footer-link::after {
content: "";
position: absolute;
bottom: 4px;
left: 50%;
width: 0;
height: 1px;
background: #3b82f6;
transition: width 0.3s ease, left 0.3s ease;
}
.footer-link:hover::after {
width: 80%;
left: 10%;
}
.separator {
color: #64748b;
font-size: 14px;
opacity: 0.7;
}
/* 响应式适配 */
@media (max-width: 768px) {
.hep-footer {
padding: 24px 16px;
}
.copyright {
font-size: 14px;
padding: 10px 20px;
gap: 6px;
}
.copyright-desc {
font-size: 12px;
}
}
@media (max-width: 480px) {
.copyright {
flex-direction: column;
gap: 4px;
}
.copyright-separator {
display: none;
}
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# 5.4.3 使用碎片(示例)
新增 templates/user/list-with-layout.html,演示如何使用布局碎片:
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title th:text="${pageTitle}">用户管理 | HEP企业管理系统</title>
<!-- 引入图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 引入Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- 引入企业级CSS -->
<link rel="stylesheet" th:href="@{/css/enterprise.css}">
<style>
/* 基础样式重置 - 确保页面无默认边距 */
html, body {
margin: 0;
padding: 0;
width: 100%;
box-sizing: border-box;
}
/* 主内容容器样式 */
.main-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
box-sizing: border-box;
}
/* footer容器样式 - 确保占满页面宽度 */
.footer-wrapper {
width: 100%;
margin: 0 auto;
padding: 0;
box-sizing: border-box;
display: block;
}
/* ========== 整合的Footer样式 - 核心居中逻辑 ========== */
.hep-footer {
width: 100%; /* 占满整个浏览器宽度 */
background: linear-gradient(135deg, #1e3a8a 0%, #1e293b 100%);
color: #f8fafc;
padding: 32px 24px;
margin-top: 48px;
border-top: 4px solid #3b82f6;
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15);
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
box-sizing: border-box;
}
.footer-content {
max-width: 1200px; /* 和主内容宽度一致 */
margin: 0 auto; /* 水平居中 */
width: 100%;
box-sizing: border-box;
}
.footer-main-section {
display: flex;
flex-direction: column;
align-items: center; /* 内部元素水平居中 */
justify-content: center; /* 内部元素垂直居中 */
text-align: center;
width: 100%;
gap: 24px;
}
.page-info-container {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.page-info {
font-size: 14px;
color: #cbd5e1;
padding: 12px 28px;
background: rgba(255, 255, 255, 0.06);
border-radius: 12px;
border: 1px solid rgba(59, 130, 246, 0.25);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.page-info:hover {
background: rgba(255, 255, 255, 0.09);
border-color: rgba(59, 130, 246, 0.4);
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.page-info strong {
color: #3b82f6;
font-weight: 600;
font-size: 15px;
}
.footer-bottom {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
}
/* 版权区域 - 强制居中+美化 */
.copyright-container {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.copyright {
font-size: 15px;
color: #94a3b8;
font-weight: 400;
letter-spacing: 0.4px;
padding: 12px 24px;
background: rgba(255, 255, 255, 0.04);
border-radius: 16px;
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
border: 1px solid rgba(59, 130, 246, 0.15);
}
.copyright-icon {
color: #3b82f6;
font-size: 16px;
font-weight: 600;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
}
.copyright-text {
color: #e2e8f0;
font-weight: 500;
}
.copyright-separator {
color: #64748b;
opacity: 0.6;
margin: 0 4px;
}
.copyright-desc {
font-size: 13px;
color: #94a3b8;
opacity: 0.9;
}
.footer-links {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
width: 100%;
}
.footer-link {
color: #f1f5f9;
text-decoration: none;
font-size: 15px;
padding: 8px 18px;
border-radius: 8px;
transition: all 0.3s ease;
font-weight: 500;
position: relative;
}
.footer-link:hover {
color: #3b82f6;
background: rgba(59, 130, 246, 0.12);
transform: translateY(-2px);
}
.footer-link::after {
content: "";
position: absolute;
bottom: 4px;
left: 50%;
width: 0;
height: 1px;
background: #3b82f6;
transition: width 0.3s ease, left 0.3s ease;
}
.footer-link:hover::after {
width: 80%;
left: 10%;
}
.separator {
color: #64748b;
font-size: 14px;
opacity: 0.7;
}
/* 响应式适配 */
@media (max-width: 768px) {
.hep-footer {
padding: 24px 16px;
}
.footer-main-section {
gap: 20px;
}
.footer-bottom {
gap: 14px;
}
.footer-links {
flex-direction: row;
gap: 12px;
}
.separator {
display: none;
}
.footer-link {
padding: 6px 14px;
font-size: 14px;
}
.page-info {
font-size: 13px;
padding: 10px 20px;
}
.copyright {
font-size: 14px;
padding: 10px 20px;
gap: 6px;
}
.copyright-desc {
font-size: 12px;
}
}
@media (max-width: 480px) {
.hep-footer {
padding: 20px 12px;
}
.footer-links {
flex-direction: column;
gap: 10px;
}
.footer-link {
display: block;
text-align: center;
width: 100%;
max-width: 180px;
}
.page-info {
padding: 8px 16px;
}
.copyright {
flex-direction: column;
gap: 4px;
}
.copyright-separator {
display: none;
}
}
/* ========== 用户详情面板样式 ========== */
.user-detail-panel {
background: white;
border-radius: 16px;
padding: 24px;
box-shadow: var(--card-shadow);
border: 1px solid var(--border-color);
position: sticky;
top: 90px;
animation: slideInRight 0.5s ease;
}
.detail-header {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.detail-header h3 {
font-size: 18px;
font-weight: 600;
color: var(--dark-color);
display: flex;
align-items: center;
gap: 10px;
}
.detail-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-light) 0%, var(--primary-color) 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 36px;
font-weight: 600;
margin: 0 auto 24px;
}
.detail-info {
display: grid;
gap: 16px;
}
.info-item {
display: grid;
grid-template-columns: 100px 1fr;
gap: 12px;
}
.info-label {
font-size: 13px;
color: var(--secondary-color);
font-weight: 500;
}
.info-value {
font-size: 14px;
color: var(--dark-color);
font-weight: 500;
}
.detail-actions {
display: grid;
gap: 12px;
margin-top: 24px;
}
/* ========== 用户操作模态框 ========== */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal {
background: white;
border-radius: 16px;
padding: 32px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
transform: translateY(-20px);
transition: transform 0.3s ease;
}
.modal-overlay.active .modal {
transform: translateY(0);
}
.modal-header {
margin-bottom: 24px;
text-align: center;
}
.modal-icon {
width: 64px;
height: 64px;
border-radius: 50%;
background: rgba(239, 68, 68, 0.1);
display: flex;
align-items: center;
justify-content: center;
color: var(--danger-color);
font-size: 24px;
margin: 0 auto 16px;
}
.modal-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
}
.modal-message {
color: var(--secondary-color);
margin-bottom: 24px;
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: center;
}
</style>
</head>
<body>
<!-- 引用企业级头部碎片 -->
<div th:replace="~{layout/header :: header}"></div>
<!-- 主要内容区域 -->
<main class="main-container">
<div style="display: grid; grid-template-columns: 1fr 380px; gap: 24px;">
<!-- 左侧主内容 -->
<div>
<!-- 页面标题和操作 -->
<div class="card-header" style="margin-bottom: 24px; padding: 0;">
<h1 class="card-title">
<i class="fas fa-users"></i>
用户管理
</h1>
<div class="card-actions">
<button class="btn btn-outline" id="exportBtn">
<i class="fas fa-download"></i>
导出
</button>
<a th:href="@{/user/add}" class="btn btn-primary">
<i class="fas fa-plus"></i>
新增用户
</a>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-title">总用户数</div>
<div class="stat-value" th:text="${totalUsers}">0</div>
</div>
<div class="stat-icon">
<i class="fas fa-users"></i>
</div>
</div>
<div class="stat-trend trend-up">
<i class="fas fa-arrow-up"></i>
<span>较上月增长 12%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-title">活跃用户</div>
<div class="stat-value" th:text="${activeUsers}">0</div>
</div>
<div class="stat-icon">
<i class="fas fa-user-check"></i>
</div>
</div>
<div class="stat-trend trend-up">
<i class="fas fa-arrow-up"></i>
<span>较上月增长 8%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-title">新增用户</div>
<div class="stat-value" id="newUsers">0</div>
</div>
<div class="stat-icon">
<i class="fas fa-user-plus"></i>
</div>
</div>
<div class="stat-trend trend-down">
<i class="fas fa-arrow-down"></i>
<span>较上月减少 3%</span>
</div>
</div>
</div>
<!-- 用户列表卡片 -->
<div class="content-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-list"></i>
用户列表
</h2>
<div style="display: flex; gap: 16px; align-items: center;">
<div class="search-box">
<i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" id="searchInput" placeholder="搜索用户名或邮箱...">
</div>
<select class="filter-select" id="statusFilter">
<option value="all">全部状态</option>
<option value="active">活跃</option>
<option value="inactive">禁用</option>
</select>
<select class="filter-select" id="roleFilter">
<option value="all">所有角色</option>
<option value="admin">管理员</option>
<option value="user">普通用户</option>
<option value="vip">VIP用户</option>
</select>
</div>
</div>
<!-- 消息提示 -->
<div id="messageContainer" style="margin: 0 24px;"></div>
<!-- 表格 -->
<div class="table-container">
<table class="data-table" id="userTable">
<thead>
<tr>
<th style="width: 60px;">ID</th>
<th>用户名</th>
<th>邮箱</th>
<th style="width: 80px;">年龄</th>
<th style="width: 100px;">状态</th>
<th style="width: 150px;">角色</th>
<th style="width: 180px;">创建时间</th>
<th style="width: 150px;">操作</th>
</tr>
</thead>
<tbody id="userTableBody">
<!-- 用户数据将通过JavaScript动态生成 -->
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="pagination">
<div class="page-info" id="pageInfo">
显示 0 条中的 0-0 条
</div>
<div class="page-numbers" id="pageNumbers">
<!-- 页码将通过JavaScript动态生成 -->
</div>
<div style="display: flex; gap: 8px; margin-left: auto;">
<button class="btn btn-outline btn-icon" id="prevPage" disabled>
<i class="fas fa-chevron-left"></i>
</button>
<button class="btn btn-outline btn-icon" id="nextPage" disabled>
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
<!-- 右侧用户详情面板 -->
<div class="user-detail-panel" id="userDetailPanel">
<div class="detail-header">
<h3>
<i class="fas fa-user-circle"></i>
用户详情
</h3>
</div>
<div id="userDetailContent">
<div class="empty-state">
<div class="empty-icon">
<i class="fas fa-user"></i>
</div>
<h3 class="empty-title">选择用户查看详情</h3>
<p class="empty-description">点击左侧表格中的用户行,查看详细信息</p>
</div>
</div>
</div>
</div>
</main>
<!-- 直接嵌入Footer - 不再引用外部文件 -->
<div class="footer-wrapper">
<footer class="hep-footer">
<div class="footer-content">
<div class="footer-main-section">
<div class="page-info-container">
<div class="page-info">
显示 <strong>26</strong> 条中的 <strong>1-10</strong> 条
</div>
</div>
<div class="footer-bottom">
<div class="copyright-container">
<div class="copyright">
<span class="copyright-icon">©</span>
<span class="copyright-text">2026 HEP企业管理系统 V2.0</span>
<span class="copyright-separator">|</span>
<span class="copyright-desc">保留所有权利 · 安全合规 · 高效管理</span>
</div>
</div>
<div class="footer-links">
<a href="#" class="footer-link">技术支持</a>
<span class="separator">|</span>
<a href="#" class="footer-link">联系我们</a>
<span class="separator">|</span>
<a href="#" class="footer-link">隐私政策</a>
</div>
</div>
</div>
</div>
</footer>
</div>
<!-- 删除确认模态框 -->
<div class="modal-overlay" id="deleteModal">
<div class="modal">
<div class="modal-header">
<div class="modal-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h2 class="modal-title">确认删除</h2>
<p class="modal-message" id="deleteMessage">确定要删除此用户吗?此操作不可撤销。</p>
</div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeModal('deleteModal')">
取消
</button>
<button class="btn btn-danger" id="confirmDeleteBtn">
<i class="fas fa-trash"></i>
确认删除
</button>
</div>
</div>
</div>
<!-- 状态切换确认模态框 -->
<div class="modal-overlay" id="statusModal">
<div class="modal">
<div class="modal-header">
<div class="modal-icon" id="statusModalIcon">
<i class="fas fa-power-off"></i>
</div>
<h2 class="modal-title" id="statusModalTitle">切换用户状态</h2>
<p class="modal-message" id="statusModalMessage">确定要切换用户状态吗?</p>
</div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeModal('statusModal')">
取消
</button>
<button class="btn btn-primary" id="confirmStatusBtn">
<i class="fas fa-check"></i>
确认更改
</button>
</div>
</div>
</div>
<script>
// 用户数据管理
class UserManager {
constructor() {
this.users = [];
this.currentPage = 1;
this.itemsPerPage = 10;
this.totalPages = 1;
this.currentFilter = {
status: 'all',
role: 'all',
search: ''
};
this.selectedUserId = null;
this.init();
}
async init() {
await this.loadUsers();
this.setupEventListeners();
this.render();
this.updateStats();
}
async loadUsers() {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
// 从localStorage加载或使用模拟数据
const savedUsers = localStorage.getItem('enterpriseUsers');
if (savedUsers) {
this.users = JSON.parse(savedUsers);
} else {
this.users = this.generateMockData();
this.saveUsers();
}
}
generateMockData() {
const names = ["张明", "李华", "王芳", "刘伟", "陈静", "杨阳", "赵敏", "周强", "吴勇", "郑爽"];
const roles = ["admin", "user", "vip"];
const departments = ["技术部", "市场部", "销售部", "人事部", "财务部"];
return Array.from({ length: 25 }, (_, i) => ({
id: i + 1,
username: names[i % names.length] + (i + 1),
email: `user${i + 1}@hep.cc`,
age: 25 + Math.floor(Math.random() * 20),
status: Math.random() > 0.3 ? 'active' : 'inactive',
role: roles[Math.floor(Math.random() * roles.length)],
department: departments[Math.floor(Math.random() * departments.length)],
phone: `138${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`,
createTime: this.generateRandomDate(),
lastLogin: this.generateRandomDate(),
avatarColor: this.getRandomColor()
}));
}
generateRandomDate() {
const start = new Date(2024, 0, 1);
const end = new Date();
const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
}
getRandomColor() {
const colors = ['#3B82F6', '#10B981', '#8B5CF6', '#F59E0B', '#EF4444'];
return colors[Math.floor(Math.random() * colors.length)];
}
saveUsers() {
localStorage.setItem('enterpriseUsers', JSON.stringify(this.users));
}
getFilteredUsers() {
return this.users.filter(user => {
// 状态过滤
if (this.currentFilter.status !== 'all' && user.status !== this.currentFilter.status) {
return false;
}
// 角色过滤
if (this.currentFilter.role !== 'all' && user.role !== this.currentFilter.role) {
return false;
}
// 搜索过滤
if (this.currentFilter.search) {
const searchLower = this.currentFilter.search.toLowerCase();
return user.username.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower) ||
user.department.toLowerCase().includes(searchLower);
}
return true;
});
}
getPaginatedUsers() {
const filteredUsers = this.getFilteredUsers();
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = Math.min(startIndex + this.itemsPerPage, filteredUsers.length);
return filteredUsers.slice(startIndex, endIndex);
}
setupEventListeners() {
// 搜索
document.getElementById('searchInput').addEventListener('input', (e) => {
this.currentFilter.search = e.target.value;
this.currentPage = 1;
this.render();
});
// 状态过滤
document.getElementById('statusFilter').addEventListener('change', (e) => {
this.currentFilter.status = e.target.value;
this.currentPage = 1;
this.render();
});
// 角色过滤
document.getElementById('roleFilter').addEventListener('change', (e) => {
this.currentFilter.role = e.target.value;
this.currentPage = 1;
this.render();
});
// 分页
document.getElementById('prevPage').addEventListener('click', () => {
if (this.currentPage > 1) {
this.currentPage--;
this.render();
}
});
document.getElementById('nextPage').addEventListener('click', () => {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.render();
}
});
// 导出按钮
document.getElementById('exportBtn').addEventListener('click', () => {
this.exportUsers();
});
// 删除确认
document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
this.deleteUser(this.selectedUserId);
this.closeModal('deleteModal');
});
// 状态切换确认
document.getElementById('confirmStatusBtn').addEventListener('click', () => {
this.toggleUserStatus(this.selectedUserId);
this.closeModal('statusModal');
});
}
render() {
const filteredUsers = this.getFilteredUsers();
this.totalPages = Math.ceil(filteredUsers.length / this.itemsPerPage);
const paginatedUsers = this.getPaginatedUsers();
// 更新表格
this.renderTable(paginatedUsers);
// 更新分页
this.updatePagination(filteredUsers.length);
// 更新详情面板(如果之前选择了用户)
if (this.selectedUserId) {
const user = this.users.find(u => u.id === this.selectedUserId);
if (user) {
this.showUserDetail(user);
}
}
// 更新统计数据
this.updateStats();
}
renderTable(users) {
const tbody = document.getElementById('userTableBody');
tbody.innerHTML = '';
if (users.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8">
<div class="empty-state" style="padding: 32px 0;">
<div class="empty-icon">
<i class="fas fa-search"></i>
</div>
<h3 class="empty-title">未找到用户</h3>
<p class="empty-description">尝试调整搜索条件或创建新用户</p>
</div>
</td>
</tr>
`;
return;
}
users.forEach(user => {
const row = document.createElement('tr');
row.dataset.userId = user.id;
row.className = this.selectedUserId === user.id ? 'selected' : '';
row.innerHTML = `
<td>${user.id}</td>
<td>
<div style="display: flex; align-items: center; gap: 12px;">
<div style="width: 32px; height: 32px; border-radius: 50%; background: ${user.avatarColor};
display: flex; align-items: center; justify-content: center; color: white; font-size: 12px;">
${user.username.substring(0, 2)}
</div>
<div>
<div style="font-weight: 500;">${user.username}</div>
<div style="font-size: 12px; color: var(--secondary-color);">${user.department}</div>
</div>
</div>
</td>
<td>${user.email}</td>
<td>${user.age}</td>
<td>
<span class="status-badge ${user.status === 'active' ? 'status-active' : 'status-inactive'}">
${user.status === 'active' ? '活跃' : '禁用'}
</span>
</td>
<td>
<span style="font-size: 12px; padding: 4px 12px; border-radius: 12px;
background: ${user.role === 'admin' ? 'rgba(239, 68, 68, 0.1)' :
user.role === 'vip' ? 'rgba(245, 158, 11, 0.1)' : 'rgba(59, 130, 246, 0.1)'};
color: ${user.role === 'admin' ? 'var(--danger-color)' :
user.role === 'vip' ? 'var(--warning-color)' : 'var(--primary-color)'};">
${user.role === 'admin' ? '管理员' : user.role === 'vip' ? 'VIP用户' : '普通用户'}
</span>
</td>
<td>${user.createTime}</td>
<td>
<div style="display: flex; gap: 8px;">
<button class="btn btn-outline btn-icon" onclick="userManager.editUser(${user.id})"
title="编辑">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline btn-icon" onclick="userManager.confirmToggleStatus(${user.id})"
title="${user.status === 'active' ? '禁用' : '启用'}">
<i class="fas fa-power-off"></i>
</button>
<button class="btn btn-outline btn-icon" onclick="userManager.confirmDelete(${user.id})"
title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
`;
// 点击行选择用户
row.addEventListener('click', (e) => {
if (!e.target.closest('button')) {
this.selectUser(user.id);
}
});
tbody.appendChild(row);
});
}
updatePagination(totalItems) {
const start = (this.currentPage - 1) * this.itemsPerPage + 1;
const end = Math.min(this.currentPage * this.itemsPerPage, totalItems);
document.getElementById('pageInfo').textContent =
`显示 ${totalItems} 条中的 ${start}-${end} 条`;
// 更新分页按钮状态
document.getElementById('prevPage').disabled = this.currentPage === 1;
document.getElementById('nextPage').disabled = this.currentPage === this.totalPages;
// 更新页码
const pageNumbers = document.getElementById('pageNumbers');
pageNumbers.innerHTML = '';
const maxVisiblePages = 5;
let startPage = Math.max(1, this.currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(this.totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
// 添加页码
for (let i = startPage; i <= endPage; i++) {
const pageBtn = document.createElement('button');
pageBtn.className = `page-number ${i === this.currentPage ? 'active' : ''}`;
pageBtn.textContent = i;
pageBtn.addEventListener('click', () => {
this.currentPage = i;
this.render();
});
pageNumbers.appendChild(pageBtn);
}
}
updateStats() {
const totalUsers = this.users.length;
const activeUsers = this.users.filter(u => u.status === 'active').length;
const newUsers = this.users.filter(u => {
const createDate = new Date(u.createTime.split(' ')[0].replace(/\//g, '-'));
const monthAgo = new Date();
monthAgo.setMonth(monthAgo.getMonth() - 1);
return createDate > monthAgo;
}).length;
document.getElementById('totalUsers').textContent = totalUsers;
document.getElementById('activeUsers').textContent = activeUsers;
document.getElementById('newUsers').textContent = newUsers;
}
selectUser(userId) {
this.selectedUserId = userId;
const user = this.users.find(u => u.id === userId);
if (user) {
this.showUserDetail(user);
// 更新表格行的选中状态
document.querySelectorAll('#userTableBody tr').forEach(row => {
if (parseInt(row.dataset.userId) === userId) {
row.classList.add('selected');
} else {
row.classList.remove('selected');
}
});
}
}
showUserDetail(user) {
const detailContent = document.getElementById('userDetailContent');
const initials = user.username.substring(0, 2);
detailContent.innerHTML = `
<div class="detail-avatar" style="background: ${user.avatarColor};">
${initials}
</div>
<div class="detail-info">
<div class="info-item">
<div class="info-label">用户名</div>
<div class="info-value">${user.username}</div>
</div>
<div class="info-item">
<div class="info-label">邮箱</div>
<div class="info-value">${user.email}</div>
</div>
<div class="info-item">
<div class="info-label">手机号</div>
<div class="info-value">${user.phone}</div>
</div>
<div class="info-item">
<div class="info-label">年龄</div>
<div class="info-value">${user.age} 岁</div>
</div>
<div class="info-item">
<div class="info-label">部门</div>
<div class="info-value">${user.department}</div>
</div>
<div class="info-item">
<div class="info-label">角色</div>
<div class="info-value">
<span style="font-size: 12px; padding: 4px 12px; border-radius: 12px;
background: ${user.role === 'admin' ? 'rgba(239, 68, 68, 0.1)' :
user.role === 'vip' ? 'rgba(245, 158, 11, 0.1)' : 'rgba(59, 130, 246, 0.1)'};
color: ${user.role === 'admin' ? 'var(--danger-color)' :
user.role === 'vip' ? 'var(--warning-color)' : 'var(--primary-color)'};">
${user.role === 'admin' ? '管理员' : user.role === 'vip' ? 'VIP用户' : '普通用户'}
</span>
</div>
</div>
<div class="info-item">
<div class="info-label">状态</div>
<div class="info-value">
<span class="status-badge ${user.status === 'active' ? 'status-active' : 'status-inactive'}">
${user.status === 'active' ? '活跃' : '禁用'}
</span>
</div>
</div>
<div class="info-item">
<div class="info-label">创建时间</div>
<div class="info-value">${user.createTime}</div>
</div>
<div class="info-item">
<div class="info-label">最后登录</div>
<div class="info-value">${user.lastLogin || '从未登录'}</div>
</div>
</div>
<div class="detail-actions">
<button class="btn btn-primary" onclick="userManager.editUser(${user.id})">
<i class="fas fa-edit"></i>
编辑用户
</button>
<button class="btn ${user.status === 'active' ? 'btn-warning' : 'btn-success'}"
onclick="userManager.confirmToggleStatus(${user.id})">
<i class="fas fa-power-off"></i>
${user.status === 'active' ? '禁用用户' : '启用用户'}
</button>
</div>
`;
}
editUser(userId) {
// 在实际应用中,这里会跳转到编辑页面
this.showMessage(`正在编辑用户 ${userId}...`, 'info');
}
confirmDelete(userId) {
const user = this.users.find(u => u.id === userId);
if (user) {
this.selectedUserId = userId;
document.getElementById('deleteMessage').textContent =
`确定要删除用户 "${user.username}" 吗?此操作不可撤销。`;
this.openModal('deleteModal');
}
}
confirmToggleStatus(userId) {
const user = this.users.find(u => u.id === userId);
if (user) {
this.selectedUserId = userId;
const newStatus = user.status === 'active' ? '禁用' : '启用';
document.getElementById('statusModalIcon').innerHTML =
user.status === 'active' ? '<i class="fas fa-ban"></i>' : '<i class="fas fa-check-circle"></i>';
document.getElementById('statusModalTitle').textContent = `${newStatus}用户`;
document.getElementById('statusModalMessage').textContent =
`确定要将用户 "${user.username}" ${newStatus}吗?`;
this.openModal('statusModal');
}
}
deleteUser(userId) {
const userIndex = this.users.findIndex(u => u.id === userId);
if (userIndex !== -1) {
const userName = this.users[userIndex].username;
this.users.splice(userIndex, 1);
this.saveUsers();
if (this.selectedUserId === userId) {
this.selectedUserId = null;
document.getElementById('userDetailContent').innerHTML = `
<div class="empty-state">
<div class="empty-icon">
<i class="fas fa-user"></i>
</div>
<h3 class="empty-title">选择用户查看详情</h3>
<p class="empty-description">点击左侧表格中的用户行,查看详细信息</p>
</div>
`;
}
this.render();
this.showMessage(`用户 "${userName}" 已成功删除`, 'success');
}
}
toggleUserStatus(userId) {
const user = this.users.find(u => u.id === userId);
if (user) {
user.status = user.status === 'active' ? 'inactive' : 'active';
this.saveUsers();
this.render();
const action = user.status === 'active' ? '启用' : '禁用';
this.showMessage(`用户 "${user.username}" 已成功${action}`, 'success');
}
}
exportUsers() {
const data = this.getFilteredUsers();
const csv = this.convertToCSV(data);
this.downloadCSV(csv, 'users_export.csv');
this.showMessage(`已导出 ${data.length} 条用户数据`, 'success');
}
convertToCSV(data) {
const headers = ['ID', '用户名', '邮箱', '年龄', '状态', '角色', '部门', '手机号', '创建时间'];
const rows = data.map(user => [
user.id,
user.username,
user.email,
user.age,
user.status === 'active' ? '活跃' : '禁用',
user.role === 'admin' ? '管理员' : user.role === 'vip' ? 'VIP用户' : '普通用户',
user.department,
user.phone,
user.createTime
]);
return [headers, ...rows].map(row =>
row.map(cell => `"${cell}"`).join(',')
).join('\n');
}
downloadCSV(csv, filename) {
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
showMessage(text, type = 'success') {
const messageContainer = document.getElementById('messageContainer');
const message = document.createElement('div');
message.className = `message message-${type}`;
message.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
<span>${text}</span>
`;
messageContainer.innerHTML = '';
messageContainer.appendChild(message);
// 3秒后自动隐藏
setTimeout(() => {
message.style.opacity = '0';
message.style.transform = 'translateY(-10px)';
setTimeout(() => {
if (message.parentNode) {
message.parentNode.removeChild(message);
}
}, 300);
}, 3000);
}
openModal(modalId) {
document.getElementById(modalId).classList.add('active');
}
closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
}
// 全局函数供HTML调用
function closeModal(modalId) {
userManager.closeModal(modalId);
}
// 初始化应用
let userManager;
document.addEventListener('DOMContentLoaded', () => {
userManager = new UserManager();
// 检查URL参数
const urlParams = new URLSearchParams(window.location.search);
const newUser = urlParams.get('newUser');
if (newUser) {
userManager.showMessage(`用户 "${newUser}" 创建成功!`, 'success');
// 清除URL参数
window.history.replaceState({}, document.title, window.location.pathname);
}
});
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
# 6. 企业级全局异常处理
# 6.1 创建全局异常处理器
package cc.hep.exception;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.NoHandlerFoundException;
/**
* 全局异常处理器(企业级统一异常页面)
* 注:需配合application-dev.yml中的mvc配置才能捕获404异常
*/
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理所有未捕获的业务异常
*/
@ExceptionHandler(Exception.class)
public String handleException(Exception e, Model model) {
// 生产环境需脱敏,仅返回友好提示
String errorMsg = "系统发生错误,请联系管理员";
String errorStack = e.getClass().getName() + ": " + e.getMessage();
// 开发环境显示详细错误信息(生产环境需关闭)
model.addAttribute("errorMsg", errorMsg);
model.addAttribute("errorStack", errorStack);
// 跳转到500错误页面
return "error/500";
}
/**
* 处理404异常(页面不存在)
*/
@ExceptionHandler(NoHandlerFoundException.class)
public String handle404Exception(Model model) {
model.addAttribute("errorMsg", "您访问的页面不存在或已被移除");
return "error/404";
}
/**
* 处理参数验证异常(企业级表单验证专用)
*/
@ExceptionHandler(org.springframework.validation.BindException.class)
public String handleBindException(org.springframework.validation.BindException e, Model model) {
// 获取第一个验证错误信息
String errorMsg = e.getBindingResult().getFieldError() != null
? e.getBindingResult().getFieldError().getDefaultMessage()
: "参数验证失败";
model.addAttribute("errorMsg", errorMsg);
// 返回表单页(实际项目中需根据场景调整)
return "user/add-new";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 6.2 创建错误页面
templates/error/404.html
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - 页面不存在</title>
<style>
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 0;
}
.error-container {
max-width: 600px;
margin: 100px auto;
text-align: center;
padding: 40px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.error-code {
font-size: 72px;
color: #6c757d;
margin: 0;
}
.error-message {
font-size: 24px;
color: #495057;
margin: 20px 0;
}
.btn {
display: inline-block;
background-color: #007bff;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 5px;
margin: 10px;
}
.btn:hover {
background-color: #0056b3;
}
.search-box {
margin: 30px 0;
}
.search-box input {
padding: 10px;
width: 70%;
border: 1px solid #ced4da;
border-radius: 4px;
}
.search-box button {
padding: 10px 20px;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="error-container">
<h1 class="error-code">404</h1>
<h2 class="error-message">页面不存在</h2>
<p>抱歉,您访问的页面不存在或已被移除。</p>
<div class="search-box">
<p>您可以尝试搜索需要的内容:</p>
<input type="text" placeholder="请输入关键词搜索...">
<button>搜索</button>
</div>
<div>
<a href="/user/list" class="btn">返回首页</a>
<a href="javascript:history.back()" class="btn" style="background-color: #6c757d;">返回上一页</a>
</div>
</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
templates/error/500.html
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>500 - 服务器内部错误</title>
<!-- 使用绝对路径,避免循环引用 -->
<style>
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 0;
}
.error-container {
max-width: 800px;
margin: 100px auto;
text-align: center;
padding: 40px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.error-code {
font-size: 72px;
color: #dc3545;
margin: 0;
}
.error-message {
font-size: 24px;
color: #495057;
margin: 20px 0;
}
.error-details {
background-color: #f8f9fa;
padding: 20px;
border-radius: 5px;
margin: 20px 0;
text-align: left;
font-family: monospace;
}
.btn {
display: inline-block;
background-color: #007bff;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 5px;
margin-top: 20px;
}
.btn:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="error-container">
<h1 class="error-code">500</h1>
<h2 class="error-message">服务器内部错误</h2>
<div th:if="${errorMsg}">
<p>错误信息:<span th:text="${errorMsg}"></span></p>
</div>
<div th:if="${errorStack}" class="error-details">
<strong>错误详情:</strong><br>
<span th:text="${errorStack}"></span>
</div>
<div th:unless="${errorMsg}">
<p>系统发生了一个意外错误,我们的工程师正在紧急修复中。</p>
</div>
<a href="/user/list" class="btn">返回首页</a>
<a href="javascript:history.back()" class="btn" style="background-color: #6c757d;">返回上一页</a>
</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# 7. 项目运行与测试
# 7.1 启动项目
方式1:IDEA直接启动(开发环境)
- 找到项目主类
SpringBootThymeleafApplication(cc.hep包下) - 右键点击
Run 'SpringBootThymeleafApplication' - 控制台显示
Started SpringBootThymeleafApplication in X seconds表示启动成功
方式2:Maven命令启动(通用)
# 开发环境
mvn spring-boot:run -Dspring-boot.run.profiles=dev
# 生产环境
mvn spring-boot:run -Dspring-boot.run.profiles=prod
2
3
4
5
方式3:打包后启动(生产环境)
# 打包
mvn clean package -DskipTests
# 运行Jar包(开发环境)
java -jar target/SpringBoot-Thymeleaf-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev
# 运行Jar包(生产环境)
java -jar target/SpringBoot-Thymeleaf-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
2
3
4
5
6
7
8
# 7.2 功能测试
| 测试场景 | 访问地址 | 预期结果 |
|---|---|---|
| 用户列表页 | http://localhost:8080/user/list | 显示模拟用户数据,统计信息正常 |
| 新增用户页 | http://localhost:8080/user/add | 显示表单,字段验证生效 |
| 用户列表页模板布局 | http://localhost:8080/user/listWithLayout | 除了显示菜单还有版权信息 |
| 提交空用户名 | 新增用户页提交空表单 | 显示“用户名不能为空”提示 |
| 提交非法邮箱 | 新增用户页输入abc@xxx | 显示“邮箱格式不正确”提示 |
| 404页面 | http://localhost:8080/xxx | 显示自定义404页面 |
| 500异常 | 手动抛出异常(如修改代码) | 显示自定义500页面,包含错误信息 |

常见问题排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 模板找不到 | 1. 模板路径错误 2. 文件名大小写不一致 3. prefix配置错误 | 1. 检查templates目录结构 2. 确保文件名与控制器返回值一致 3. 确认application.yml中prefix配置正确 |
| 静态资源加载失败 | 1. 资源路径错误 2. static-path-pattern配置错误 3. 资源文件不存在 | 1. 使用@{/css/xxx.css}引用 2. 确认static-path-pattern: /** 3. 检查static目录下文件是否存在 |
| 表单验证不生效 | 1. 缺少validation依赖 2. 未加@Valid注解 3. 未传入BindingResult | 1. 确认pom.xml中引入validation依赖 2. 控制器参数添加@Valid 3. 方法参数添加BindingResult |
| 404异常不生效 | 1. 未配置throw-exception-if-no-handler-found 2. 未关闭资源映射 | 1. application-dev.yml中添加mvc.throw-exception-if-no-handler-found=true 2. 添加spring.web.resources.add-mappings=false |
| Thymeleaf表达式报错 | 1. 变量名错误 2. 集合为空调用方法 3. 语法错误 | 1. 检查Model中变量名是否一致 2. 使用#lists.isEmpty()判断集合是否为空 3. 检查${}、th:each等语法是否正确 |
# 总结
企业级最佳实践
- 职责分离:严格遵循Controller→Service→Repository分层,控制器仅处理请求,不写业务逻辑
- 配置规范:环境分离配置,开发环境关闭缓存/开启详细日志,生产环境开启缓存/精简日志
- 异常处理:全局异常统一处理,404/500/参数验证异常分类处理,生产环境脱敏
- 性能优化:Thymeleaf模板缓存、静态资源CDN、分页查询、避免模板复杂逻辑
- 安全防护:XSS防护、CSRF防护、敏感信息脱敏、日志规范