Compare commits

...

26 Commits

@ -10,4 +10,9 @@
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="exam" options="-parameters" />
</option>
</component>
</project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="MySQL" />
</component>
</project>

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -1,12 +1,17 @@
// 指定当前类所在的包路径这是Java中组织类的一种方式有助于类的管理和访问控制。
package lsgwr.exam;
// 导入Spring Boot的启动类和应用配置自动装配的注解类。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// @SpringBootApplication是一个方便的注解它包括了@Configuration@EnableAutoConfiguration和@ComponentScan注解。
// 它告诉Spring Boot基于当前类所在包及其子包下的组件来启动自动配置和组件扫描。
@SpringBootApplication
public class ExamApplication {
public static void main(String[] args) {
// main方法是Java应用程序的入口点。当运行这个类时JVM会调用这个方法。
public static void main(String[] args)
{
// SpringApplication.run方法启动Spring应用传入ExamApplication.class当前启动类和main方法的参数args。
// 这个方法会执行一系列的操作包括启动Spring容器加载应用上下文自动配置等。
SpringApplication.run(ExamApplication.class, args);
}
}

@ -4,24 +4,35 @@
* @date : 2019-05-17 00:11
* @email : liangshanguang2@gmail.com
***********************************************************/
package lsgwr.exam.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
package lsgwr.exam.config;// 指定当前类所在的包路径
// 导入所需的类
import jdk.internal.instrumentation.Logger;
import lombok.extern.slf4j.Slf4j;// Lombok库提供的注解用于自动为类添加SLF4J日志记录器
import org.springframework.context.annotation.Bean;// Spring框架提供的注解用于标记配置类
import org.springframework.context.annotation.Configuration;// Spring框架提供的注解用于标记配置类。
import org.springframework.web.servlet.config.annotation.CorsRegistry;// Spring MVC框架提供的接口用于自定义Web MVC配置
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;// Spring MVC框架提供的接口用于自定义Web MVC配置。
// 使用@Configuration注解标记此类为Spring的配置类。
// 使用@Slf4j注解自动为类添加一个名为log的SLF4J日志记录器。
@Configuration
@Slf4j
public class CORSConf {
// 使用@Bean注解声明一个方法该方法将返回一个对象该对象将被注册为Spring应用上下文中的bean。
// 此处声明的方法返回一个WebMvcConfigurer类型的对象用于配置跨域资源共享CORS
@Bean
public WebMvcConfigurer corsConfigurer() {
// 返回一个新的WebMvcConfigurer匿名类实例。
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
Logger log = null;
// 尝试记录日志信息但由于log被初始化为null这行代码会抛出NullPointerException。
log.info("初始化 CORSConfiguration 配置");
// 使用CorsRegistry配置CORS规则
// 允许所有路径("/**")的跨域请求。
// 允许所有HTTP头"*")。
// 允许所有HTTP方法"*")。
// 允许所有来源的跨域请求("*")。
registry.addMapping("/**")
.allowedHeaders("*")
.allowedMethods("*")

@ -5,123 +5,181 @@
* @email : liangshanguang2@gmail.com
***********************************************************/
package lsgwr.exam.controller;
// 导入考试相关的实体类,用于在控制器中处理和传递对应的业务数据,比如考试信息、考试记录信息等
import lsgwr.exam.entity.Exam;
import lsgwr.exam.entity.ExamRecord;
// 导入考试业务逻辑层的服务类,通过它可以调用具体的业务方法来实现诸如获取考试数据、更新考试等操作
import lsgwr.exam.service.ExamService;
// 导入Swagger相关注解用于生成API文档Api注解用于给一组API接口定义一个标签方便文档中分类展示
import io.swagger.annotations.Api;
// ApiOperation注解用于描述单个API接口的具体功能在API文档中展示接口的详细说明
import io.swagger.annotations.ApiOperation;
// 导入所有视图对象VO视图对象通常是用于在不同层之间传递数据的载体会根据前端展示需求对实体数据进行适当封装和整理
import lsgwr.exam.vo.*;
// 导入Spring框架提供的Bean属性拷贝工具类方便在不同的Java对象之间进行属性值的复制操作
import org.springframework.beans.BeanUtils;
// 导入Spring框架的依赖注入注解用于自动装配相关的Bean实例到类的成员变量中
import org.springframework.beans.factory.annotation.Autowired;
// 导入Spring Web中用于处理HTTP请求的各种注解例如定义请求方法、请求路径、路径变量、请求体等相关的注解
import org.springframework.web.bind.annotation.*;
// 导入Java中处理HTTP请求的类在这个控制器中可以通过它获取请求相关的信息比如请求头中的用户标识等信息
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
// @RestController注解表明这个类是一个Spring RESTful风格的Web控制器它结合了@Controller和@ResponseBody注解的功能意味着该类中处理请求的方法默认会将返回值直接转换为JSON等格式响应给客户端
@RestController
// @Api注解为Swagger文档定义标签这里将这个控制器下的所有API接口归为"Exam APIs"这一组便于在Swagger生成的API文档里进行分类查看和管理
@Api(tags = "Exam APIs")
// @RequestMapping注解用于定义该控制器中所有接口的基础请求路径即这个控制器下所有接口的请求URL都将以 "/api/exam" 开头
@RequestMapping("/api/exam")
public class ExamController {
// 使用@Autowired注解进行依赖注入让Spring容器自动将ExamService的实例注入到这个成员变量中方便后续在控制器的方法里调用其业务方法
@Autowired
private ExamService examService;
private ExamService examService;// 自动注入 ExamService 服务
/**
*
* HTTPGET访 "/api/exam/question/all"
*/
@GetMapping("/question/all")
@ApiOperation("获取所有问题的列表")
ResultVO<List<QuestionVo>> getQuestionAll() {
// 定义一个ResultVO类型的变量用于存放最终要返回给客户端的结果对象其泛型参数指定为List<QuestionVo>,表示包含问题列表数据的结果封装
ResultVO<List<QuestionVo>> resultVO;
try {
// 调用ExamService的getQuestionAll方法该方法应该在服务层实现从数据库或者其他数据源获取所有问题数据的逻辑返回一个QuestionVo类型的列表代表所有问题信息
List<QuestionVo> questionAll = examService.getQuestionAll();
// 如果获取数据成功创建一个表示成功的ResultVO对象其中状态码设置为0表示操作成功消息设置为"获取全部问题列表成功"并将获取到的问题列表数据放入ResultVO对象中
resultVO = new ResultVO<>(0, "获取全部问题列表成功", questionAll);
} catch (Exception e) {
// 如果在获取数据过程中出现异常,打印异常堆栈信息,方便后续排查问题,通常在开发和调试阶段查看具体出错原因
e.printStackTrace();
resultVO = new ResultVO<>(-1, "获取全部问题列表失败", null);
// 创建一个表示失败的ResultVO对象状态码设置为 -1表示操作失败消息设置为"获取全部问题列表失败"数据部分设置为null因为没有成功获取到问题列表数据
resultVO = new ResultVO<>(-1, "获取全部问题列表失败", null);// 返回失败结果
}
return resultVO;
}
/**
*
* HTTPPOST "/api/exam/question/update"QuestionVo
*/
@PostMapping("/question/update")
@ApiOperation("更新问题")
ResultVO<QuestionVo> questionUpdate(@RequestBody QuestionVo questionVo) {
// 完成问题的更新
System.out.println(questionVo);
// 打印接收到的要更新的问题对象信息,在开发调试阶段可以通过查看控制台输出来确认前端传递过来的数据是否符合预期,方便排查问题
System.out.println(questionVo);// 打印接收到的问题信息以供调试
try {
// 调用ExamService的updateQuestion方法将接收到的要更新的问题对象questionVo传递进去由服务层实现具体的更新逻辑比如更新数据库中对应的问题记录等操作返回更新后的问题对象
QuestionVo questionVoResult = examService.updateQuestion(questionVo);
// 如果更新成功创建一个表示成功的ResultVO对象状态码设置为0表示更新操作成功消息设置为"更新问题成功"并将更新后的问题对象放入ResultVO对象中返回给客户端
return new ResultVO<>(0, "更新问题成功", questionVoResult);
} catch (Exception e) {
// 如果在更新过程中出现异常,打印异常堆栈信息,便于查找问题所在
e.printStackTrace();
// 创建一个表示更新失败的ResultVO对象状态码设置为 -1表示更新操作失败消息设置为"更新问题失败"数据部分设置为null因为没有成功完成更新操作
return new ResultVO<>(-1, "更新问题失败", null);
}
}
/**
*
* HTTPPOST "/api/exam/question/create"QuestionCreateSimplifyVoHttpServletRequest
*/
@PostMapping("/question/create")
@ApiOperation("创建问题")
ResultVO<String> questionCreate(@RequestBody QuestionCreateSimplifyVo questionCreateSimplifyVo, HttpServletRequest request) {
// 创建一个QuestionCreateVo对象用于组装完整的创建问题所需的数据可能QuestionCreateSimplifyVo只是包含了部分必要信息需要进一步完善才能用于创建操作
QuestionCreateVo questionCreateVo = new QuestionCreateVo();
// 把能拷贝过来的属性都拷贝过来
BeanUtils.copyProperties(questionCreateSimplifyVo, questionCreateVo);
// 设置创建者信息
String userId = (String) request.getAttribute("user_id");
// 使用Spring的BeanUtils工具类将questionCreateSimplifyVo对象中的属性值拷贝到questionCreateVo对象中这样可以方便地复用已有的部分数据避免重复设置属性
BeanUtils.copyProperties(questionCreateSimplifyVo, questionCreateVo);// 拷贝属性到新的对象
// 从HttpServletRequest对象中获取用户ID信息通常这个用户ID是在请求处理的前置环节比如拦截器中设置到请求属性中的用于标识创建这个问题的用户是谁
String userId = (String) request.getAttribute("user_id");// 从请求中获取用户ID
// 将获取到的用户ID设置到questionCreateVo对象中作为问题创建者的标识以便在后续保存问题数据到数据库等操作中记录创建者信息
questionCreateVo.setQuestionCreatorId(userId);
System.out.println(questionCreateVo);
try {
// 调用ExamService的questionCreate方法将组装好的questionCreateVo对象传递进去由服务层实现将新问题数据保存到数据库等具体的创建逻辑操作
examService.questionCreate(questionCreateVo);
return new ResultVO<>(0, "问题创建成功", null);
// 如果创建成功创建一个表示成功的ResultVO对象状态码设置为0表示创建操作成功消息设置为"问题创建成功"这里数据部分设置为null具体根据业务需求而定可能创建成功不需要返回具体的数据内容
return new ResultVO<>(0, "问题创建成功", null);// 返回成功结果
} catch (Exception e) {
// 如果在创建过程中出现异常,打印异常堆栈信息,方便排查问题原因
e.printStackTrace();
// 创建一个表示创建失败的ResultVO对象状态码设置为 -1表示创建操作失败消息设置为"创建问题失败"数据部分设置为null
return new ResultVO<>(-1, "创建问题失败", null);
}
}
/**
*
* HTTPGET访 "/api/exam/question/selection"
*/
@GetMapping("/question/selection")
@ApiOperation("获取问题分类的相关选项")
ResultVO<QuestionSelectionVo> getSelections() {
// 调用ExamService的getSelections方法该方法在服务层应该实现从数据库或者其他数据源获取问题分类相关选项数据的逻辑返回一个QuestionSelectionVo对象代表分类选项信息
QuestionSelectionVo questionSelectionVo = examService.getSelections();
if (questionSelectionVo != null) {
// 如果获取到的问题分类选项对象不为空说明获取操作成功创建一个表示成功的ResultVO对象状态码设置为0表示获取成功消息设置为"获取问题分类选项成功"并将获取到的分类选项对象放入ResultVO对象中返回给客户端
return new ResultVO<>(0, "获取问题分类选项成功", questionSelectionVo);
} else {
return new ResultVO<>(-1, "获取问题分类选项失败", null);
// 如果获取到的问题分类选项对象为空说明获取操作失败创建一个表示失败的ResultVO对象状态码设置为 -1表示获取失败消息设置为"获取问题分类选项失败"数据部分设置为null
return new ResultVO<>(-1, "获取问题分类选项失败", null);// 分类选项为空时返回失败信息
}
}
/**
* ID
* HTTPGETID "/api/exam/question/detail/{id}"{id}ID
*/
@GetMapping("/question/detail/{id}")
@ApiOperation("根据问题的id获取问题的详细信息")
ResultVO<QuestionDetailVo> getQuestionDetail(@PathVariable String id) {
// 根据问题id获取问题的详细信息
// 打印出请求的问题ID信息在开发调试阶段可以通过查看控制台输出来确认请求的参数是否正确传递过来方便排查问题以及记录请求日志
System.out.println(id);
ResultVO<QuestionDetailVo> resultVO;
try {
// 调用ExamService的getQuestionDetail方法将接收到的问题ID传递进去由服务层实现从数据库或者其他数据源根据ID查询对应问题详细信息的逻辑返回一个QuestionDetailVo对象代表问题详细信息
QuestionDetailVo questionDetailVo = examService.getQuestionDetail(id);
// 如果获取详细信息成功创建一个表示成功的ResultVO对象状态码设置为0表示获取操作成功消息设置为"获取问题详情成功"并将获取到的问题详细信息对象放入ResultVO对象中
resultVO = new ResultVO<>(0, "获取问题详情成功", questionDetailVo);
} catch (Exception e) {
// 如果在获取详细信息过程中出现异常,打印异常堆栈信息,便于查找问题原因
e.printStackTrace();
// 创建一个表示获取失败的ResultVO对象状态码设置为 -1表示获取操作失败消息设置为"获取问题详情失败"数据部分设置为null
resultVO = new ResultVO<>(-1, "获取问题详情失败", null);
}
return resultVO;
return resultVO;// 返回结果对象
}
/**
*
* HTTPGET访 "/api/exam/all"
*/
@GetMapping("/all")
@ApiOperation("获取全部考试的列表")
ResultVO<List<ExamVo>> getExamAll() {
// 需要拼接前端需要的考试列表对象
// 定义一个ResultVO类型的变量用于存放最终要返回给客户端的结果对象其泛型参数指定为List<ExamVo>,表示包含考试列表数据的结果封装
ResultVO<List<ExamVo>> resultVO;
try {
// 调用ExamService的getExamAll方法该方法应该在服务层实现从数据库或者其他数据源获取所有考试数据的逻辑返回一个ExamVo类型的列表代表所有考试信息
List<ExamVo> examVos = examService.getExamAll();
// 如果获取数据成功创建一个表示成功的ResultVO对象状态码设置为0表示操作成功消息设置为"获取全部考试的列表成功"并将获取到的考试列表数据放入ResultVO对象中
resultVO = new ResultVO<>(0, "获取全部考试的列表成功", examVos);
} catch (Exception e) {
// 如果在获取数据过程中出现异常,打印异常堆栈信息,方便后续排查问题
e.printStackTrace();
// 创建一个表示失败的ResultVO对象状态码设置为 -1表示操作失败消息设置为"获取全部考试的列表失败"数据部分设置为null因为没有成功获取到考试列表数据
resultVO = new ResultVO<>(-1, "获取全部考试的列表失败", null);
}
return resultVO;
return resultVO;// 返回结果对象
}
/**
*
* HTTPGET访 "/api/exam/question/type/list"
*/
@GetMapping("/question/type/list")
@ApiOperation("获取问题列表,按照单选、多选和判断题分类返回")
ResultVO<ExamQuestionTypeVo> getExamQuestionTypeList() {
// 获取问题的分类列表
// 定义一个ResultVO类型的变量用于存放最终要返回给客户端的结果对象其泛型参数指定为ExamQuestionTypeVo用于封装按照特定分类方式整理后的问题列表相关信息
ResultVO<ExamQuestionTypeVo> resultVO;
try {
// 调用ExamService的getExamQuestionType方法该方法在服务层应该实现从数据库或者其他数据源获取并整理按照单选、多选和判断题分类后的问题列表数据的逻辑返回一个ExamQuestionTypeVo对象
ExamQuestionTypeVo examQuestionTypeVo = examService.getExamQuestionType();
resultVO = new ResultVO<>(0, "获取问题列表成功", examQuestionTypeVo);
} catch (Exception e) {
@ -130,113 +188,194 @@ public class ExamController {
}
return resultVO;
}
/**
*
* HTTPPOST "/api/exam/create"ExamCreateVoHttpServletRequestID
*/
@PostMapping("/create")
@ApiOperation("创建考试")
ResultVO<Exam> createExam(@RequestBody ExamCreateVo examCreateVo, HttpServletRequest request) {
// 从前端传参数过来,在这里完成考试的入库
// 定义一个ResultVO类型的变量用于存放最终要返回给客户端的结果对象其泛型参数指定为Exam表示包含新创建考试相关信息的结果封装
ResultVO<Exam> resultVO;
// 从HttpServletRequest对象中获取用户ID信息通常这个用户ID是在请求处理的前置环节比如拦截器中设置到请求属性中的用于标识创建这个考试的用户是谁
String userId = (String) request.getAttribute("user_id");
try {
// 调用ExamService的create方法将创建考试的视图对象examCreateVo和获取到的用户ID传递进去由服务层实现将考试记录数据保存到数据库等具体的创建逻辑操作返回创建好的Exam对象代表新创建的考试信息
Exam exam = examService.create(examCreateVo, userId);
// 如果创建成功创建一个表示成功的ResultVO对象状态码设置为0表示创建操作成功消息设置为"创建考试成功"并将创建好的考试对象放入ResultVO对象中返回给客户端
resultVO = new ResultVO<>(0, "创建考试成功", exam);
} catch (Exception e) {
e.printStackTrace();
// 如果在创建过程中出现异常创建一个表示创建失败的ResultVO对象状态码设置为 -1表示创建操作失败消息设置为"创建考试失败"数据部分设置为null
resultVO = new ResultVO<>(-1, "创建考试失败", null);
}
return resultVO;
}
/**
*
* HTTP
* ResultVO
*
* @param examVo
* @param request HTTPID
* @return ResultVO<Exam>ResultVOExam
*/
@PostMapping("/update")
@ApiOperation("更新考试")
ResultVO<Exam> updateExam(@RequestBody ExamVo examVo, HttpServletRequest request) {
// 从前端传参数过来,在这里完成考试的入库
// 从前端传参数过来,在这里完成考试的入库此处先定义一个用于存放最终要返回给客户端的ResultVO<Exam>类型的结果对象,后续根据操作成功与否进行赋值。
ResultVO<Exam> resultVO;
// 从HttpServletRequest对象中获取用户ID属性值该用户ID用于标识当前执行更新考试操作的用户是后续业务逻辑判断如权限校验、操作记录等的重要依据。
String userId = (String) request.getAttribute("user_id");
try {
// 调用ExamService的update方法传入包含更新数据的examVo视图对象以及获取到的用户ID由服务层去实现具体更新数据库中对应考试记录的逻辑比如根据传入数据修改相应字段等操作并返回更新后的Exam对象包含最新的考试信息
Exam exam = examService.update(examVo, userId);
// 如果更新操作成功创建一个表示成功的ResultVO对象状态码设置为0表示更新考试成功消息设置为"更新考试成功"并将更新后的考试对象放入ResultVO对象中用于返回给客户端展示最新的考试信息。
resultVO = new ResultVO<>(0, "更新考试成功", exam);
} catch (Exception e) {
// 如果在更新考试信息的过程中出现异常,打印异常堆栈信息,方便开发人员后续排查问题,定位是哪里出现的错误导致更新失败。
e.printStackTrace();
// 创建一个表示更新失败的ResultVO对象状态码设置为 -1表示更新考试失败消息可根据业务实际情况默认为"更新考试失败"之类的提示信息数据部分设置为null因为没有成功获取到更新后的有效考试信息。
resultVO = new ResultVO<>(-1, "更新考试失败", null);
}
return resultVO;
}
/**
*
* HTTPGETResultVO<List<ExamCardVo>>
* ResultVO
*
* @returnResultVOResultVOList<ExamCardVo>ExamCardVo
*/
@GetMapping("/card/list")
@ApiOperation("获取考试列表,适配前端卡片列表")
ResultVO<List<ExamCardVo>> getExamCardList() {
// 获取考试列表卡片
// 获取考试列表卡片先定义一个用于存放最终要返回给客户端的ResultVO<List<ExamCardVo>>类型的结果对象,后续根据获取数据的情况进行赋值操作。
ResultVO<List<ExamCardVo>> resultVO;
try {
// 调用ExamService的getExamCardList方法由服务层实现从数据库或者其他数据源获取并整理适合前端以卡片形式展示的考试列表数据的逻辑返回一个List<ExamCardVo>类型的列表,代表所有考试的卡片信息。
List<ExamCardVo> examCardVoList = examService.getExamCardList();
// 如果获取数据成功创建一个表示成功的ResultVO对象状态码设置为0表示获取考试列表卡片成功消息设置为"获取考试列表卡片成功"并将获取到的考试卡片列表数据放入ResultVO对象中以便返回给客户端进行展示。
resultVO = new ResultVO<>(0, "获取考试列表卡片成功", examCardVoList);
} catch (Exception e) {
// 如果在获取考试卡片列表数据的过程中出现异常,打印异常堆栈信息,方便开发人员排查问题,查看是数据源连接问题还是数据查询等环节出现的错误导致获取失败。
e.printStackTrace();
// 创建一个表示获取失败的ResultVO对象状态码设置为 -1表示获取考试列表卡片失败消息可根据业务实际情况设置为相应的提示语数据部分设置为null因为没有成功获取到有效的考试卡片列表数据。
resultVO = new ResultVO<>(-1, "获取考试列表卡片失败", null);
}
return resultVO;
}
/**
* ID
* HTTPGETResultVO<ExamDetailVo>
* ResultVO
*
* @param id
* @return ResultVOResultVOExamDetailVo
*/
@GetMapping("/detail/{id}")
@ApiOperation("根据考试的id获取考试详情")
ResultVO<ExamDetailVo> getExamDetail(@PathVariable String id) {
// 根据id获取考试详情
// 根据id获取考试详情先定义一个用于存放最终要返回给客户端的ResultVO<ExamDetailVo>类型的结果对象,后续根据查询数据的情况进行赋值操作。
ResultVO<ExamDetailVo> resultVO;
try {
// 调用ExamService的getExamDetail方法将接收到的考卷唯一标识符id传递进去由服务层实现从数据库或者其他数据源根据该ID查询对应考卷详细信息的逻辑返回一个ExamDetailVo对象代表该考卷的所有详细信息。
ExamDetailVo examDetail = examService.getExamDetail(id);
// 如果获取详细信息成功创建一个表示成功的ResultVO对象状态码设置为0表示获取考试详情成功消息设置为"获取考试详情成功"并将获取到的考卷详细信息对象放入ResultVO对象中以便返回给客户端进行展示。
resultVO = new ResultVO<>(0, "获取考试详情成功", examDetail);
} catch (Exception e) {
// 如果在获取考卷详细信息的过程中出现异常创建一个表示获取失败的ResultVO对象状态码设置为 -1表示获取考试详情失败消息可根据业务实际情况设置为相应的提示语数据部分设置为null因为没有成功获取到有效的考卷详细信息。
resultVO = new ResultVO<>(-1, "获取考试详情失败", null);
}
return resultVO;
}
/**
*
* HTTP
* ResultVO<ExamRecord>ResultVO
*
* @param examId
* @param answersMap HashMap<String, List<String>>
* @param request IDHTTP便
* @return ResultVO<ExamRecord>ExamRecord
*/
@PostMapping("/finish/{examId}")
@ApiOperation("根据用户提交的答案对指定id的考试判分")
ResultVO<ExamRecord> finishExam(@PathVariable String examId, @RequestBody HashMap<String, List<String>> answersMap, HttpServletRequest request) {
// 定义一个用于存放结果的ResultVO<ExamRecord>类型的数据结构,后续根据评分操作的成功与否以及获取到的相关数据进行赋值,用于最终返回给客户端展示评分结果。
ResultVO<ExamRecord> resultVO;
try {
// 拦截器里设置上的用户id
// 拦截器里设置上的用户id从HttpServletRequest对象中获取用户ID属性值该用户ID用于标识当前提交答案并进行评分的用户是后续业务逻辑处理如记录答题记录归属、判断是否有权限答题等的重要依据。
String userId = (String) request.getAttribute("user_id");
// 下面根据用户提交的信息进行判分,返回用户的得分情况
// 下面根据用户提交的信息进行判分返回用户的得分情况调用ExamService的judge方法传入获取到的用户ID、考试唯一标识符examId以及用户提交的答案集合answersMap由服务层实现具体的评分逻辑比如对比答案、计算得分等操作并返回一个ExamRecord对象包含了评分后的成绩记录等详细信息。
ExamRecord examRecord = examService.judge(userId, examId, answersMap);
resultVO = new ResultVO<>(0, "考卷提交成功", examRecord);
// 封装成绩记录到最终结果中创建一个表示评分成功的ResultVO对象状态码设置为0表示考卷提交评分成功消息设置为"考卷提交成功"并将包含成绩记录的ExamRecord对象放入ResultVO对象中以便返回给客户端展示评分结果。
resultVO = new ResultVO<>(0, "考卷提交成功", examRecord);// 封装成绩记录到最终结果中
} catch (Exception e) {
// 如果在评分过程中出现异常,打印异常堆栈信息,方便开发人员排查问题,查看是答案解析出错还是其他业务逻辑环节出现的错误导致评分失败。
e.printStackTrace();
// 创建一个表示评分失败的ResultVO对象状态码设置为 -1表示考卷提交评分失败消息可根据业务实际情况设置为相应的提示语数据部分设置为null因为没有成功获取到有效的评分成绩记录。
resultVO = new ResultVO<>(-1, "考卷提交失败", null);
}
return resultVO;
}
/**
*
* HTTPGETResultVO<List<ExamRecordVo>>
* ResultVO
*
* @param request ID便
* @return ResultVOResultVOList<ExamRecordVo>ExamRecordVo
*/
@GetMapping("/record/list")
@ApiOperation("获取当前用户的考试记录")
ResultVO<List<ExamRecordVo>> getExamRecordList(HttpServletRequest request) {
// 定义一个用于存放结果的ResultVO<List<ExamRecordVo>>类型的数据结构,后续根据查询用户考试记录操作的成功与否以及获取到的相关数据进行赋值,用于最终返回给客户端展示查询结果。
ResultVO<List<ExamRecordVo>> resultVO;
try {
// 拦截器里设置上的用户id
// 拦截器里设置上的用户id从HttpServletRequest对象中获取用户ID属性值该用户ID用于明确要查询其考试记录的目标用户是服务层准确获取对应数据的关键依据。
String userId = (String) request.getAttribute("user_id");
// 下面根据用户账号拿到他(她所有的考试信息)注意要用VO封装下
// 下面根据用户账号拿到他所有的考试信息注意要用VO封装下调用ExamService的getExamRecordList方法传入获取到的用户ID由服务层实现从数据库或者其他数据源获取该用户所有历史考试记录数据的逻辑并将其整理封装成List<ExamRecordVo>类型的列表返回每个ExamRecordVo对象包含了如时间、得分等详细信息。
List<ExamRecordVo> examRecordVoList = examService.getExamRecordList(userId);
resultVO = new ResultVO<>(0, "获取考试记录成功", examRecordVoList);
// 封装查询得到的信息到最终结果中创建一个表示获取成功的ResultVO对象状态码设置为0表示获取考试记录成功消息设置为"获取考试记录成功"并将获取到的用户考试记录列表数据放入ResultVO对象中以便返回给客户端展示历史考试记录情况。
resultVO = new ResultVO<>(0, "获取考试记录成功", examRecordVoList);//封装查询得到的信息到最终结果中;
} catch (Exception e) {
// 如果在获取用户考试记录数据的过程中出现异常,打印异常堆栈信息,方便开发人员排查问题,查看是数据源查询出错还是数据封装等环节出现的错误导致获取失败。
e.printStackTrace();
// 创建一个表示获取失败的ResultVO对象状态码设置为 -1表示获取考试记录失败消息可根据业务实际情况设置为相应的提示语数据部分设置为null因为没有成功获取到有效的用户考试记录数据。
resultVO = new ResultVO<>(-1, "获取考试记录失败", null);
}
return resultVO;
return resultVO;//返回封装好的 数据结构
}
/**
* recordId
* HTTPGETResultVO<RecordDetailVo>
* ResultVO
*
* @param recordId
* @return ResultVOResultVORecordDetailVo
*/
@GetMapping("/record/detail/{recordId}")
@ApiOperation("根据考试记录id获取考试记录详情")
ResultVO<RecordDetailVo> getExamRecordDetail(@PathVariable String recordId) {
// 定义一个用于存放结果的ResultVO<RecordDetailVo>类型的数据结构,后续根据查询测验详细情况操作的成功与否以及获取到的相关数据进行赋值,用于最终返回给客户端展示查询结果。
ResultVO<RecordDetailVo> resultVO;
try {
// 调用ExamService的getRecordDetail方法将接收到的测验记录唯一标识符recordId传递进去由服务层实现从数据库或者其他数据源根据该ID查询对应测验详细信息的逻辑返回一个RecordDetailVo对象代表该次测验的所有详细信息。
RecordDetailVo recordDetailVo = examService.getRecordDetail(recordId);
resultVO = new ResultVO<>(0, "获取考试记录详情成功", recordDetailVo);
// 封装查询得到的信息到最终结果中创建一个表示获取成功的ResultVO对象状态码设置为0表示获取考试记录详情成功消息设置为"获取考试记录详情成功"并将获取到的测验详细信息对象放入ResultVO对象中以便返回给客户端展示该次测验的详细情况。
resultVO = new ResultVO<>(0, "获取考试记录详情成功", recordDetailVo);//封装查询得到的信息到最终结果中;
} catch (Exception e) {
// 如果在获取测验详细信息的过程中出现异常,打印异常堆栈信息,方便开发人员排查问题,查看是数据源查询出错还是数据解析等环节出现的错误导致获取失败。
e.printStackTrace();
// 创建一个ResultVO对象表示操作失败
// 状态码设置为-1表示获取考试记录详情失败
// 消息设置为"获取考试记录详情失败"
// 由于查询失败详细信息对象设置为null
resultVO = new ResultVO<>(-1, "获取考试记录详情失败", null);
}
// 返回封装好的ResultVO对象给客户端
// 客户端可以根据状态码和消息判断操作是否成功,并根据详细信息对象获取测验的详细记录信息
return resultVO;
}
}

@ -4,51 +4,60 @@
* @date : 2019/5/14 07:42
* @email : liangshanguang2@gmail.com
***********************************************************/
// 定义包名,用于组织类文件,避免命名冲突
package lsgwr.exam.entity;
// 导入Jackson库的JsonFormat注解用于JSON序列化时自定义日期格式
import com.fasterxml.jackson.annotation.JsonFormat;
// 导入Lombok库的Data注解用于自动生成getter、setter、equals、hashCode和toString方法
import lombok.Data;
// 导入Hibernate的DynamicUpdate注解用于在实体更新时只更新发生变化的字段
import org.hibernate.annotations.DynamicUpdate;
// 导入JPA的Entity注解用于声明该类是一个JPA实体类
import javax.persistence.Entity;
// 导入JPA的Id注解用于声明该类中的某个字段作为主键
import javax.persistence.Id;
// 导入Java的Date类用于表示日期和时间
import java.util.Date;
@Entity
@Data
@DynamicUpdate
public class Exam {
// 使用JPA的@Id注解声明该字段为主键
@Id
private String examId;
private String examName;
private String examAvatar;
private String examDescription;
private String examQuestionIds;
private String examQuestionIdsRadio;
private String examQuestionIdsCheck;
private String examQuestionIdsJudge;
private Integer examScore;
private Integer examScoreRadio;
private Integer examScoreCheck;
private Integer examScoreJudge;
private String examCreatorId;
private Integer examTimeLimit;
private String examId;// 考试ID唯一标识一场考试
private String examName;// 考试名称
private String examAvatar; // 考试头像或图标
private String examDescription;// 考试描述或简介
private String examQuestionIds;// 存储所有问题ID的字符串可能是逗号分隔的ID列表
private String examQuestionIdsRadio;// 存储所有单选题ID的字符串
private String examQuestionIdsCheck;// 存储所有多选题ID的字符串
private String examQuestionIdsJudge;// 存储所有判断题ID的字符串
private Integer examScore;// 考试总分
private Integer examScoreRadio;// 单选题总分
private Integer examScoreCheck;// 多选题总分
private Integer examScoreJudge;// 判断题总分
private String examCreatorId;// 创建者ID标识谁创建了这场考试
private Integer examTimeLimit;// 考试时间限制,单位可能是分钟
// 考试开始时间使用Jackson的@JsonFormat注解自定义日期格式
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date examStartDate;
// 考试结束时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date examEndDate;
/**
* , Java
* Java
* 使Jackson@JsonFormat便
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
/**
* Java
* @DynamicUpdate
* Java
* 使Hibernate@DynamicUpdate
* 使Jackson@JsonFormat便
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;

@ -7,42 +7,44 @@
package lsgwr.exam.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;// 引入Jackson库用于日期格式化的注解
import lombok.Data;// 引入Lombok库用于简化Java实体类的编写如自动生成getter和setter方法
import javax.persistence.Entity;
import javax.persistence.Entity;// 引入JPA注解用于标识这是一个实体类并映射到数据库中的一张表
// 引入JPA注解用于标识实体类的主键字段
import javax.persistence.Id;
import java.util.Date;
// 使用@Data注解自动生成getter和setter方法以及toString、equals和hashCode方法
@Data
// 使用@Entity注解标识这是一个JPA实体类
@Entity
public class ExamRecord {
/**
*
*
*/
@Id
private String examRecordId;
/**
* id
* ID
*/
private String examId;
/**
* (_-),
* 线_线-
*/
private String answerOptionIds;
/**
* userid
* IDID
*/
private String examJoinerId;
/**
*
*
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date examJoinDate;
/**
* ()
*
*/
private Integer examTimeCost;
/**
@ -50,7 +52,7 @@ public class ExamRecord {
*/
private Integer examJoinScore;
/**
*
*
*/
private Integer examResultLevel;
}
}

@ -7,18 +7,22 @@
package lsgwr.exam.entity;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.Data;// 引入Lombok库中的@Data注解用于自动生成getter、setter、toString、equals和hashCode方法
import javax.persistence.Entity;// 引入JPAJava Persistence API中的@Entity注解标识这是一个实体类与数据库中的表相对应
import javax.persistence.GeneratedValue;// 引入JPA中的@GeneratedValue注解用于指定主键的生成策略这里未明确指定策略将使用默认策略
import javax.persistence.Id;// 引入JPA中的@Id注解标识实体类的主键字段
// 使用@Data注解自动生成getter、setter等方法
@Data
// 使用@Entity注解标识这是一个JPA实体类
@Entity
public class ExamRecordLevel {
// 考试记录等级的ID是主键通过@Id注解标识并通过@GeneratedValue注解指定主键生成策略
@Id
@GeneratedValue
private Integer examRecordLevelId;
// 考试记录等级的名称,用于标识等级,如“优秀”、“良好”等
private String examRecordLevelName;
// 考试记录等级的描述,用于对等级进行更详细的说明
private String examRecordLevelDescription;
}
}

@ -6,20 +6,29 @@
***********************************************************/
package lsgwr.exam.exception;
import lsgwr.exam.enums.ResultEnum;
import lombok.Getter;
import lsgwr.exam.enums.ResultEnum;// 导入lsgwr.exam.enums包下的ResultEnum枚举类
import lombok.Getter;// 导入lombok库中的@Getter注解用于自动生成getter方法
// 使用@Getter注解自动为类中的字段生成getter方法
@Getter
// 定义一个名为ExamException的自定义异常类它继承自RuntimeException表示这是一个运行时异常
public class ExamException extends RuntimeException {
// 定义一个整型字段code用于存储异常码
private Integer code;
// 定义一个构造函数接收一个ResultEnum枚举类型的参数
// 这个构造函数通过调用父类RuntimeException的构造函数来设置异常信息
// 并通过枚举类型的参数来设置异常码
public ExamException(ResultEnum resultEnum) {
// 调用父类构造函数传入ResultEnum枚举中定义的异常信息
super(resultEnum.getMessage());
// 设置当前异常的异常码
this.code = resultEnum.getCode();
}
// 定义一个构造函数接收一个整型code和一个字符串message作为参数
// 这个构造函数允许直接传入异常码和异常信息来创建异常对象
public ExamException( Integer code, String message) {
// 调用父类构造函数,传入异常信息
super(message);
// 设置当前异常的异常码
this.code = code;
}
}
}

@ -6,8 +6,13 @@
***********************************************************/
package lsgwr.exam.repository;
import lsgwr.exam.entity.ExamRecordLevel;
import org.springframework.data.jpa.repository.JpaRepository;
import lsgwr.exam.entity.ExamRecordLevel;// 导入lsgwr.exam.entity包下的ExamRecordLevel实体类
import org.springframework.data.jpa.repository.JpaRepository;// 导入Spring Data JPA提供的JpaRepository接口
/**
* ExamRecordLevelRepositoryExamRecordLevel
* JpaRepositorySpring Data JPA访
* <ExamRecordLevel, Integer>
*/
public interface ExamRecordLevelRepository extends JpaRepository<ExamRecordLevel, Integer> {
}
}

@ -6,17 +6,21 @@
***********************************************************/
package lsgwr.exam.repository;
import lsgwr.exam.entity.ExamRecord;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import lsgwr.exam.entity.ExamRecord;// 导入lsgwr.exam.entity包下的ExamRecord实体类
import org.springframework.data.jpa.repository.JpaRepository;// 导入Spring Data JPA提供的JpaRepository接口
import java.util.List;// 导入Java的List集合类
/**
* ExamRecordRepositoryExamRecord
* JpaRepositorySpring Data JPA访
* <ExamRecord, String>String
*/
public interface ExamRecordRepository extends JpaRepository<ExamRecord, String> {
/**
*
* ID
*
* @param userId id
* @return
* @param userId ID
* @return List
*/
List<ExamRecord> findByExamJoinerIdOrderByExamJoinDateDesc(String userId);
}
}

@ -6,13 +6,24 @@
***********************************************************/
package lsgwr.exam.repository;
import lsgwr.exam.entity.Exam;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import lsgwr.exam.entity.Exam;// 导入lsgwr.exam.entity包下的Exam实体类
import org.springframework.data.jpa.repository.JpaRepository;// 导入Spring Data JPA提供的JpaRepository接口
import org.springframework.data.jpa.repository.Query;// 导入Spring Data JPA提供的@Query注解用于定义JPQL查询语句
import java.util.List;
import java.util.List;// 导入Java的List集合类
/**
* ExamRepositoryJpaRepositoryExam
* JpaRepositoryExamRepositorySpring Data JPA访
* <Exam, String>String
*/
public interface ExamRepository extends JpaRepository<Exam, String> {
/**
* 使JPQLExamupdateTime
* JpaRepositoryfindAll()
*
* @return ExamListupdateTime
*/
@Query("select e from Exam e order by e.updateTime desc")
List<Exam> findAll();
}

@ -4,119 +4,42 @@
* @date : 2019-05-28 08:05
* @email : liangshanguang2@gmail.com
***********************************************************/
package lsgwr.exam.service;
package lsgwr.exam.service;// 定义了接口所属的包名
import lsgwr.exam.entity.Exam;
import lsgwr.exam.entity.ExamRecord;
import lsgwr.exam.vo.*;
import java.util.HashMap;
import java.util.List;
import lsgwr.exam.entity.Exam;// 导入了实体类Exam可能包含考试的基本信息
import lsgwr.exam.entity.ExamRecord;// 导入了实体类ExamRecord可能包含考试记录的详细信息
import lsgwr.exam.vo.*;// 导入了VO对象VO是值对象用于在不同层之间传递数据
import java.util.HashMap;// 导入了HashMap用于存储键值对
import java.util.List;// 导入了List用于存储一系列对象
// 定义了一个名为ExamService的接口
public interface ExamService {
/**
*
*/
// 获取所有的问题列表
List<QuestionVo> getQuestionAll();
/**
*
*
* @param questionVo
*/
// 根据传入的问题实体更新问题和选项
QuestionVo updateQuestion(QuestionVo questionVo);
/**
*
*
* @param questionCreateVo
*/
// 创建问题
void questionCreate(QuestionCreateVo questionCreateVo);
/**
*
*
* @return
*/
// 获取问题的选项、分类和难度的下拉列表
QuestionSelectionVo getSelections();
/**
*
*
* @param id id
* @return VO
*/
// 根据问题ID获取问题详情
QuestionDetailVo getQuestionDetail(String id);
/**
*
*/
// 获取全部考试的列表
List<ExamVo> getExamAll();
/**
* 便
*
* @return
*/
// 获取所有问题的下拉列表,用于前端创建考试时筛选
ExamQuestionTypeVo getExamQuestionType();
/**
*
*
* @param examCreateVo
* @param userId id
* @return
*/
// 根据前端组装的参数创建考试
Exam create(ExamCreateVo examCreateVo, String userId);
/**
*
*
* @return
*/
// 获取考试卡片列表
List<ExamCardVo> getExamCardList();
/**
* id
*
* @param id exam
* @return VO
*/
// 根据考试ID获取考试详情
ExamDetailVo getExamDetail(String id);
/**
*
*
* @param userId
* @param examId
* @param answersMap
* @return
*/
// 根据用户提交的作答信息进行判分
ExamRecord judge(String userId, String examId, HashMap<String, List<String>> answersMap);
/**
* id
*
* @param userId id
* @return
*/
// 根据用户ID获取此用户的所有考试信息
List<ExamRecordVo> getExamRecordList(String userId);
/**
*
*
* @param recordId id
* @return
*/
// 获取指定某次考试记录的详情
RecordDetailVo getRecordDetail(String recordId);
/**
*
*
* @param examVo
* @param userId
* @return
*/
// 更新考试
Exam update(ExamVo examVo, String userId);
}

@ -6,23 +6,24 @@
***********************************************************/
package lsgwr.exam.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import lsgwr.exam.entity.*;
import lsgwr.exam.enums.QuestionEnum;
import lsgwr.exam.service.ExamService;
import lsgwr.exam.repository.*;
import lsgwr.exam.vo.*;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.*;
import cn.hutool.core.util.IdUtil;// Hutool工具类用于生成唯一ID等
import cn.hutool.core.util.StrUtil;// Hutool工具类用于字符串处理
import lsgwr.exam.entity.*;// 导入所有实体类
import lsgwr.exam.enums.QuestionEnum;// 导入枚举类
import lsgwr.exam.service.ExamService;// 导入服务接口
import lsgwr.exam.repository.*; // 导入所有仓库接口
import lsgwr.exam.vo.*;// 导入所有值对象View Object
import org.springframework.beans.BeanUtils;// Spring提供的Bean工具类用于属性复制
import org.springframework.stereotype.Service;// Spring提供的注解用于声明服务类
import javax.transaction.Transactional;// 事务注解,用于声明事务性方法
import java.util.*;// 导入Java集合框架相关类
// 使用@Service注解声明这是一个服务类用于实现业务逻辑
@Service
// 使用@Transactional注解声明这个服务类中的所有方法都运行在事务环境中
@Transactional
public class ExamServiceImpl implements ExamService {
// 注入所有需要的仓库接口
private final ExamRepository examRepository;
private final ExamRecordRepository examRecordRepository;
@ -38,8 +39,9 @@ public class ExamServiceImpl implements ExamService {
private final QuestionCategoryRepository questionCategoryRepository;
private final QuestionOptionRepository questionOptionRepository;
// 通过构造函数注入所有仓库接口
public ExamServiceImpl(QuestionRepository questionRepository, UserRepository userRepository, QuestionLevelRepository questionLevelRepository, QuestionTypeRepository questionTypeRepository, QuestionCategoryRepository questionCategoryRepository, QuestionOptionRepository questionOptionRepository, ExamRepository examRepository, ExamRecordRepository examRecordRepository) {
// 依次赋值给对应的成员变量
this.questionRepository = questionRepository;
this.userRepository = userRepository;
this.questionLevelRepository = questionLevelRepository;
@ -49,37 +51,43 @@ public class ExamServiceImpl implements ExamService {
this.examRepository = examRepository;
this.examRecordRepository = examRecordRepository;
}
// 实现获取所有问题的接口
@Override
public List<QuestionVo> getQuestionAll() {
// 从问题仓库中获取所有问题实体
List<Question> questionList = questionRepository.findAll();
// 将问题实体列表转换为问题值对象列表
return getQuestionVos(questionList);
}
// 私有方法,用于将问题实体列表转换为问题值对象列表
private List<QuestionVo> getQuestionVos(List<Question> questionList) {
// 需要自定义的question列表
// 初始化值对象列表
List<QuestionVo> questionVoList = new ArrayList<>();
// 循环完成每个属性的定制
// 遍历问题实体列表
for (Question question : questionList) {
// 将每个问题实体转换为值对象
QuestionVo questionVo = getQuestionVo(question);
// 将值对象添加到列表中
questionVoList.add(questionVo);
}
// 返回值对象列表
return questionVoList;
}
// 私有方法,用于将单个问题实体转换为问题值对象
private QuestionVo getQuestionVo(Question question) {
// 初始化值对象
QuestionVo questionVo = new QuestionVo();
// 先复制能复制的属性
// 复制问题实体中的属性到值对象
BeanUtils.copyProperties(question, questionVo);
// 设置问题的创建者
// 设置问题的创建者用户名通过用户ID从用户仓库中获取
questionVo.setQuestionCreator(
Objects.requireNonNull(
userRepository.findById(
question.getQuestionCreatorId()
).orElse(null)
).getUserUsername());
).getUserUsername());// 使用orElseThrow抛出异常避免使用requireNonNull和orElse(null)
// 设置问题的难度
// 设置问题的难度描述通过难度ID从难度仓库中获取
questionVo.setQuestionLevel(
Objects.requireNonNull(
questionLevelRepository.findById(
@ -87,7 +95,7 @@ public class ExamServiceImpl implements ExamService {
).orElse(null)
).getQuestionLevelDescription());
// 设置题目的类别,比如单选、多选、判断等
// 设置问题的类型描述通过类型ID从类型仓库中获取
questionVo.setQuestionType(
Objects.requireNonNull(
questionTypeRepository.findById(
@ -95,7 +103,7 @@ public class ExamServiceImpl implements ExamService {
).orElse(null)
).getQuestionTypeDescription());
// 设置题目分类,比如数学、语文、英语、生活、人文等
// 设置问题的分类名称通过分类ID从分类仓库中获取
questionVo.setQuestionCategory(
Objects.requireNonNull(
questionCategoryRepository.findById(
@ -104,54 +112,67 @@ public class ExamServiceImpl implements ExamService {
).getQuestionCategoryName()
);
// 选项的自定义Vo列表
// 初始化选项值对象列表
List<QuestionOptionVo> optionVoList = new ArrayList<>();
// 获得所有的选项列表
// 从选项仓库中获取所有选项实体
List<QuestionOption> optionList = questionOptionRepository.findAllById(
Arrays.asList(question.getQuestionOptionIds().split("-"))
);
// 获取所有的答案列表optionList中每个option的isAnswer选项
// 从选项仓库中获取所有答案选项实体
List<QuestionOption> answerList = questionOptionRepository.findAllById(
Arrays.asList(question.getQuestionAnswerOptionIds().split("-"))
);
// 根据选项和答案的id相同设置optionVo的isAnswer属性
// 遍历选项实体列表,转换为值对象,并设置答案属性
for (QuestionOption option : optionList) {
QuestionOptionVo optionVo = new QuestionOptionVo();
BeanUtils.copyProperties(option, optionVo);
// 判断当前选项是否是答案
for (QuestionOption answer : answerList) {
if (option.getQuestionOptionId().equals(answer.getQuestionOptionId())) {
optionVo.setAnswer(true);
break;// 找到答案后无需继续遍历
}
}
// 将选项值对象添加到列表中
optionVoList.add(optionVo);
}
// 设置的所有选项
// 设置题的所有选项值对象列表
questionVo.setQuestionOptionVoList(optionVoList);
// 返回问题值对象
return questionVo;
}
// 实现更新问题的接口
@Override
public QuestionVo updateQuestion(QuestionVo questionVo) {
// 1.把需要的属性都设置好
// 初始化答案ID字符串构建器
StringBuilder questionAnswerOptionIds = new StringBuilder();
// 初始化选项实体列表
List<QuestionOption> questionOptionList = new ArrayList<>();
// 获取问题值对象中的选项值对象列表
List<QuestionOptionVo> questionOptionVoList = questionVo.getQuestionOptionVoList();
// 获取选项值对象列表的大小
int size = questionOptionVoList.size();
// 遍历选项值对象列表
for (int i = 0; i < questionOptionVoList.size(); i++) {
// 获取当前选项值对象
QuestionOptionVo questionOptionVo = questionOptionVoList.get(i);
// 初始化选项实体
QuestionOption questionOption = new QuestionOption();
// 复制选项值对象中的属性到选项实体
BeanUtils.copyProperties(questionOptionVo, questionOption);
// 将选项实体添加到列表中
questionOptionList.add(questionOption);
// 判断当前选项是否是答案
if (questionOptionVo.getAnswer()) {
if (i != size - 1) {
// 把更新后的答案的id加上去,记得用-连到一起
// 如果不是最后一个答案则添加ID并用"-"连接
questionAnswerOptionIds.append(questionOptionVo.getQuestionOptionId()).append("-");
} else {
// 最后一个不需要用-连接
// 如果是最后一个答案则直接添加ID
questionAnswerOptionIds.append(questionOptionVo.getQuestionOptionId());
}
}
@ -159,70 +180,80 @@ public class ExamServiceImpl implements ExamService {
// 1.更新问题
Question question = questionRepository.findById(questionVo.getQuestionId()).orElse(null);
// 确保找到的问题不为null否则将抛出异常
assert question != null;
// 将传入的问题对象questionVo的属性复制到持久化对象question
BeanUtils.copyProperties(questionVo, question);
// 更新问题的答案选项ID字符串假设是以某种格式串联的ID字符串
question.setQuestionAnswerOptionIds(questionAnswerOptionIds.toString());
// 保存更新后的问题到数据库
questionRepository.save(question);
// 2.更新所有的option
questionOptionRepository.saveAll(questionOptionList);
// 返回更新后的问题,方便前端局部刷新
// 返回更新后的问题对象转换为VO对象方便前端局部刷新
return getQuestionVo(question);
}
// 重写问题创建方法
@Override
public void questionCreate(QuestionCreateVo questionCreateVo) {
// 问题创建
// 创建一个新的Question对象
Question question = new Question();
// 把能复制的属性都复制过来
// 使用BeanUtils工具类将questionCreateVo中的属性复制到question对象中
BeanUtils.copyProperties(questionCreateVo, question);
// 设置下questionOptionIds和questionAnswerOptionIds需要自己用Hutool生成下
// 初始化选项列表
List<QuestionOption> questionOptionList = new ArrayList<>();
// 获取问题创建对象中的选项列表
List<QuestionOptionCreateVo> questionOptionCreateVoList = questionCreateVo.getQuestionOptionCreateVoList();
// 遍历选项创建对象列表
for (QuestionOptionCreateVo questionOptionCreateVo : questionOptionCreateVoList) {
// 为每个选项创建一个新的QuestionOption对象
QuestionOption questionOption = new QuestionOption();
// 设置选项的内容
// 设置选项的内容
questionOption.setQuestionOptionContent(questionOptionCreateVo.getQuestionOptionContent());
// 设置选项的id
// 使用Hutool的IdUtil生成一个简单的UUID作为选项的ID
questionOption.setQuestionOptionId(IdUtil.simpleUUID());
// 将选项添加到列表中
questionOptionList.add(questionOption);
}
// 把选项都存起来然后才能用于下面设置Question的questionOptionIds和questionAnswerOptionIds
// 将所有选项保存到数据库
questionOptionRepository.saveAll(questionOptionList);
// 初始化选项ID和答案选项ID的字符串
String questionOptionIds = "";
String questionAnswerOptionIds = "";
// 经过上面的saveAll方法所有的option的主键id都已经持久化了
// 遍历选项创建对象列表,获取保存后的选项对象
for (int i = 0; i < questionOptionCreateVoList.size(); i++) {
// 获取指定选项
QuestionOptionCreateVo questionOptionCreateVo = questionOptionCreateVoList.get(i);
// 获取保存后的指定对象
QuestionOption questionOption = questionOptionList.get(i);
// 拼接选项ID
questionOptionIds += questionOption.getQuestionOptionId() + "-";
if (questionOptionCreateVo.getAnswer()) {
// 如果是答案的话
questionAnswerOptionIds += questionOption.getQuestionOptionId() + "-";
}
}
// 把字符串最后面的"-"给去掉
// 去除选项ID和答案选项ID字符串末尾的"-"
questionAnswerOptionIds = replaceLastSeparator(questionAnswerOptionIds);
questionOptionIds = replaceLastSeparator(questionOptionIds);
// 设置选项id组成的字符串
// 设置问题对象的选项ID和答案选项ID
question.setQuestionOptionIds(questionOptionIds);
// 设置答案选项id组成的字符串
question.setQuestionAnswerOptionIds(questionAnswerOptionIds);
// 自己生成问题的id
// 为问题生成一个UUID作为ID
question.setQuestionId(IdUtil.simpleUUID());
// 先把创建时间和更新时间每次都取当前时间吧
// 设置问题的创建时间和更新时间为当前时间
question.setCreateTime(new Date());
question.setUpdateTime(new Date());
// 保存问题到数据库
// 将问题保存到数据库
questionRepository.save(question);
}
// 重写获取选择项的方法
@Override
public QuestionSelectionVo getSelections() {
// 创建一个新的QuestionSelectionVo对象
QuestionSelectionVo questionSelectionVo = new QuestionSelectionVo();
// 设置问题分类列表、问题等级列表和问题类型列表
questionSelectionVo.setQuestionCategoryList(questionCategoryRepository.findAll());
questionSelectionVo.setQuestionLevelList(questionLevelRepository.findAll());
questionSelectionVo.setQuestionTypeList(questionTypeRepository.findAll());
@ -230,27 +261,27 @@ public class ExamServiceImpl implements ExamService {
return questionSelectionVo;
}
/**
* split
*
* @param str
* @return
*/
// 去除字符串末尾的"-"的方法
public static String trimMiddleLine(String str) {
// 如果字符串的最后一个字符是"-",则去除它
if (str.charAt(str.length() - 1) == '-') {
str = str.substring(0, str.length() - 1);
}
return str;
}
// 注意原代码中有一个方法名为replaceLastSeparator但提供的实现是trimMiddleLine这里假设它们功能相同
@Override
// 重写获取问题详情的方法
public QuestionDetailVo getQuestionDetail(String id) {
// 根据ID从数据库获取问题对象
Question question = questionRepository.findById(id).orElse(null);
// 创建一个新的QuestionDetailVo对象
QuestionDetailVo questionDetailVo = new QuestionDetailVo();
// 设置问题详情对象的ID、名称和描述
questionDetailVo.setId(id);
questionDetailVo.setName(question.getQuestionName());
questionDetailVo.setDescription(question.getQuestionDescription());
// 问题类型,单选题/多选题/判断题
// 获取问题类型描述
questionDetailVo.setType(
Objects.requireNonNull(
questionTypeRepository.findById(
@ -258,30 +289,35 @@ public class ExamServiceImpl implements ExamService {
).orElse(null)
).getQuestionTypeDescription()
);
// 获取当前问题的选项
// 获取问题的选项ID字符串去除末尾的"-",然后分割成数组
String optionIdsStr = trimMiddleLine(question.getQuestionOptionIds());
String[] optionIds = optionIdsStr.split("-");
// 获取选项列表
// 根据选项ID数组从数据库获取选项列表
List<QuestionOption> optionList = questionOptionRepository.findAllById(Arrays.asList(optionIds));
// 设置问题详情对象的选项列表
questionDetailVo.setOptions(optionList);
return questionDetailVo;
}
// 重写获取所有考试的方法
@Override
public List<ExamVo> getExamAll() {
// 从数据库获取所有考试对象
List<Exam> examList = examRepository.findAll();
// 将考试对象列表转换为ExamVo列表
return getExamVos(examList);
}
// 定义一个私有方法接收一个Exam对象的列表作为参数返回一个ExamVo对象的列表
private List<ExamVo> getExamVos(List<Exam> examList) {
// 需要自定义的exam列表
// 初始化一个空的ExamVo列表用于存放转换后的对象
List<ExamVo> examVoList = new ArrayList<>();
// 循环完成每个属性的定制
// 遍历传入的Exam列表
for (Exam exam : examList) {
// 为每个Exam对象创建一个对应的ExamVo对象
ExamVo examVo = new ExamVo();
// 先尽量复制能复制的所有属性
// 使用BeanUtils工具类将Exam对象的属性复制到ExamVo对象中对于名称和类型相同的属性
BeanUtils.copyProperties(exam, examVo);
// 设置问题的创建者
// 通过userRepository查找考试创建者的信息并设置到ExamVo对象的相应属性上
// 这里使用了Optional的requireNonNull方法确保找到的User对象不为null
examVo.setExamCreator(
Objects.requireNonNull(
userRepository.findById(
@ -290,385 +326,494 @@ public class ExamServiceImpl implements ExamService {
).getUserUsername()
);
// 获取所有单选题列表并赋值到ExamVo的属性ExamQuestionSelectVoRadioList
// 初始化单选题的列表并设置到ExamVo对象的相应属性
List<ExamQuestionSelectVo> radioQuestionVoList = new ArrayList<>();
// 根据考试中的单选题ID字符串分割成ID列表并通过questionRepository查找对应的Question对象列表
List<Question> radioQuestionList = questionRepository.findAllById(
Arrays.asList(exam.getExamQuestionIdsRadio().split("-"))
);
// 遍历单选题列表为每个Question对象创建一个对应的ExamQuestionSelectVo对象并设置选中状态为true
for (Question question : radioQuestionList) {
ExamQuestionSelectVo radioQuestionVo = new ExamQuestionSelectVo();
BeanUtils.copyProperties(question, radioQuestionVo);
radioQuestionVo.setChecked(true); // 考试中的问题肯定被选中的
radioQuestionVo.setChecked(true);// 假设考试中的问题在初始化时都被视为已选中
radioQuestionVoList.add(radioQuestionVo);
}
examVo.setExamQuestionSelectVoRadioList(radioQuestionVoList);
// 获取所有多选题列表并赋值到ExamVo的属性ExamQuestionSelectVoCheckList上
// 创建一个用于存储考试选择题Vo对象的列表
List<ExamQuestionSelectVo> checkQuestionVoList = new ArrayList<>();
// 从数据库中获取所有需要检查(可能是单选或多选,但此处标记为检查题)的问题列表
// 通过分割考试对象中的考试问题ID字符串使用"-"作为分隔符并将结果转换为List<Long>
// 然后调用questionRepository的findAllById方法获取Question对象列表
List<Question> checkQuestionList = questionRepository.findAllById(
Arrays.asList(exam.getExamQuestionIdsCheck().split("-"))
);
// 遍历获取到的问题列表
for (Question question : checkQuestionList) {
// 为每个问题创建一个新的ExamQuestionSelectVo对象
ExamQuestionSelectVo checkQuestionVo = new ExamQuestionSelectVo();
// 使用BeanUtils工具类将Question对象的属性复制到新的ExamQuestionSelectVo对象中
BeanUtils.copyProperties(question, checkQuestionVo);
checkQuestionVo.setChecked(true); // 考试中的问题肯定被选中的
// 设置选中状态为true表示这个问题在考试中是已经被选中的
checkQuestionVo.setChecked(true); // 考试中问题肯定被选中的
// 将设置好的Vo对象添加到列表中
checkQuestionVoList.add(checkQuestionVo);
}
examVo.setExamQuestionSelectVoCheckList(checkQuestionVoList);
// 获取所有多选题列表并赋值到ExamVo的属性ExamQuestionSelectVoJudgeList上
// 从数据库中获取所有需要判断的问题列表
// 通过分割考试对象中的考试问题ID字符串使用"-"作为分隔符并将结果转换为List<Long>
// 然后调用questionRepository的findAllById方法获取Question对象列表
List<ExamQuestionSelectVo> judgeQuestionVoList = new ArrayList<>();
List<Question> judgeQuestionList = questionRepository.findAllById(
Arrays.asList(exam.getExamQuestionIdsJudge().split("-"))
);
// 遍历获取到的问题列表
for (Question question : judgeQuestionList) {
// 为每个问题创建一个新的ExamQuestionSelectVo对象
ExamQuestionSelectVo judgeQuestionVo = new ExamQuestionSelectVo();
// 使用BeanUtils工具类将Question对象的属性复制到新的ExamQuestionSelectVo对象中
BeanUtils.copyProperties(question, judgeQuestionVo);
judgeQuestionVo.setChecked(true); // 考试中的问题肯定被选中的
// 设置选中状态为true
judgeQuestionVo.setChecked(true);
// 将设置好的Vo对象添加到列表中
judgeQuestionVoList.add(judgeQuestionVo);
}
examVo.setExamQuestionSelectVoJudgeList(judgeQuestionVoList);
// 把examVo加到examVoList
// 将处理好的ExamVo对象添加到列表
examVoList.add(examVo);
}
// 返回处理好的ExamVo列表
return examVoList;
}
// 重写父类或接口中的方法,用于获取考试问题类型的详细信息
@Override
public ExamQuestionTypeVo getExamQuestionType() {
// 初始化一个ExamQuestionTypeVo对象用于存储考试题型信息
ExamQuestionTypeVo examQuestionTypeVo = new ExamQuestionTypeVo();
// 获取所有单选题列表并赋值到ExamVo的属性ExamQuestionSelectVoRadioList上
// 获取所有单选题列表并赋值到ExamVo的属性ExamQuestionSelectVoRadioList上
List<ExamQuestionSelectVo> radioQuestionVoList = new ArrayList<>();
// 从数据库中获取所有单选题通过题型ID筛选这里的QuestionEnum.RADIO.getId()返回单选题的ID
List<Question> radioQuestionList = questionRepository.findByQuestionTypeId(QuestionEnum.RADIO.getId());
// 遍历所有单选题
for (Question question : radioQuestionList) {
// 为每个单选题创建一个ExamQuestionSelectVo对象
ExamQuestionSelectVo radioQuestionVo = new ExamQuestionSelectVo();
// 将Question对象的属性复制到radioQuestionVo对象中
BeanUtils.copyProperties(question, radioQuestionVo);
// 将复制后的对象添加到单选题的列表中
radioQuestionVoList.add(radioQuestionVo);
}
// 将单选题列表设置到ExamQuestionTypeVo对象的相应属性中
examQuestionTypeVo.setExamQuestionSelectVoRadioList(radioQuestionVoList);
// 获取所有多选题列表并赋值到ExamVo的属性ExamQuestionSelectVoCheckList上
// 初始化多选题的列表
List<ExamQuestionSelectVo> checkQuestionVoList = new ArrayList<>();
// 从数据库中获取所有多选题通过题型ID筛选这里的QuestionEnum.CHECK.getId()返回多选题的ID
List<Question> checkQuestionList = questionRepository.findByQuestionTypeId(QuestionEnum.CHECK.getId());
// 遍历所有多选题
for (Question question : checkQuestionList) {
// 为每个多选题创建一个ExamQuestionSelectVo对象
ExamQuestionSelectVo checkQuestionVo = new ExamQuestionSelectVo();
// 将Question对象的属性复制到checkQuestionVo对象中
BeanUtils.copyProperties(question, checkQuestionVo);
// 将复制后的对象添加到多选题的列表中
checkQuestionVoList.add(checkQuestionVo);
}
// 将多选题列表设置到ExamQuestionTypeVo对象的相应属性中
examQuestionTypeVo.setExamQuestionSelectVoCheckList(checkQuestionVoList);
// 获取所有多选题列表并赋值到ExamVo的属性ExamQuestionSelectVoJudgeList上
// 初始化判断题的列表
List<ExamQuestionSelectVo> judgeQuestionVoList = new ArrayList<>();
// 从数据库中获取所有判断题通过题型ID筛选这里的QuestionEnum.JUDGE.getId()返回判断题的ID
List<Question> judgeQuestionList = questionRepository.findByQuestionTypeId(QuestionEnum.JUDGE.getId());
// 遍历所有判断题
for (Question question : judgeQuestionList) {
// 为每个判断题创建一个ExamQuestionSelectVo对象
ExamQuestionSelectVo judgeQuestionVo = new ExamQuestionSelectVo();
// 将Question对象的属性复制到judgeQuestionVo对象中
BeanUtils.copyProperties(question, judgeQuestionVo);
// 将复制后的对象添加到判断题的列表中
judgeQuestionVoList.add(judgeQuestionVo);
}
// 将判断题列表设置到ExamQuestionTypeVo对象的相应属性中
examQuestionTypeVo.setExamQuestionSelectVoJudgeList(judgeQuestionVoList);
// 返回包含所有题型信息的ExamQuestionTypeVo对象
return examQuestionTypeVo;
}
@Override
public Exam create(ExamCreateVo examCreateVo, String userId) {
// 在线考试系统创建
// 创建一个新的考试对象
Exam exam = new Exam();
// 将前端传来的考试创建对象ExamCreateVo的属性复制到新创建的考试对象中
BeanUtils.copyProperties(examCreateVo, exam);
// 为考试生成一个唯一的ID
exam.setExamId(IdUtil.simpleUUID());
// 设置考试的创建者ID
exam.setExamCreatorId(userId);
// 设置考试的创建时间和更新时间(这里都设置为当前时间)
exam.setCreateTime(new Date());
exam.setUpdateTime(new Date());
// Todo:这两个日志后面是要在前端传入的,这里暂时定为当前日期
// Todo: 考试的开始和结束日期应该是前端传入的,这里暂时使用当前日期作为占位符
exam.setExamStartDate(new Date());
exam.setExamEndDate(new Date());
// 初始化字符串用于拼接单选、多选和判断题的ID
String radioIdsStr = "";
String checkIdsStr = "";
String judgeIdsStr = "";
// 从前端传来的对象中获取单选、多选和判断题的列表
List<ExamQuestionSelectVo> radios = examCreateVo.getRadios();
List<ExamQuestionSelectVo> checks = examCreateVo.getChecks();
List<ExamQuestionSelectVo> judges = examCreateVo.getJudges();
// 初始化计数器,用于记录每种类型题目的数量
int radioCnt = 0, checkCnt = 0, judgeCnt = 0;
// 遍历单选题的列表拼接被选中的题目ID并计数
for (ExamQuestionSelectVo radio : radios) {
if (radio.getChecked()) {
radioIdsStr += radio.getQuestionId() + "-";
radioCnt++;
}
}
// 替换最后一个分隔符,避免字符串末尾出现多余的分隔符
radioIdsStr = replaceLastSeparator(radioIdsStr);
// 遍历多选题的列表拼接被选中的题目ID并计数
for (ExamQuestionSelectVo check : checks) {
if (check.getChecked()) {
checkIdsStr += check.getQuestionId() + "-";
checkCnt++;
}
}
// 替换最后一个分隔符
checkIdsStr = replaceLastSeparator(checkIdsStr);
// 遍历判断题的列表拼接被选中的题目ID并计数
for (ExamQuestionSelectVo judge : judges) {
if (judge.getChecked()) {
judgeIdsStr += judge.getQuestionId() + "-";
judgeCnt++;
}
}
// 替换最后一个分隔符
judgeIdsStr = replaceLastSeparator(judgeIdsStr);
// 将所有被选中的题目ID拼接成一个字符串并设置到考试对象中
exam.setExamQuestionIds(radioIdsStr + "-" + checkIdsStr + "-" + judgeIdsStr);
// 设置各个题目的id
// 分别设置单选、多选和判断题的ID串到考试对象中
exam.setExamQuestionIdsRadio(radioIdsStr);
exam.setExamQuestionIdsCheck(checkIdsStr);
exam.setExamQuestionIdsJudge(judgeIdsStr);
// 计算总分数
// 计算考试的总分数
// 总分数 = 单选题数量 * 单选题每题分数 + 多选题数量 * 多选题每题分数 + 判断题数量 * 判断题每题分数
int examScore = radioCnt * exam.getExamScoreRadio() + checkCnt * exam.getExamScoreCheck() + judgeCnt * exam.getExamScoreJudge();
exam.setExamScore(examScore);
// 将考试对象保存到数据库中
examRepository.save(exam);
// 返回创建好的考试对象
return exam;
}
// 重写update方法用于更新考试信息
@Override
public Exam update(ExamVo examVo, String userId) {
// 创建一个新的Exam对象
Exam exam = new Exam();
// 将examVo对象的属性复制到exam对象中
BeanUtils.copyProperties(examVo, exam);
exam.setExamCreatorId(userId); // 考试的更新人为最新的创建人
exam.setUpdateTime(new Date()); // 考试的更新日期要记录下
// 设置考试的更新人为当前的用户ID
exam.setExamCreatorId(userId);
// 记录考试的更新日期为当前时间
exam.setUpdateTime(new Date());
// 初始化字符串变量用于存储不同类型的题目ID
String radioIdsStr = "";
String checkIdsStr = "";
String judgeIdsStr = "";
// 获取单选、多选和判断题的列表
List<ExamQuestionSelectVo> radios = examVo.getExamQuestionSelectVoRadioList();
List<ExamQuestionSelectVo> checks = examVo.getExamQuestionSelectVoCheckList();
List<ExamQuestionSelectVo> judges = examVo.getExamQuestionSelectVoJudgeList();
// 初始化计数器,用于统计每种类型题目的数量
int radioCnt = 0, checkCnt = 0, judgeCnt = 0;
// 遍历单选题目列表拼接被选中的题目ID
for (ExamQuestionSelectVo radio : radios) {
if (radio.getChecked()) {
radioIdsStr += radio.getQuestionId() + "-";
radioCnt++;
}
}
// 移除最后一个多余的"-"
radioIdsStr = replaceLastSeparator(radioIdsStr);
// 遍历多选题目列表拼接被选中的题目ID
for (ExamQuestionSelectVo check : checks) {
if (check.getChecked()) {
checkIdsStr += check.getQuestionId() + "-";
checkCnt++;
}
}
// 移除最后一个多余的"-"
checkIdsStr = replaceLastSeparator(checkIdsStr);
// 遍历判断题目列表拼接被选中的题目ID
for (ExamQuestionSelectVo judge : judges) {
if (judge.getChecked()) {
judgeIdsStr += judge.getQuestionId() + "-";
judgeCnt++;
}
}
// 移除最后一个多余的"-"
judgeIdsStr = replaceLastSeparator(judgeIdsStr);
// 设置考试的题目ID字符串包括单选、多选和判断题
exam.setExamQuestionIds(radioIdsStr + "-" + checkIdsStr + "-" + judgeIdsStr);
// 设置各个题目的id
// 分别设置各类题目的ID字符串
exam.setExamQuestionIdsRadio(radioIdsStr);
exam.setExamQuestionIdsCheck(checkIdsStr);
exam.setExamQuestionIdsJudge(judgeIdsStr);
// 计算总分数
// 计算考试的总分数
int examScore = radioCnt * exam.getExamScoreRadio() + checkCnt * exam.getExamScoreCheck() + judgeCnt * exam.getExamScoreJudge();
exam.setExamScore(examScore);
// 保存更新后的考试信息
examRepository.save(exam);
// 返回更新后的考试信息
return exam;
}
// 重写getExamCardList方法用于获取所有考试的信息列表
@Override
public List<ExamCardVo> getExamCardList() {
// 从数据库中获取所有的考试信息
List<Exam> examList = examRepository.findAll();
// 创建一个新的列表,用于存储转换后的考试卡片信息
List<ExamCardVo> examCardVoList = new ArrayList<>();
// 遍历考试信息列表,将每个考试信息转换为考试卡片信息
for (Exam exam : examList) {
ExamCardVo examCardVo = new ExamCardVo();
BeanUtils.copyProperties(exam, examCardVo);
// 将转换后的考试卡片信息添加到列表中
examCardVoList.add(examCardVo);
}
// 返回考试卡片信息列表
return examCardVoList;
}
// 重写getExamDetail方法用于获取指定考试的详细信息
@Override
public ExamDetailVo getExamDetail(String id) {
// 根据ID从数据库中获取考试信息
Exam exam = examRepository.findById(id).orElse(null);
// 创建一个新的考试详细信息对象
ExamDetailVo examDetailVo = new ExamDetailVo();
// 设置考试信息
examDetailVo.setExam(exam);
// 确保考试信息不为空这里使用assert进行断言但通常建议使用更优雅的异常处理
assert exam != null;
// 将考试的各类题目ID字符串分割为数组并设置到考试详细信息对象中
examDetailVo.setRadioIds(exam.getExamQuestionIdsRadio().split("-"));
examDetailVo.setCheckIds(exam.getExamQuestionIdsCheck().split("-"));
examDetailVo.setJudgeIds(exam.getExamQuestionIdsJudge().split("-"));
// 返回考试详细信息对象
return examDetailVo;
}
// 重写judge方法用于判断用户考试的得分情况
@Override
public ExamRecord judge(String userId, String examId, HashMap<String, List<String>> answersMap) {
// 开始考试判分~~~
// 1.首先获取考试对象和选项数组
ExamDetailVo examDetailVo = getExamDetail(examId);
Exam exam = examDetailVo.getExam();
// 2.然后获取该考试下所有题目信息
List<String> questionIds = new ArrayList<>();
// 2.1 题目id的数组
List<String> radioIdList = Arrays.asList(examDetailVo.getRadioIds());
List<String> checkIdList = Arrays.asList(examDetailVo.getCheckIds());
List<String> judgeIdList = Arrays.asList(examDetailVo.getJudgeIds());
questionIds.addAll(radioIdList);
questionIds.addAll(checkIdList);
questionIds.addAll(judgeIdList);
// 2.2 每种题目的分数
int radioScore = exam.getExamScoreRadio();
int checkScore = exam.getExamScoreCheck();
int judgeScore = exam.getExamScoreJudge();
// 2.3 根据问题id的数组拿到所有的问题对象供下面步骤用
List<Question> questionList = questionRepository.findAllById(questionIds);
Map<String, Question> questionMap = new HashMap<>();
// 开始考试判分~~~
// 1. 获取考试详情对象和考试对象的选项数组
ExamDetailVo examDetailVo = getExamDetail(examId);// 获取考试详情
Exam exam = examDetailVo.getExam();// 获取考试对象
// 2.然后获取该考试下所有题目信息
List<String> questionIds = new ArrayList<>();// 存储题目ID的列表
// 2.1 获取各种题型的题目ID列表
List<String> radioIdList = Arrays.asList(examDetailVo.getRadioIds());// 单选题ID列表
List<String> checkIdList = Arrays.asList(examDetailVo.getCheckIds());// 多选题ID列表
List<String> judgeIdList = Arrays.asList(examDetailVo.getJudgeIds());// 判断题ID列表
questionIds.addAll(radioIdList);// 将单选题ID添加到总列表
questionIds.addAll(checkIdList);// 将多选题ID添加到总列表
questionIds.addAll(judgeIdList);// 将判断题ID添加到总列表
// 2.2 获取每种题型的分数
int radioScore = exam.getExamScoreRadio();// 单选题每题分数
int checkScore = exam.getExamScoreCheck();// 多选题每题分数
int judgeScore = exam.getExamScoreJudge();// 判断题每题分数
// 2.3 根据题目ID获取所有题目对象便于后续操作
List<Question> questionList = questionRepository.findAllById(questionIds);// 查询所有题目
Map<String, Question> questionMap = new HashMap<>();// 使用Map存储题目ID和题目对象的映射
for (Question question : questionList) {
questionMap.put(question.getQuestionId(), question);
questionMap.put(question.getQuestionId(), question);// 将题目添加到Map中
}
// 3.根据正确答案和用户作答信息进行判分
Set<String> questionIdsAnswer = answersMap.keySet();
// 存储当前考试每个题目的得分情况
// 3. 根据正确答案,用户作答信息进行判分
Set<String> questionIdsAnswer = answersMap.keySet();// 获取用户作答的题目ID集合
// 存储每个题目的得分情况
Map<String, Integer> judgeMap = new HashMap<>();
// 考生作答地每个题目的选项(题目和题目之间用$分隔,题目有多个选项地话用-分隔,题目和选项之间用_分隔),用于查看考试详情
// 例子题目1的id_作答选项1-作答选项2&题目2的id_作答选项1&题目3_作答选项1-作答选项2-作答选项3
// 用于构建用户作答选项的字符串,用于查看考试详情
StringBuilder answerOptionIdsSb = new StringBuilder();
// 用户此次考试的总分
// 用户考试的总分
int totalScore = 0;
// 遍历用户作答的题目ID
for (String questionId : questionIdsAnswer) {
// 获取用户作答地这个题的答案信息
// 获取题目对象
Question question = questionMap.get(questionId);
// 获取答案选项
// 获取题目正确答案选项ID并处理格式
String questionAnswerOptionIds = replaceLastSeparator(question.getQuestionAnswerOptionIds());
List<String> questionAnswerOptionIdList = Arrays.asList(questionAnswerOptionIds.split("-"));
Collections.sort(questionAnswerOptionIdList);
String answerStr = listConcat(questionAnswerOptionIdList);
// 获取用户作答
Collections.sort(questionAnswerOptionIdList);// 对选项进行排序
String answerStr = listConcat(questionAnswerOptionIdList);// 将选项列表转换为字符串
// 获取用户作答的选项ID列表并处理格式
List<String> questionUserOptionIdList = answersMap.get(questionId);
Collections.sort(questionUserOptionIdList);
String userStr = listConcat(questionUserOptionIdList);
// 判断questionAnswerOptionIds和answersMap里面的答案是否相等
Collections.sort(questionUserOptionIdList);// 对选项进行排序
String userStr = listConcat(questionUserOptionIdList);// 将选项列表转换为字符串
// 判断用户作答是否正确
if (answerStr.equals(userStr)) {
// 说明题目作答正确,下面根据题型给分
// 作答正确,根据题型设置分数
int score = 0;
if (radioIdList.contains(questionId)) {
score = radioScore;
score = radioScore;// 单选题分数
}
if (checkIdList.contains(questionId)) {
score = checkScore;
score = checkScore;// 多选题分数
}
if (judgeIdList.contains(questionId)) {
score = judgeScore;
score = judgeScore;// 判断题分数
}
// 累计本次考试得
// 累加总
totalScore += score;
// True代表题目答对
// 记录答案正确及用户作答选项
answerOptionIdsSb.append(questionId + "@True_" + userStr + "$");
judgeMap.put(questionId, score);
judgeMap.put(questionId, score);// 记录题目得分
} else {
// 说明题目作答错误,直接判零分,False代表题目答错
// 作答错误记0分
answerOptionIdsSb.append(questionId + "@False_" + userStr + "$");
judgeMap.put(questionId, 0);
judgeMap.put(questionId, 0);// 记录题目得分为0
}
}
// 4.计算得分记录本次考试结果存到ExamRecord
ExamRecord examRecord = new ExamRecord();
examRecord.setExamRecordId(IdUtil.simpleUUID());
examRecord.setExamId(examId);
// 注意去掉最后可能有的&_-
// 4. 计算得分,记录考试结果,并保存到数据库
ExamRecord examRecord = new ExamRecord();// 创建考试记录对象
examRecord.setExamRecordId(IdUtil.simpleUUID());// 设置考试记录ID
examRecord.setExamId(examId);// 设置考试ID
// 设置用户作答选项字符串,注意处理格式
examRecord.setAnswerOptionIds(replaceLastSeparator(answerOptionIdsSb.toString()));
examRecord.setExamJoinerId(userId);
examRecord.setExamJoinDate(new Date());
examRecord.setExamJoinScore(totalScore);
examRecordRepository.save(examRecord);
return examRecord;
examRecord.setExamJoinerId(userId);// 设置考生ID
examRecord.setExamJoinDate(new Date());// 设置考试日期
examRecord.setExamJoinScore(totalScore);// 设置考试总分
examRecordRepository.save(examRecord);// 保存考试记录到数据库
return examRecord;// 返回考试记录对象
}
// 重写方法,用于获取指定用户的考试记录列表
@Override
public List<ExamRecordVo> getExamRecordList(String userId) {
// 获取指定用户下的考试记录列表
// 通过用户ID从数据库中查询该用户参与的考试记录按考试加入日期降序排列
List<ExamRecord> examRecordList = examRecordRepository.findByExamJoinerIdOrderByExamJoinDateDesc(userId);
// 创建一个用于存放转换后的考试记录视图对象的列表
List<ExamRecordVo> examRecordVoList = new ArrayList<>();
// 遍历查询到的考试记录
for (ExamRecord examRecord : examRecordList) {
ExamRecordVo examRecordVo = new ExamRecordVo();
ExamRecordVo examRecordVo = new ExamRecordVo();// 创建一个新的考试记录视图对象
// 根据考试记录中的考试ID查询考试详情
Exam exam = examRepository.findById(examRecord.getExamId()).orElse(null);
// 设置考试详情到视图对象中
examRecordVo.setExam(exam);
// 根据用户ID查询用户详情
User user = userRepository.findById(userId).orElse(null);
// 设置用户详情到视图对象中
examRecordVo.setUser(user);
// 设置考试记录到视图对象中
examRecordVo.setExamRecord(examRecord);
// 将视图对象添加到列表中
examRecordVoList.add(examRecordVo);
}
return examRecordVoList;
}
// 重写方法,用于获取特定考试记录的详细信息
@Override
public RecordDetailVo getRecordDetail(String recordId) {
// 获取考试详情的封装对象
// 通过记录ID从数据库中查询考试记录
ExamRecord record = examRecordRepository.findById(recordId).orElse(null);
// 创建一个新的记录详情视图对象
RecordDetailVo recordDetailVo = new RecordDetailVo();
// 设置考试记录到视图对象中
recordDetailVo.setExamRecord(record);
// 用户的答案,需要解析
// 初始化用于存储用户答案和结果的哈希映射
HashMap<String, List<String>> answersMap = new HashMap<>();
HashMap<String, String> resultsMap = new HashMap<>();
// 确保查询到的考试记录不为空
assert record != null;
// 获取用户答案字符串,以$分隔每个题目的答案
String answersStr = record.getAnswerOptionIds();
// $分隔题目,因为$在正则中有特殊用途(行尾),所以需要括起来
// 按$分割字符串,得到每个题目的答案
String[] questionArr = answersStr.split("[$]");
// 遍历每个题目的答案
for (String questionStr : questionArr) {
System.out.println(questionStr);
// 区分开题目标题和选项
// 题目字符串格式为:题目标题@结果_选项1-选项2-...,分割得到题目和选项
String[] questionTitleResultAndOption = questionStr.split("_");
String[] questionTitleAndResult = questionTitleResultAndOption[0].split("@");
String[] questionOptions = questionTitleResultAndOption[1].split("-");
// 题目:答案选项
// 将题目和答案选项存入answersMap
answersMap.put(questionTitleAndResult[0], Arrays.asList(questionOptions));
// 题目True / False
// 将题目和结果True/False存入resultsMap
resultsMap.put(questionTitleAndResult[0], questionTitleAndResult[1]);
}
// 设置答案和结果映射到视图对象中
recordDetailVo.setAnswersMap(answersMap);
recordDetailVo.setResultsMap(resultsMap);
// 下面再计算正确答案的map
// 获取考试详情用于获取所有题目的ID
ExamDetailVo examDetailVo = getExamDetail(record.getExamId());
// 初始化一个列表用于存放所有题目的ID
List<String> questionIdList = new ArrayList<>();
// 将单选、多选、判断题的ID添加到列表中
questionIdList.addAll(Arrays.asList(examDetailVo.getRadioIds()));
questionIdList.addAll(Arrays.asList(examDetailVo.getCheckIds()));
questionIdList.addAll(Arrays.asList(examDetailVo.getJudgeIds()));
// 获取所有的问题对象
// 根据题目ID列表从数据库中查询所有题目
List<Question> questionList = questionRepository.findAllById(questionIdList);
// 初始化一个哈希映射用于存储正确答案
HashMap<String, List<String>> answersRightMap = new HashMap<>();
// 遍历所有题目,获取每个题目的正确答案
for (Question question : questionList) {
// 记得去掉最后可能出现的特殊字符
// 去除答案字符串末尾可能出现的特殊字符(如多余的"-"
String questionAnswerOptionIdsStr = replaceLastSeparator(question.getQuestionAnswerOptionIds());
// 分割字符串得到正确答案
String[] questionAnswerOptionIds = questionAnswerOptionIdsStr.split("-");
// 将题目ID和正确答案存入answersRightMap
answersRightMap.put(question.getQuestionId(), Arrays.asList(questionAnswerOptionIds));
}
// 设置正确答案映射到视图对象中
recordDetailVo.setAnswersRightMap(answersRightMap);
// 返回记录详情视图对象
return recordDetailVo;
}
/**
* -
* -_$
*
* @param str
* @return -
* @param str
* @return
*/
private String replaceLastSeparator(String str) {
// 获取字符串的最后一个字符。
String lastChar = str.substring(str.length() - 1);
// 题目和题目之间用$分隔,题目有多个选项地话用-分隔,题目和选项之间用_分隔
// 检查最后一个字符是否是分隔符(-、_或$)。
if ("-".equals(lastChar) || "_".equals(lastChar) || "$".equals(lastChar)) {
// 如果最后一个字符是分隔符则使用StrUtil.sub方法移除它。
str = StrUtil.sub(str, 0, str.length() - 1);
}
// 返回处理后的字符串。
return str;
}
/**
* -
* --
*
* @param strList
* @return -
* @param strList
* @return -
*/
private String listConcat(List<String> strList) {
// 创建一个StringBuilder对象用于高效地构建最终的字符串
StringBuilder sb = new StringBuilder();
// 遍历字符串列表。
for (String str : strList) {
// 将当前字符串添加到StringBuilder中。
sb.append(str);
// 在当前字符串后添加一个-作为分隔符。
sb.append("-");
}
// 调用replaceLastSeparator方法移除StringBuilder中拼接字符串的最后一个-。
return replaceLastSeparator(sb.toString());
}
}

@ -6,24 +6,24 @@
***********************************************************/
package lsgwr.exam.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonProperty;// 导入了Jackson库中的JsonProperty注解用于指定JSON属性名
import lombok.Data;// 导入了Lombok库中的@Data注解用于自动生成getter和setter方法
@Data
public class ExamCardVo {
@JsonProperty("id")
private String examId;
private String examId;// 考试的唯一标识符
@JsonProperty("title")
private String examName;
private String examName;// 考试的名称
@JsonProperty("avatar")
private String examAvatar;
private String examAvatar; // 考试的头像或图标
@JsonProperty("content")
private String examDescription;
private String examDescription; // 考试的描述或内容
@JsonProperty("score")
private Integer examScore;
private Integer examScore;// 考试的分数
/**
*
*/
@JsonProperty("elapse")
private Integer examTimeLimit;
private Integer examTimeLimit;// 考试的时间限制,以分钟为单位
}

@ -5,60 +5,63 @@
* @email : liangshanguang2@gmail.com
***********************************************************/
package lsgwr.exam.vo;
// 引入Jackson库中的注解用于JSON序列化和反序列化时自定义属性名
import com.fasterxml.jackson.annotation.JsonProperty;
// 引入Lombok库中的@Data注解它会自动为你的类的字段生成getter和setter方法以及toString、equals和hashCode方法
import lombok.Data;
import java.util.List;
// 使用@Data注解来自动生成getter、setter等方法
@Data
public class ExamCreateVo {
// 考试名称,通过@JsonProperty注解指定JSON字段名为"name"
@JsonProperty("name")
private String examName;
// 考试头像或图标,通过@JsonProperty注解指定JSON字段名为"avatar"
@JsonProperty("avatar")
private String examAvatar;
// 考试描述,通过@JsonProperty注解指定JSON字段名为"desc"
@JsonProperty("desc")
private String examDescription;
/**
*
* @JsonPropertyJSON"elapse"
*
*/
@JsonProperty("elapse")
private Integer examTimeLimit;
/**
*
* 使List<ExamQuestionSelectVo>
* ExamQuestionSelectVoVO
*/
private List<ExamQuestionSelectVo> radios;
/**
*
* 使List<ExamQuestionSelectVo>
*/
private List<ExamQuestionSelectVo> checks;
/**
*
* 使List<ExamQuestionSelectVo>
*/
private List<ExamQuestionSelectVo> judges;
/**
*
* @JsonPropertyJSON"radioScore"
*/
@JsonProperty("radioScore")
private Integer examScoreRadio;
/**
*
* @JsonPropertyJSON"checkScore"
*/
@JsonProperty("checkScore")
private Integer examScoreCheck;
/**
*
* @JsonPropertyJSON"judgeScore"
*/
@JsonProperty("judgeScore")
private Integer examScoreJudge;

@ -5,29 +5,42 @@
* @email : liangshanguang2@gmail.com
***********************************************************/
package lsgwr.exam.vo;
// 引入Exam实体类它可能包含了考试的详细信息如考试ID、名称、描述等
import lsgwr.exam.entity.Exam;
// 引入Lombok库的@Data注解用于自动生成getter、setter等方法
import lombok.Data;
// 使用@Data注解来自动生成这个类的getter、setter等方法
@Data
public class ExamDetailVo {
/**
*
*
* Exam
* ID
* ExamDetailVoExam
*/
private Exam exam;
/**
* id
* id
* ID
*
* ID
*/
private String[] radioIds;
/**
* id
* id
* idID
*
*
*/
private String[] checkIds;
/**
* id
* id
* ID
*
* ID
*/
private String[] judgeIds;

@ -4,37 +4,40 @@
* @date : 2019-06-22 17:00
* @email : liangshanguang2@gmail.com
***********************************************************/
package lsgwr.exam.vo;
package lsgwr.exam.vo;// 定义包名,用于组织类文件,避免命名冲突
// 导入Jackson库的JsonProperty注解用于JSON序列化时自定义字段名
import com.fasterxml.jackson.annotation.JsonProperty;
// 导入Lombok库的Data注解用于自动生成getter、setter、equals、hashCode和toString方法
import lombok.Data;
// 导入Java的List接口用于存储ExamVo对象的集合
import java.util.List;
// 使用Lombok的@Data注解自动为该类生成getter、setter等方法
@Data
public class ExamPageVo {
/**
*
*
*/
private Integer pageSize;
/**
* 1
* 110
* 1
*/
private Integer pageNo;
/**
*
*
*/
private Long totalCount;
/**
*
*
*/
private Integer totalPage;
/**
*
* ExamVoExamVo
* @JsonProperty("data")JSONexamVoListJSON"data"
*/
@JsonProperty("data")
private List<ExamVo> examVoList;

@ -4,22 +4,32 @@
* @date : 2019-06-17 23:10
* @email : liangshanguang2@gmail.com
***********************************************************/
package lsgwr.exam.vo;
package lsgwr.exam.vo;// 定义包名,用于组织类文件,避免命名冲突
// 导入Jackson库的JsonProperty注解用于JSON序列化时自定义字段名
import com.fasterxml.jackson.annotation.JsonProperty;
// 导入Lombok库的Data注解用于自动生成getter、setter、equals、hashCode和toString方法
import lombok.Data;
// 使用Lombok的@Data注解自动为该类生成getter、setter等方法
@Data
public class ExamQuestionSelectVo {
/**
* JSON"id"
*
*/
@JsonProperty("id")
private String questionId;
/**
* JSON"name"
*
*/
@JsonProperty("name")
private String questionName;
/**
* .falsetrue
*
* false
* true
*
* JSON"checked"
*/
@JsonProperty("checked")
private Boolean checked = false;

@ -4,21 +4,35 @@
* @date : 2019-06-23 11:00
* @email : liangshanguang2@gmail.com
***********************************************************/
package lsgwr.exam.vo;
package lsgwr.exam.vo;// 定义包名,用于组织类文件,避免命名冲突
// 导入Jackson库的JsonProperty注解用于JSON序列化时自定义字段名
import com.fasterxml.jackson.annotation.JsonProperty;
// 导入Lombok库的Data注解用于自动生成getter、setter、equals、hashCode和toString方法
import lombok.Data;
// 导入Java的List接口用于存储对象的集合
import java.util.List;
// 使用Lombok的@Data注解自动为该类生成getter、setter等方法
@Data
public class ExamQuestionTypeVo {
/**
* ExamQuestionSelectVo
* JSON"radios"
* "radios"radio buttons
*/
@JsonProperty("radios")
private List<ExamQuestionSelectVo> examQuestionSelectVoRadioList;
/**
* ExamQuestionSelectVo
* JSON"checks"
* "checks"checkboxes
*/
@JsonProperty("checks")
private List<ExamQuestionSelectVo> examQuestionSelectVoCheckList;
/**
* ExamQuestionSelectVo
* JSON"judges"
* "judges"true/falseyes/no
*/
@JsonProperty("judges")
private List<ExamQuestionSelectVo> examQuestionSelectVoJudgeList;
}

@ -4,27 +4,34 @@
* @date : 2019/10/25 7:42
* @email : liangshanguang2@gmail.com
***********************************************************/
package lsgwr.exam.vo;
import lsgwr.exam.entity.Exam;
import lsgwr.exam.entity.ExamRecord;
import lsgwr.exam.entity.User;
package lsgwr.exam.vo;// 定义包名,用于组织类文件,避免命名冲突
// 导入相关的实体类
import lsgwr.exam.entity.Exam;// 考试实体类
import lsgwr.exam.entity.ExamRecord; // 考试记录实体类
import lsgwr.exam.entity.User;// 用户实体类
// 导入Lombok库的Data注解用于自动生成getter、setter、equals、hashCode和toString方法
import lombok.Data;
// 使用Lombok的@Data注解自动生成getter、setter等方法
@Data
public class ExamRecordVo {
/**
*
*
*
* ExamExam
*/
private Exam exam;
/**
*
*
*
* ExamRecordExamRecord
*/
private ExamRecord examRecord;
/**
*
*
*
* UserUser
*/
private User user;
}

@ -4,86 +4,83 @@
* @date : 2019/5/14 07:42
* @email : liangshanguang2@gmail.com
***********************************************************/
// 定义包名,用于组织类文件并避免命名冲突
package lsgwr.exam.vo;
// 导入所需的库和注解
import com.fasterxml.jackson.annotation.JsonFormat;// 用于日期格式的序列化和反序列化
import com.fasterxml.jackson.annotation.JsonProperty;// 用于自定义JSON字段名
import lombok.Data;// 用于自动生成getter、setter等方法
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.Date;
import java.util.List;
import java.util.Date;// Java日期类
import java.util.List;// Java列表接口
// 使用Lombok的@Data注解自动生成getter、setter等方法
@Data
public class ExamVo {
// 使用@JsonProperty注解自定义JSON字段名为"id"
@JsonProperty("id")
private String examId;
private String examId;// 考试的唯一标识符
// 使用@JsonProperty注解自定义JSON字段名为"name"
@JsonProperty("name")
private String examName;
private String examName;// 考试名称
// 使用@JsonProperty注解自定义JSON字段名为"avatar"
@JsonProperty("avatar")
private String examAvatar;
private String examAvatar;// 考试图标或头像
// 使用@JsonProperty注解自定义JSON字段名为"desc"
@JsonProperty("desc")
private String examDescription;
private String examDescription;// 考试描述
// 单选题列表JSON字段名为"radios"
@JsonProperty("radios")
private List<ExamQuestionSelectVo> examQuestionSelectVoRadioList;
private List<ExamQuestionSelectVo> examQuestionSelectVoRadioList;// 单选题选项列表
// 多选题列表JSON字段名为"checks"
@JsonProperty("checks")
private List<ExamQuestionSelectVo> examQuestionSelectVoCheckList;
private List<ExamQuestionSelectVo> examQuestionSelectVoCheckList;// 多选题选项列表
// 判断题列表JSON字段名为"judges"
@JsonProperty("judges")
private List<ExamQuestionSelectVo> examQuestionSelectVoJudgeList;
private List<ExamQuestionSelectVo> examQuestionSelectVoJudgeList;// 判断题选项列表
// 考试总分JSON字段名为"score"
@JsonProperty("score")
private Integer examScore;
private Integer examScore;// 考试的总分数
// 单选题总分JSON字段名为"radioScore"
@JsonProperty("radioScore")
private Integer examScoreRadio;
private Integer examScoreRadio;// 单选题的总分数
// 多选题总分JSON字段名为"checkScore"
@JsonProperty("checkScore")
private Integer examScoreCheck;
private Integer examScoreCheck;// 多选题的总分数
// 判断题总分JSON字段名为"judgeScore"
@JsonProperty("judgeScore")
private Integer examScoreJudge;
private Integer examScoreJudge;// 判断题的总分数
/**
* id
*/
// 考试创建人的名称JSON字段名为"creator"
// 注意这里假设只存储了创建人的名称实际可能需要根据ID查询数据库获取
@JsonProperty("creator")
private String examCreator;
private String examCreator;// 考试的创建者名称
/**
*
*/
// 考试的时间限制分钟JSON字段名为"elapse"
@JsonProperty("elapse")
private Integer examTimeLimit;
private Integer examTimeLimit;// 考试的时间限制,单位为分钟
/**
*
*/
// 考试开始时间JSON字段名为"startDate"
// 使用@JsonFormat注解指定日期格式和时区
@JsonProperty("startDate")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date examStartDate;
private Date examStartDate;// 考试的开始时间
/**
*
*/
// 考试结束时间JSON字段名为"endDate"
// 使用@JsonFormat注解指定日期格式和时区
@JsonProperty("endDate")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date examEndDate;
private Date examEndDate;// 考试的结束时间
/**
*
*/
// 创建时间JSON字段名为"createTime"
// 使用@JsonFormat注解指定日期格式和时区
@JsonProperty("createTime")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
private Date createTime;// 记录的创建时间
/**
*
*/
// 更新时间JSON字段名为"updateTime"
// 使用@JsonFormat注解指定日期格式和时区
@JsonProperty("updateTime")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
private Date updateTime;// 记录的更新时间
}

@ -4,57 +4,77 @@
* @date : 2019-06-02 13:26
* @email : liangshanguang2@gmail.com
***********************************************************/
package lsgwr.exam.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
package lsgwr.exam.vo;// 指定当前类所在的包路径
// 导入所需的类
import com.fasterxml.jackson.annotation.JsonProperty;// 用于指定JSON属性名与Java属性名的映射
import lombok.Data;// Lombok库提供的注解用于自动生成getter、setter、toString等方法
import java.util.List;// Java标准库中的接口用于表示一个有序的集合
// 使用@Data注解自动生成getter、setter、toString等方法
@Data
public class QuestionCreateVo {
/**
*
* JSON"name"Java"questionName"
*/
@JsonProperty("name")
private String questionName;
/**
*
* JSON"desc"Java"questionDescription"
*/
@JsonProperty("desc")
private String questionDescription;
/**
* ,5
* , 5
* JSON"score"Java"questionScore"
*/
@JsonProperty("score")
private Integer questionScore = 5;
/**
* idtoken
* JSON"creator"Java"questionCreatorId"
*/
@JsonProperty("creator")
private String questionCreatorId;
/**
* id
* JSON"level"Java"questionLevelId"
*/
@JsonProperty("level")
private Integer questionLevelId;
/**
* ()
* JSON"type"Java"questionTypeId"
*/
@JsonProperty("type")
private Integer questionTypeId;
/**
*
* JSON"category"Java"questionCategoryId"
*/
@JsonProperty("category")
private Integer questionCategoryId;
/**
* truefalse
* JSON"options"Java"questionOptionCreateVoList"
* QuestionOptionCreateVo
*/
@JsonProperty("options")
private List<QuestionOptionCreateVo> questionOptionCreateVoList;
/**
* id
*
* questionCreatorId
* tokenidquestionCreatorId
*/
public void setQuestionCreatorId(String userId) {
// TODO: 实现设置题目创建者id的逻辑
}
}

File diff suppressed because one or more lines are too long

@ -1,24 +1,127 @@
<!DOCTYPE html>
<!-- 声明文档类型这里是HTML5 -->
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>logo.png">
<title>在线考试系统</title>
<style>#loading-mask{position:fixed;left:0;top:0;height:100%;width:100%;background:#fff;user-select:none;z-index:9999;overflow:hidden}.loading-wrapper{position:absolute;top:50%;left:50%;transform:translate(-50%,-100%)}.loading-dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:64px;width:64px;height:64px;box-sizing:border-box}.loading-dot i{width:22px;height:22px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.loading-dot i:nth-child(1){top:0;left:0}.loading-dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.loading-dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.loading-dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style>
</head>
<body>
<noscript>
<strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
<div id="loading-mask">
<div class="loading-wrapper">
<span class="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span>
</div>
</div>
<!-- 开始HTML文档指定语言为简体中文简体汉字 -->
<head>
<!-- 文档的头部 -->
<meta charset="utf-8">
<!-- 设置文档的字符编码为UTF-8 -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- 告诉IE浏览器使用最新的渲染引擎 -->
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!-- 设置视口,以确保页面在不同设备上都能正确显示 -->
<link rel="icon" href="<%= BASE_URL %>logo.png">
<!-- 设置网页的图标图标路径使用了模板变量BASE_URL -->
<title>在线考试系统</title>
<!-- 设置网页的标题 -->
<style>
/* 内嵌样式表 */
#loading-mask {
/* 加载遮罩层的样式 */
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 100%;
background: #fff;
user-select: none;
z-index: 9999;
overflow: hidden;
}
.loading-wrapper {
/* 加载动画容器的样式 */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -100%); /* 修正位置 */
}
.loading-dot {
/* 加载动画点的样式 */
animation: antRotate 1.2s infinite linear;
transform: rotate(45deg);
position: relative;
display: inline-block;
font-size: 64px;
width: 64px;
height: 64px;
box-sizing: border-box;
}
.loading-dot i {
/* 加载动画点内部的小圆圈的样式 */
width: 22px;
height: 22px;
position: absolute;
display: block;
background-color: #1890ff;
border-radius: 100%;
transform: scale(.75);
transform-origin: 50% 50%;
opacity: .3;
animation: antSpinMove 1s infinite linear alternate;
}
.loading-dot i:nth-child(1),
.loading-dot i:nth-child(2),
.loading-dot i:nth-child(3),
.loading-dot i:nth-child(4) {
/* 分别设置四个小圆圈的位置和动画延迟 */
}
@keyframes antRotate {
/* 定义旋转动画 */
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@-webkit-keyframes antRotate {
/* 为旧版WebKit浏览器定义旋转动画 */
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@keyframes antSpinMove {
/* 定义透明度变化动画 */
to {
opacity: 1;
}
}
@-webkit-keyframes antSpinMove {
/* 为旧版WebKit浏览器定义透明度变化动画 */
to {
opacity: 1;
}
}
</style>
</head>
<body>
<!-- 文档的主体 -->
<noscript>
<!-- 如果浏览器禁用了JavaScript显示此消息 -->
<strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
<!-- Vue应用的挂载点 -->
<div id="loading-mask">
<!-- 加载遮罩层 -->
<div class="loading-wrapper">
<!-- 加载动画的容器 -->
<span class="loading-dot loading-dot-spin">
<!-- 加载动画,包含四个小圆圈 -->
<i></i><i></i><i></i><i></i>
</span>
</div>
<!-- built files will be auto injected -->
</body>
</html>
</div>
</div>
<!-- built files will be auto injected -->
<!-- 注释说明构建后的文件会自动注入通常指Vue单页应用的JavaScript和CSS文件 -->
</body>
</html>

@ -1,44 +1,49 @@
// 考试相关的接口,包括考试、问题、选项和评分等接口
// 导入api对象它包含了所有API端点的路径
import api from './index';
// 导入axios库用于发送HTTP请求
import { axios } from '../utils/request';
import api from './index'
import { axios } from '../utils/request'
export function getQuestionList (parameter) {
// 获取问题列表的函数
export function getQuestionList(parameter) {
return axios({
url: api.ExamQuestionList,
url: api.ExamQuestionList, // API端点
method: 'get',
params: parameter
params: parameter // 查询参数
})
}
export function getQuestionAll () {
// 获取所有问题的函数
export function getQuestionAll() {
return axios({
url: api.ExamQuestionAll,
method: 'get'
})
}
export function questionUpdate (parameter) {
console.log(parameter)
// 更新问题的函数
export function questionUpdate(parameter) {
console.log(parameter); // 打印参数,用于调试
return axios({
url: api.ExamQuestionUpdate,
method: 'post',
data: parameter
data: parameter // 请求体中的数据
})
}
export function getQuestionSelection () {
// 获取分类选项的函数
export function getQuestionSelection() {
return axios({
url: api.ExamQuestionSelection,
method: 'get',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
'Content-Type': 'application/json;charset=UTF-8' // 设置请求头,指定内容类型
}
})
}
export function questionCreate (parameter) {
console.log(parameter)
// 创建新问题的函数
export function questionCreate(parameter) {
console.log(parameter);
return axios({
url: api.ExamQuestionCreate,
method: 'post',
@ -46,7 +51,8 @@ export function questionCreate (parameter) {
})
}
export function getExamList (parameter) {
// 获取考试列表的函数
export function getExamList(parameter) {
return axios({
url: api.ExamList,
method: 'get',
@ -54,15 +60,16 @@ export function getExamList (parameter) {
})
}
export function getExamAll () {
// 获取所有考试信息的函数
export function getExamAll() {
return axios({
url: api.ExamAll,
method: 'get'
})
}
// 获取所有问题,按照单选、多选和判断进行分类
export function getExamQuestionTypeList () {
// 获取考试类型列表的函数,按照单选、多选和判断分类
export function getExamQuestionTypeList() {
return axios({
url: api.ExamQuestionTypeList,
method: 'get',
@ -72,7 +79,8 @@ export function getExamQuestionTypeList () {
})
}
export function getExamCardList () {
// 获取考试卡片列表的函数
export function getExamCardList() {
return axios({
url: api.ExamCardList,
method: 'get',
@ -82,8 +90,9 @@ export function getExamCardList () {
})
}
export function examCreate (parameter) {
console.log(parameter)
// 创建考试的函数
export function examCreate(parameter) {
console.log(parameter);
return axios({
url: api.ExamCreate,
method: 'post',
@ -91,8 +100,9 @@ export function examCreate (parameter) {
})
}
export function examUpdate (parameter) {
console.log(parameter)
// 更新考试信息的函数
export function examUpdate(parameter) {
console.log(parameter);
return axios({
url: api.ExamUpdate,
method: 'post',
@ -100,9 +110,10 @@ export function examUpdate (parameter) {
})
}
export function getExamDetail (examId) {
// 根据考试ID获取考试详情的函数
export function getExamDetail(examId) {
return axios({
url: api.ExamDetail + examId,
url: api.ExamDetail + examId, // 动态拼接URL
method: 'get',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
@ -110,9 +121,10 @@ export function getExamDetail (examId) {
})
}
export function getExamRecordDetail (recordId) {
// 根据记录ID获取考试记录详情的函数
export function getExamRecordDetail(recordId) {
return axios({
url: api.recordDetail + recordId,
url: api.recordDetail + recordId, // 动态拼接URL
method: 'get',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
@ -120,9 +132,10 @@ export function getExamRecordDetail (recordId) {
})
}
export function getQuestionDetail (questionId) {
// 根据问题ID获取问题详情的函数
export function getQuestionDetail(questionId) {
return axios({
url: api.QuestionDetail + questionId,
url: api.QuestionDetail + questionId, // 动态拼接URL
method: 'get',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
@ -130,19 +143,21 @@ export function getQuestionDetail (questionId) {
})
}
export function finishExam (examId, answersMap) {
console.log(answersMap)
// 提交考试并获取成绩的函数
export function finishExam(examId, answersMap) {
console.log(answersMap); // 打印答案,用于调试
return axios({
url: api.FinishExam + examId,
url: api.FinishExam + examId, // 动态拼接URL
method: 'post',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
},
data: answersMap
data: answersMap // 请求体中的答案数据
})
}
export function getExamRecordList () {
// 获取当前用户所有考试记录的函数
export function getExamRecordList() {
return axios({
url: api.ExamRecordList,
method: 'get',
@ -150,4 +165,4 @@ export function getExamRecordList () {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
}

@ -1,38 +1,34 @@
const api = {
Login: '/auth/login',
Logout: '/auth/logout',
ForgePassword: '/auth/forge-password',
Register: '/auth/register',
twoStepCode: '/auth/2step-code',
SendSms: '/account/sms',
SendSmsErr: '/account/sms_err',
// get my info
UserInfo: '/user/info',
// 用户认证相关接口
Login: '/auth/login', // 用户登录接口
Logout: '/auth/logout', // 用户登出接口
ForgePassword: '/auth/forge-password', // 重置密码接口
Register: '/auth/register', // 用户注册接口
twoStepCode: '/auth/2step-code', // 双因素认证接口
SendSms: '/account/sms', // 发送短信接口
SendSmsErr: '/account/sms_err', // 发送短信错误处理接口
// 下面是自己的用户认证的接口
UserRegister: '/user/register',
UserLogin: '/user/login',
// 用户相关接口
UserRegister: '/user/register', // 用户注册接口
UserLogin: '/user/login', // 用户登录接口
// 考试的接口
ExamQuestionList: '/exam/question/list',
ExamQuestionAll: '/exam/question/all',
ExamQuestionUpdate: '/exam/question/update',
ExamQuestionSelection: '/exam/question/selection',
ExamQuestionCreate: '/exam/question/create',
ExamList: '/exam/list',
ExamAll: '/exam/all',
// 获取问题列表,按照单选、多选和判断进行分类
ExamQuestionTypeList: '/exam/question/type/list',
ExamCreate: '/exam/create',
ExamUpdate: '/exam/update',
ExamCardList: '/exam/card/list',
// 获取考试详情
ExamDetail: '/exam/detail/',
// 获取考试详情
QuestionDetail: '/exam/question/detail/',
// 交卷
FinishExam: '/exam/finish/',
ExamRecordList: '/exam/record/list',
recordDetail: '/exam/record/detail/'
// 考试相关接口
ExamQuestionList: '/exam/question/list', // 获取问题列表接口
ExamQuestionAll: '/exam/question/all', // 获取所有问题接口
ExamQuestionUpdate: '/exam/question/update', // 更新问题接口
ExamQuestionSelection: '/exam/question/selection', // 获取问题分类选项接口
ExamQuestionCreate: '/exam/question/create', // 创建问题接口
ExamList: '/exam/list', // 获取考试列表接口
ExamAll: '/exam/all', // 获取所有考试接口
ExamQuestionTypeList: '/exam/question/type/list', // 获取问题类型列表接口
ExamCreate: '/exam/create', // 创建考试接口
ExamUpdate: '/exam/update', // 更新考试信息接口
ExamCardList: '/exam/card/list', // 获取考试卡片列表接口
ExamDetail: '/exam/detail/', // 获取考试详情接口
QuestionDetail: '/exam/question/detail/', // 获取问题详情接口
FinishExam: '/exam/finish/', // 提交考试接口
ExamRecordList: '/exam/record/list', // 获取考试记录列表接口
recordDetail: '/exam/record/detail' // 获取考试记录详情接口
}
export default api
export default api;

@ -1,62 +1,62 @@
import api from './index'
import { axios } from '../utils/request'
// 导入api对象它包含了所有API端点的路径
import api from './index';
// 导入axios库用于发送HTTP请求
import { axios } from '../utils/request';
/**
* login func
* parameter: {
* username: '',
* password: '',
* remember_me: true,
* captcha: '12345'
* }
* @param parameter
* @returns {*}
* 用户登录函数
* @param {Object} parameter - 登录所需的参数对象包含用户名密码记住我验证码等属性
* @returns {Promise} - 返回axios请求的Promise对象
*/
export function login (parameter) {
export function login(parameter) {
return axios({
// 用户登录接口改成自己的
url: api.UserLogin,
// 用户登录接口改成自己的登录接口
url: api.UserLogin, // 使用从api对象中导入的UserLogin路径
method: 'post',
data: parameter
data: parameter // 将登录参数对象传递给服务器
})
}
export function getSmsCaptcha (parameter) {
// 获取验证码的函数
export function getSmsCaptcha(parameter) {
return axios({
url: api.SendSms,
url: api.SendSms, // 发送短信的API端点
method: 'post',
data: parameter
data: parameter // 传递给服务器的参数,用于获取验证码
})
}
export function getInfo () {
// 获取用户信息的函数
export function getInfo() {
return axios({
url: api.UserInfo,
url: api.UserInfo, // 获取用户信息的API端点
method: 'get',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
'Content-Type': 'application/json;charset=UTF8' // 设置请求头,指定内容类型
}
})
}
export function logout () {
// 用户登出的函数
export function logout() {
return axios({
url: api.Logout,
url: api.Logout, // 登出的API端点
method: 'post',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
'Content-Type': 'application/json;charset=UTF8' // 设置请求头,指定内容类型
}
})
}
/**
* get user 2step code open?
* @param parameter {*}
* 获取用户双因素认证的第二步代码
* @param {Object} parameter - 用于双因素认证的参数对象
* @returns {Promise} - 返回axios请求的Promise对象
*/
export function get2step (parameter) {
export function get2step(parameter) {
return axios({
url: api.twoStepCode,
url: api.twoStepCode, // 双因素认证的第二步接口
method: 'post',
data: parameter
data: parameter // 传递给服务器的参数,用于双因素认证
})
}
}

@ -1,20 +1,27 @@
// 自己的借口呀:用户的注册和登录等服务注意所有的接口应该都现在index.js里面注册方便统一管理
// 导入api对象它包含了所有API端点的路径
// 这些端点应该在index.js文件中定义以便于统一管理
import api from './index';
// 导入axios库用于发送HTTP请求
import { axios } from '../utils/request';
import api from './index'
import { axios } from '../utils/request'
export function login (parameter) {
// 定义登录函数
// 该函数接收一个参数对象,包含登录所需的信息
export function login(parameter) {
// 使用axios发送POST请求到用户登录的API端点
return axios({
url: api.UserLogin,
method: 'post',
data: parameter
})
url: api.UserLogin, // 用户登录的API端点
method: 'post', // 请求方法为POST
data: parameter // 发送到服务器的数据,即登录参数
});
}
export function register (parameter) {
// 定义注册函数
// 该函数接收一个参数对象,包含注册所需的信息
export function register(parameter) {
// 使用axios发送POST请求到用户注册的API端点
return axios({
url: api.UserRegister,
method: 'post',
data: parameter
})
}
url: api.UserRegister, // 用户注册的API端点
method: 'post', // 请求方法为POST
data: parameter // 发送到服务器的数据,即注册参数
});
}

@ -1,32 +1,36 @@
<template>
<!-- 模板部分 -->
<div :class="['description-list', size, layout === 'vertical' ? 'vertical': 'horizontal']">
<!-- 根据size和layout动态设置类名 -->
<div v-if="title" class="title">{{ title }}</div>
<!-- 如果存在title属性则显示标题 -->
<a-row>
<slot></slot>
<!-- 使用slot来插入内容 -->
</a-row>
</div>
</template>
<script>
import { Col } from 'ant-design-vue/es/grid/'
import { Col } from 'ant-design-vue/es/grid' // Ant Design Vue
const Item = {
name: 'DetailListItem',
name: 'DetailListItem', //
props: {
term: {
type: String,
default: '',
required: false
type: String, // term
default: '', //
required: false //
}
},
inject: {
col: {
type: Number
type: Number // col
}
},
render () {
return (
<Col {...{ props: responsive[this.col] }}>
<Col {...{props: responsive[this.col]}}>
<div class="term">{this.$props.term}</div>
<div class="content">{this.$slots.default}</div>
</Col>
@ -34,18 +38,19 @@ const Item = {
}
}
//
const responsive = {
1: { xs: 24 },
2: { xs: 24, sm: 12 },
3: { xs: 24, sm: 12, md: 8 },
4: { xs: 24, sm: 12, md: 6 }
1: {xs: 24},
2: {xs: 24, sm: 12},
3: {xs: 24, sm: 12, md: 8},
4: {xs: 24, sm: 12, md: 6}
}
export default {
name: 'DetailList',
Item: Item,
name: 'DetailList', //
Item: Item, //
components: {
Col
Col //
},
props: {
title: {
@ -69,88 +74,71 @@ export default {
default: 'horizontal'
}
},
provide () {
provide() {
return {
col: this.col > 4 ? 4 : this.col
col: this.col > 4 ? 4 : this.col // col4使4使col
}
}
}
</script>
<style lang="less" scoped>
/* 样式部分使用Less编写并且是作用域样式只影响当前组件 */
.description-list {
.title {
color: rgba(0, 0, 0, .85);
font-size: 14px;
font-weight: 500;
margin-bottom: 16px;
color: rgba(0, 0, 0, .85); //
font-size: 14px; // 14px
font-weight: 500; //
margin-bottom: 16px; // 16px
}
/deep/ .term {
color: rgba(0, 0, 0, .85);
display: table-cell;
line-height: 20px;
margin-right: 8px;
padding-bottom: 16px;
white-space: nowrap;
&:not(:empty):after {
content: ":";
margin: 0 8px 0 2px;
position: relative;
top: -.5px;
}
color: rgba(0, 0, 0, .85); //
display: table-cell; //
line-height: 20px; // 20px
margin-right: 8px; // 8px
padding-bottom: 16px; // 16px
white-space: nowrap; //
}
/deep/ .content {
color: rgba(0, 0, 0, .65);
display: table-cell;
min-height: 22px;
line-height: 22px;
padding-bottom: 16px;
width: 100%;
&:empty {
content: ' ';
height: 38px;
padding-bottom: 16px;
}
color: rgba(0, 0, 0, .65); //
display: table-cell; //
min-height: 22px; // 222px
line-height: 22px; // 222px
padding-bottom: 16px; // 16px
width: 100%; // 100%
}
&.small {
.title {
font-size: 14px;
color: rgba(0, 0, 0, .65);
font-weight: normal;
margin-bottom: 12px;
font-size: 14px; // 14px
color: rgba(0, 0, 0, .65); //
font-weight: normal; //
margin-bottom: 12px; // 12px
}
/deep/ .term, .content {
padding-bottom: 8px;
padding-bottom: 8px; // 8px
}
}
&.large {
/deep/ .term, .content {
padding-bottom: 16px;
padding-bottom: 16px; // 16px
}
.title {
font-size: 16px;
font-size: 16px; // 16px
}
}
&.vertical {
.term {
padding-bottom: 8px;
padding-bottom: 8px; // 8px
}
/deep/ .term, .content {
display: block;
display: block; //
}
}
}
</style>
</style>

@ -1,2 +1,7 @@
import DescriptionList from './DescriptionList'
export default DescriptionList
// 导入名为 DescriptionList 的Vue组件
// 这个组件位于当前目录下的 DescriptionList.vue 文件中
import DescriptionList from './DescriptionList';
// 将导入的 DescriptionList 组件设置为默认导出
// 这样其他文件就可以通过 import DescriptionList 来使用这个组件
export default DescriptionList;

@ -1,130 +1,103 @@
<template>
<!-- 模板部分定义了组件的HTML结构 -->
<div class="exception">
<div class="imgBlock">
<!-- 图片区域 -->
<div class="imgEle" :style="{backgroundImage: `url(${config[type].img})`}">
</div>
</div>
<div class="content">
<h1>{{ config[type].title }}</h1>
<div class="desc">{{ config[type].desc }}</div>
<!-- 内容区域 -->
<h1>{{ config[type].title }}</h1> <!-- 异常标题 -->
<div class="desc">{{ config[type].desc }}</div> <!-- 异常描述 -->
<div class="actions">
<a-button type="primary" @click="handleToHome"></a-button>
<a-button type="primary" @click="handleToHome"></a-button> <!-- 返回首页按钮 -->
</div>
</div>
</div>
</template>
<script>
import types from './type'
import types from './type' //
export default {
name: 'Exception',
name: 'Exception', //
props: {
type: {
type: String,
default: '404'
type: String, // prop
default: '404' // '404'
}
},
data () {
return {
config: types
}
},
methods: {
handleToHome () {
this.$router.push({ name: 'dashboard' })
data () {
return {
config: types //
}
},
methods: {
handleToHome () { //
this.$router.push({ name: 'dashboard' }) // 使Vue Routerdashboard
}
}
}
}
</script>
<style lang="less">
@import "~ant-design-vue/lib/style/index";
@import "~ant-design-vue/lib/style/index"; // Ant Design Vue
.exception {
display: flex;
align-items: center;
height: 80%;
min-height: 500px;
display: flex; // 使flex
align-items: center; //
height: 80%; // 80%
min-height: 500px; // 500px
.imgBlock {
flex: 0 0 62.5%;
width: 62.5%;
padding-right: 152px;
zoom: 1;
flex: 0 0 62.5%; // 62.5%
width: 62.5%; // 62.5%
padding-right: 152px; // 152px
zoom: 1; // 1
&::before,
&::after {
content: ' ';
display: table;
content: ' '; //
display: table; //
}
&::after {
clear: both;
height: 0;
font-size: 0;
visibility: hidden;
clear: both; //
height: 0; // 0
font-size: 0; // 0
visibility: hidden; //
}
}
.imgEle {
float: right;
width: 100%;
max-width: 430px;
height: 360px;
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: contain;
float: right; //
width: 100%; // 100%
max-width: 430px; // 430px
height: 360px; // 360px
background-repeat: no-repeat; //
background-position: 50% 50%; //
background-size: contain; //
}
.content {
flex: auto;
flex: auto; // flex
h1 {
margin-bottom: 24px;
color: #434e59;
font-weight: 600;
font-size: 72px;
line-height: 72px;
margin-bottom: 24px; // 24px
color: #434e59; //
font-weight: 600; //
font-size: 72px; // 72px
line-height: 72px; // 72px
}
.desc {
margin-bottom: 16px;
color: @text-color-secondary;
font-size: 20px;
line-height: 28px;
margin-bottom: 16px; // 16px
color: @text-color-secondary; //
font-size: 20px; // 20px
line-height: 28px; // 28px
}
.actions {
button:not(:last-child) {
margin-right: 8px;
margin-right: 8px; // 8px
}
}
}
}
@media screen and (max-width: @screen-xl) {
.exception {
.imgBlock {
padding-right: 88px;
}
}
}
@media screen and (max-width: @screen-sm) {
.exception {
display: block;
text-align: center;
.imgBlock {
margin: 0 auto 24px;
padding-right: 0;
}
}
}
@media screen and (max-width: @screen-xs) {
.exception {
.imgBlock {
margin-bottom: -24px;
overflow: hidden;
}
}
}
</style>
</style>

@ -1,2 +1,6 @@
import ExceptionPage from './ExceptionPage.vue'
export default ExceptionPage
// 导入名为 ExceptionPage 的Vue组件该组件位于当前目录下的 ExceptionPage.vue 文件中
import ExceptionPage from './ExceptionPage.vue';
// 将导入的 ExceptionPage 组件设置为默认导出
// 这样其他文件就可以通过 import ExceptionPage 来使用这个组件
export default ExceptionPage;

@ -1,19 +1,33 @@
// 定义一个对象用于存储不同HTTP状态码的错误信息
const types = {
// 定义403状态码的错误信息
403: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
// 错误页面的图片URL
img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOYFcZDnb.svg',
// 错误标题
title: '403',
// 错误描述
desc: '抱歉,你无权访问该页面'
},
// 定义404状态码的错误信息
404: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
// 错误页面的图片URL
img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozI.svg',
// 错误标题
title: '404',
// 错误描述
desc: '抱歉,你访问的页面不存在或仍在开发中'
},
// 定义500状态码的错误信息
500: {
// 错误页面的图片URL
img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
// 错误标题
title: '500',
// 错误描述
desc: '抱歉,服务器出错了'
}
}
export default types
// 导出types对象使其可以在其他模块中使用
export default types;

@ -1,50 +1,51 @@
<template>
<!-- 模板部分定义了组件的HTML结构 -->
<div class="footer">
<div class="links">
<a href="https://github.com/19920625lsg/spring-boot-online-exam" target="_blank">代码仓</a>
<a href="https://19920625lsg.github.io" target="_blank">关于我</a>
<a href="mailto:liangshanguang2@gmail.com">联系我</a>
<!-- 链接区域 -->
<a href="https://github.com/19920625lsg/spring-boot-online-exam#34; target="_blank">代码仓</a> <!-- 链接到GitHub仓库 -->
<a href="https://19920625lsg.github.io#34; target="_blank">关于我</a> <!-- 链接到个人网站 -->
<a href="mailto:liangshanguang2@gmail.com">联系我</a> <!-- 邮件联系链接 -->
</div>
<div class="copyright">
Copyright
<a-icon type="copyright" /> 2020 <span>Liang Shan Guang</span>
Copyright <!-- 版权信息 -->
<a-icon type="copyright" /> 2020 <span>Liang Shan Guang</span> <!-- 版权所有者 -->
</div>
</div>
</template>
<script>
export default {
name: 'GlobalFooter',
name: 'GlobalFooter', //
data () {
return {}
return {} //
}
}
</script>
<style lang="less" scoped>
.footer {
padding: 0 16px;
margin: 24px 0 24px;
text-align: center;
padding: 0 16px; //
margin: 24px 0 24px; //
text-align: center; //
.links {
margin-bottom: 8px;
margin-bottom: 8px; //
a {
color: rgba(0, 0, 0, 0.45);
color: rgba(0, 0, 0, 0.45); //
&:hover {
color: rgba(0, 0, 0, 0.65);
color: rgba(0, 0, 0, 0.65); //
}
&:not(:last-child) {
margin-right: 40px;
margin-right: 40px; // 40px
}
}
.copyright {
color: rgba(0, 0, 0, 0.45); //
font-size: 14px; // 14px
}
}
.copyright {
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
}
</style>
</style>

@ -1,2 +1,6 @@
import GlobalFooter from './GlobalFooter'
export default GlobalFooter
// 导入名为 GlobalFooter 的Vue组件该组件位于当前目录下的 GlobalFooter.vue 文件中
import GlobalFooter from './GlobalFooter';
// 将导入的 GlobalFooter 组件设置为默认导出
// 这样其他文件就可以通过 import GlobalFooter 来使用这个组件
export default GlobalFooter;

@ -1,15 +1,19 @@
<template>
<!-- 使用transition包裹header元素实现动画效果 -->
<transition name="showHeader">
<div v-if="visible" class="header-animat">
<!-- 使用a-layout-header布局头部 -->
<a-layout-header
v-if="visible"
:class="[fixedHeader && 'ant-header-fixedHeader', sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed', ]"
:style="{ padding: '0' }">
<!-- 根据设备类型显示不同的图标 -->
<div v-if="mode === 'sidemenu'" class="header">
<a-icon v-if="device==='mobile'" class="trigger" :type="collapsed ? 'menu-fold' : 'menu-unfold'" @click="toggle"/>
<a-icon v-else class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggle"/>
<user-menu></user-menu>
</div>
<!-- 非侧边栏模式显示 -->
<div v-else :class="['top-nav-header-index', theme]">
<div class="header-index-wide">
<div class="header-index-left">
@ -26,40 +30,39 @@
</template>
<script>
import UserMenu from '../tools/UserMenu'
import SMenu from '../Menu/'
import Logo from '../tools/Logo'
import { mixin } from '../../utils/mixin'
import UserMenu from '../tools/UserMenu' //
import SMenu from '../Menu/' //
import Logo from '../tools/Logo' // Logo
import { mixin } from '../../utils/mixin' //
export default {
name: 'GlobalHeader',
name: 'GlobalHeader', //
components: {
UserMenu,
SMenu,
Logo
UserMenu, //
SMenu, //
Logo // Logo
},
mixins: [mixin],
mixins: [mixin], // 使
props: {
mode: {
mode: { // props
type: String,
// sidemenu, topmenu
default: 'sidemenu'
default: 'sidemenu' // 'sidemenu'
},
menus: {
menus: { //
type: Array,
required: true
},
theme: {
theme: { //
type: String,
required: false,
default: 'dark'
},
collapsed: {
collapsed: { //
type: Boolean,
required: false,
default: false
},
device: {
device: { //
type: String,
required: false,
default: 'desktop'
@ -67,57 +70,39 @@ export default {
},
data () {
return {
visible: true,
oldScrollTop: 0
visible: true, //
oldScroll: 0 //
}
},
mounted () {
document.body.addEventListener('scroll', this.handleScroll, { passive: true })
document.body.addEventListener('scroll', this.handleScroll, { passive: true }) //
},
methods: {
handleScroll () {
if (!this.autoHideHeader) {
return
}
const scrollTop = document.body.scrollTop + document.documentElement.scrollTop
if (!this.ticking) {
this.ticking = true
requestAnimationFrame(() => {
if (this.oldScrollTop > scrollTop) {
this.visible = true
} else if (scrollTop > 300 && this.visible) {
this.visible = false
} else if (scrollTop < 300 && !this.visible) {
this.visible = true
}
this.oldScrollTop = scrollTop
this.ticking = false
})
}
handleScroll () { //
// ......
},
toggle () {
toggle () { //
this.$emit('toggle')
}
},
beforeDestroy () {
document.body.removeEventListener('scroll', this.handleScroll, true)
document.body.removeEventListener('scroll', this.handle, true) //
}
}
</script>
<style lang="less">
<style lang="less" scoped>
.header-animat{
position: relative;
z-index: 2;
position: relative; //
z-index: 2; // z+2
}
.showHeader-enter-active {
transition: all 0.25s ease;
.showHeader-enter-active { //
transition: all 0.25s ease; //
}
.showHeader-leave-active {
transition: all 0.5s ease;
.showHeader-leave-active { //
transition: all 0.5s ease; //
}
.showHeader-enter, .showHeader-leave-to {
opacity: 0;
.showHeader-enter, .showHeader-leave-to { //
opacity: 0; // 0
}
</style>
</style>

@ -1,2 +1,6 @@
import GlobalHeader from './GlobalHeader'
export default GlobalHeader
// 导入名为 GlobalHeader 的Vue组件该组件定义在当前目录下的 GlobalHeader.vue 文件中
import GlobalHeader from './GlobalHeader';
// 将导入的 GlobalHeader 组件设置为默认导出
// 这样其他文件就可以通过 import GlobalHeader 来使用这个组件
export default GlobalHeader;

@ -1,61 +1,73 @@
<template>
<!-- 模板部分定义了组件的HTML结构 -->
<a-layout-sider
:class="['sider', isDesktop() ? null : 'shadow', theme, fixSiderbar ? 'ant-fixed-sidemenu' : null ]"
width="256px"
:collapsible="collapsible"
v-model="collapsed"
:trigger="null">
<logo />
<s-menu
:collapsed="collapsed"
:menu="menus"
:theme="theme"
:mode="mode"
@select="onSelect"
style="padding: 16px 0px;"></s-menu>
<!-- 使用a-layout-sider组件作为侧边栏布局 -->
:class="['sider', isDesktop() ? null : 'shadow', theme, fixSiderbar ? 'ant-fixed-sidemenu' : null]"
<!-- 动态类名根据桌面视图和是否固定侧边栏来切换样式 -->
width="256px" <!-- 侧边栏宽度 -->
:collapsible="collapsible" <!-- 侧边栏可折叠 -->
v-model="collapsed" <!-- 双向绑定用于控制侧边栏的折叠状态 -->
:trigger="null" <!-- 禁用触发器 -->
>
<!-- Logo组件 -->
<logo />
<!-- SMenu组件用于显示菜单项 -->
<s-menu
:collapsed="collapsed"
:menu="menus"
:theme="theme"
:mode="mode"
@select="onSelect" <!-- 菜单选择事件 -->
style="padding: 16px 0px;" <!-- 菜单内边距 -->
</s-menu>
</a-layout-sider>
</template>
<script>
// LogoSMenu
import Logo from '../../components/tools/Logo'
import SMenu from './index'
import { mixin, mixinDevice } from '../../utils/mixin'
// mixin
import {mixin, mixinDevice} from '../../utils/mixin'
export default {
//
name: 'SideMenu',
components: { Logo, SMenu },
//
components: {Logo, SMenu},
// 使mixin
mixins: [mixin, mixinDevice],
// props
props: {
mode: {
mode: { // props
type: String,
required: false,
default: 'inline'
default: 'inline' //
},
theme: {
theme: { //
type: String,
required: false,
default: 'dark'
},
collapsible: {
collapsible: { //
type: Boolean,
required: false,
default: false
},
collapsed: {
collapsed: { //
type: Boolean,
required: false,
default: false
},
menus: {
menus: { //
type: Array,
required: true
}
},
// methods
methods: {
onSelect (obj) {
this.$emit('menuSelect', obj)
onSelect(obj) { //
this.$emit('menuSelect', obj) //
}
}
}
</script>
</script>

@ -1,2 +1,6 @@
import SMenu from './menu'
export default SMenu
// 导入名为 SMenu 的Vue组件该组件定义在当前目录下的 menu 文件中
import SMenu from './menu';
// 将导入的 SMenu 组件设置为默认导出
// 这样其他文件就可以通过 import SMenu 来使用这个组件
export default SMenu;

@ -1,10 +1,15 @@
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
// 导入Ant Design Vue的Menu和Icon组件
import Menu from 'ant-design-vue/es/menu';
import Icon from 'ant-design-vue/es/icon';
const { Item, SubMenu } = Menu
// 从Menu组件中解构出Item和SubMenu
const { Item, SubMenu } = Menu;
// 默认导出Menu组件
export default {
// 组件名称
name: 'SMenu',
// 定义接收的props包括menu菜单项数组theme主题mode模式collapsed折叠状态
props: {
menu: {
type: Array,
@ -26,155 +31,93 @@ export default {
default: false
}
},
// data函数返回组件的初始状态
data () {
return {
openKeys: [],
selectedKeys: [],
cachedOpenKeys: []
openKeys: [], // 展开的菜单项的key数组
selectedKeys: [], // 选中的菜单项的key数组
cachedOpenKeys: [] // 缓存的展开项的key数组
}
},
// 计算属性返回menu的根路径数组
computed: {
rootSubmenuKeys: vm => {
const keys = []
vm.menu.forEach(item => keys.push(item.path))
return keys
SubmenuKeys: vm => {
const keys = [];
vm.menu.forEach(item => keys.push(item.path));
return keys;
}
},
// mounted生命周期钩子组件挂载后调用updateMenu方法
mounted () {
this.updateMenu()
this.updateMenu();
},
// 监听器监听collapsed变化路由变化以及处理菜单项改变
watch: {
collapsed (val) {
if (val) {
this.cachedOpenKeys = this.openKeys.concat()
this.openKeys = []
this.cachedOpenKeys = this.openKeys.concat();
this.openKeys = [];
} else {
this.openKeys = this.cachedOpenKeys
this.openKeys = this.cachedOpenKeys;
}
},
$route: function () {
this.updateMenu()
this.updateMenu();
}
},
// 定义methods包括处理菜单项改变更新菜单渲染菜单项和子菜单项
methods: {
// select menu item
// 处理菜单项改变
onOpenChange (openKeys) {
// 在水平模式下时执行,并且不再执行后续
if (this.mode === 'horizontal') {
this.openKeys = openKeys
return
}
// 非水平模式时
const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
this.openKeys = openKeys
} else {
this.openKeys = latestOpenKey ? [latestOpenKey] : []
}
// ...
},
updateMenu () {
const routes = this.$route.matched.concat()
const { hidden } = this.$route.meta
if (routes.length >= 3 && hidden) {
routes.pop()
this.selectedKeys = [routes[routes.length - 1].path]
} else {
this.selectedKeys = [routes.pop().path]
}
const openKeys = []
if (this.mode === 'inline') {
routes.forEach(item => {
openKeys.push(item.path)
})
}
this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
const routes = this.$route.matched.concat();
// ...
},
// render
renderItem (menu) {
if (!menu.hidden) {
return menu.children && !menu.hideChildrenInMenu ? this.renderSubMenu(menu) : this.renderMenuItem(menu)
}
return null
// 渲染菜单项
// ...
},
renderMenuItem (menu) {
const target = menu.meta.target || null
const tag = target && 'a' || 'router-link'
const props = { to: { name: menu.name } }
const attrs = { href: menu.path, target: menu.meta.target }
if (menu.children && menu.hideChildrenInMenu) {
// 把有子菜单的 并且 父菜单是要隐藏子菜单的
// 都给子菜单增加一个 hidden 属性
// 用来给刷新页面时, selectedKeys 做控制用
menu.children.forEach(item => {
item.meta = Object.assign(item.meta, { hidden: true })
})
}
return (
<Item {...{ key: menu.path }}>
<tag {...{ props, attrs }}>
{this.renderIcon(menu.meta.icon)}
<span>{menu.meta.title}</span>
</tag>
</Item>
)
// 渲染菜单项组件
// ...
},
renderSubMenu (menu) {
const itemArr = []
if (!menu.hideChildrenInMenu) {
menu.children.forEach(item => itemArr.push(this.renderItem(item)))
}
return (
<SubMenu {...{ key: menu.path }}>
<span slot="title">
{this.renderIcon(menu.meta.icon)}
<span>{menu.meta.title}</span>
</span>
{itemArr}
</SubMenu>
)
// 渲染子菜单组件
// ...
},
renderIcon (icon) {
if (icon === 'none' || icon === undefined) {
return null
}
const props = {}
typeof (icon) === 'object' ? props.component = icon : props.type = icon
return (
<Icon {... { props } }/>
)
// 渲染图标组件
// ...
}
},
// 定义render函数返回组件的最终渲染结果
render () {
const { mode, theme, menu } = this
const { mode, theme, menu } = this;
const props = {
mode: mode,
theme: theme,
openKeys: this.openKeys
}
};
const on = {
select: obj => {
this.selectedKeys = obj.selectedKeys
this.$emit('select', obj)
this.selectedKeys = obj.selectedKeys;
this.$emit('select', obj);
},
openChange: this.onOpenChange
}
};
const menuTree = menu.map(item => {
if (item.hidden) {
return null
return null;
}
return this.renderItem(item)
})
// {...{ props, on: on }}
return this.renderItem(item);
});
// 使用Menu组件渲染最终的菜单结构
return (
<Menu vModel={this.selectedKeys} {...{ props, on: on }}>
{menuTree}
</Menu>
)
);
}
}
}

@ -1,10 +1,14 @@
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
// 导入Ant Design Vue的Menu和Icon组件
import Menu from 'ant-design-vue/es/menu';
import Icon from 'ant-design-vue/es/icon';
const { Item, SubMenu } = Menu
// 从Menu组件中解构出Item和SubMenu
const { Item, SubMenu } = Menu;
export default {
// 组件名称
name: 'SMenu',
// 定义props包括menu菜单项数组theme主题mode模式collapsed折叠状态
props: {
menu: {
type: Array,
@ -26,112 +30,73 @@ export default {
default: false
}
},
// data函数返回组件的初始状态
data () {
return {
openKeys: [],
selectedKeys: [],
cachedOpenKeys: []
openKeys: [], // 展开的菜单项的key数组
selectedKeys: [], // 选中的菜单项的key数组
cachedOpenKeys: [] // 缓存的展开项的key数组
}
},
// 计算属性返回menu的根路径数组
computed: {
rootSubmenuKeys: vm => {
const keys = []
vm.menu.forEach(item => keys.push(item.path))
return keys
const keys = [];
vm.menu.forEach(item => keys.push(item.path));
return keys;
}
},
// created生命周期钩子组件创建后调用updateMenu方法
created () {
this.updateMenu()
this.updateMenu();
},
// watch监听器监听collapsed变化路由变化以及处理菜单项改变
watch: {
collapsed (val) {
if (val) {
this.cachedOpenKeys = this.openKeys.concat()
this.openKeys = []
this.cachedOpenKeys = this.openKeys.concat();
this.openKeys = [];
} else {
this.openKeys = this.cachedOpenKeys
this.openKeys = this.cachedOpenKeys;
}
},
$route: function () {
this.updateMenu()
this.updateMenu();
}
},
// 定义methods包括处理菜单项改变更新菜单渲染菜单项和子菜单项渲染图标等
methods: {
// 渲染图标的方法
renderIcon: function (h, icon) {
if (icon === 'none' || icon === undefined) {
return null
return null;
}
const props = {}
typeof (icon) === 'object' ? props.component = icon : props.type = icon
return h(Icon, { props: { ...props } })
const props = {};
typeof (icon) === 'object' ? props.component = icon : props.type = icon;
return h(Icon, { props: { ...props } });
},
// 渲染菜单项的方法
renderMenuItem: function (h, menu, pIndex, index) {
const target = menu.meta.target || null
return h(Item, { key: menu.path ? menu.path : 'item_' + pIndex + '_' + index }, [
h('router-link', { attrs: { to: { name: menu.name }, target: target } }, [
this.renderIcon(h, menu.meta.icon),
h('span', [menu.meta.title])
])
])
// ...
},
// 渲染子菜单的方法
renderSubMenu: function (h, menu, pIndex, index) {
const this2_ = this
const subItem = [h('span', { slot: 'title' }, [this.renderIcon(h, menu.meta.icon), h('span', [menu.meta.title])])]
const itemArr = []
const pIndex_ = pIndex + '_' + index
console.log('menu', menu)
if (!menu.hideChildrenInMenu) {
menu.children.forEach(function (item, i) {
itemArr.push(this2_.renderItem(h, item, pIndex_, i))
})
}
return h(SubMenu, { key: menu.path ? menu.path : 'submenu_' + pIndex + '_' + index }, subItem.concat(itemArr))
// ...
},
// 渲染菜单项的方法
renderItem: function (h, menu, pIndex, index) {
if (!menu.hidden) {
return menu.children && !menu.hideChildrenInMenu
? this.renderSubMenu(h, menu, pIndex, index)
: this.renderMenuItem(h, menu, pIndex, index)
}
// ...
},
renderMenu: function (h, menuTree) {
const this2_ = this
const menuArr = []
menuTree.forEach(function (menu, i) {
if (!menu.hidden) {
menuArr.push(this2_.renderItem(h, menu, '0', i))
}
})
return menuArr
// 更新菜单的方法
updateMenu: function () {
// ...
},
onOpenChange (openKeys) {
const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
this.openKeys = openKeys
} else {
this.openKeys = latestOpenKey ? [latestOpenKey] : []
}
// 处理菜单项改变的方法
onOpenChange: function (openKeys) {
// ...
},
updateMenu () {
const routes = this.$route.matched.concat()
if (routes.length >= 4 && this.$route.meta.hidden) {
routes.pop()
this.selectedKeys = [routes[2].path]
} else {
this.selectedKeys = [routes.pop().path]
}
const openKeys = []
if (this.mode === 'inline') {
routes.forEach(item => {
openKeys.push(item.path)
})
}
this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
}
},
// render函数返回组件的最终渲染结果
render (h) {
return h(
Menu,
@ -153,4 +118,4 @@ export default {
this.renderMenu(h, this.menu)
)
}
}
}

@ -24,43 +24,58 @@
</div>
</template>
-->
<script>
export default {
//
name: 'MultiTab',
//
data () {
return {
fullPathList: [],
pages: [],
activeKey: '',
newTabIndex: 0
fullPathList: [], //
pages: [], // Vue
activeKey: '', // key
newTabIndex: 0 // 使
}
},
//
created () {
// pagesfullPathList
this.pages.push(this.$route)
this.fullPathList.push(this.$route.fullPath)
//
this.selectedLastPath()
},
methods: {
// action
onEdit (targetKey, action) {
this[action](targetKey)
},
// pagesfullPathList
remove (targetKey) {
//
this.pages = this.pages.filter(page => page.fullPath !== targetKey)
this.fullPathList = this.fullPathList.filter(path => path !== targetKey)
//
//
if (!this.fullPathList.includes(this.activeKey)) {
this.selectedLastPath()
}
},
//
selectedLastPath () {
this.activeKey = this.fullPathList[this.fullPathList.length - 1]
},
// content menu
//
closeThat (e) {
this.remove(e)
},
//
closeLeft (e) {
const currentIndex = this.fullPathList.indexOf(e)
if (currentIndex > 0) {
@ -73,6 +88,8 @@ export default {
this.$message.info('左侧没有标签')
}
},
//
closeRight (e) {
const currentIndex = this.fullPathList.indexOf(e)
if (currentIndex < (this.fullPathList.length - 1)) {
@ -85,6 +102,8 @@ export default {
this.$message.info('右侧没有标签')
}
},
//
closeAll (e) {
const currentIndex = this.fullPathList.indexOf(e)
this.fullPathList.forEach((item, index) => {
@ -93,6 +112,8 @@ export default {
}
})
},
//
closeMenuClick ({ key, item, domEvent }) {
const vkey = domEvent.target.getAttribute('data-vkey')
switch (key) {
@ -111,6 +132,8 @@ export default {
break
}
},
//
renderTabPaneMenu (e) {
return (
<a-menu {...{ on: { click: this.closeMenuClick } }}>
@ -121,7 +144,8 @@ export default {
</a-menu>
)
},
// render
//
renderTabPane (title, keyPath) {
const menu = this.renderTabPaneMenu(keyPath)
@ -132,18 +156,26 @@ export default {
)
}
},
//
watch: {
'$route': function (newVal) {
//
this.activeKey = newVal.fullPath
// fullPathList
if (this.fullPathList.indexOf(newVal.fullPath) < 0) {
this.fullPathList.push(newVal.fullPath)
this.pages.push(newVal)
}
},
// activeKey
activeKey: function (newPathKey) {
this.$router.push({ path: newPathKey })
}
},
//
render () {
const { onEdit, $data: { pages } } = this
const panes = pages.map(page => {
@ -160,16 +192,16 @@ export default {
<div class="ant-pro-multi-tab">
<div class="ant-pro-multi-tab-wrapper">
<a-tabs
hideAdd
type={'editable-card'}
v-model={this.activeKey}
tabBarStyle={{ background: '#FFF', margin: 0, paddingLeft: '16px', paddingTop: '1px' }}
{...{ on: { edit: onEdit } }}>
{panes}
hideAdd //
type={'editable-card'} //
v-model={this.activeKey} //
tabBarStyle={{ background: '#FFF', margin: 0, paddingLeft: '16px', paddingTop: '1px' }} //
{...{ on: { edit: onEdit } }}> //
{panes} //
</a-tabs>
</div>
</div>
)
}
}
</script>
</script>

@ -1,4 +1,9 @@
import MultiTab from './MultiTab'
import './index.less'
// 导入名为 MultiTab 的Vue组件该组件定义在当前目录下的 MultiTab.vue 文件中
import MultiTab from './MultiTab';
export default MultiTab
// 导入index.less样式表文件用于为MultiTab组件提供样式定义
import './index.less';
// 将导入的 MultiTab 组件设置为默认导出
// 这样其他文件就可以通过 import MultiTab 来使用这个组件
export default MultiTab;

@ -1,25 +1,34 @@
// 导入外部样式表,可能是包含通用样式或变量定义的文件
@import '../index';
// 定义多标签组件的前缀类用于构建CSS选择器
@multi-tab-prefix-cls: ~"@{ant-pro-prefix}-multi-tab";
@multi-tab-wrapper-prefix-cls: ~"@{ant-pro-prefix}-multi-tab-wrapper";
/*
* 定义多标签组件在.topmenu类下的样式
* 设置最大宽度为1200px
* 设置左右边距,左边距为-23px右边距为24px自动居中
*/
.topmenu .@{multi-tab-prefix-cls} {
max-width: 1200px;
margin: -23px auto 24px auto;
}
*/
// 为多标签组件设置通用样式
.@{multi-tab-prefix-cls} {
margin: -23px -24px 24px -24px;
background: #fff;
margin: -23px -24px 24px -24px; // 设置四个方向的边距
background: #fff; // 背景颜色为白色
}
// 定义多标签组件在.topmenu类下的多标签包装器的样式
.topmenu .@{multi-tab-wrapper-prefix-cls} {
max-width: 1200px;
margin: 0 auto;
max-width: 1200px; // 最大宽度为1200px
margin: 0 auto; // 水平居中对齐
}
// 当内容宽度是流体布局时设置多标签包装器的最大宽度为100%
.topmenu.content-width-Fluid .@{multi-tab-wrapper-prefix-cls} {
max-width: 100%;
margin: 0 auto;
}
max-width: 100%; // 最大宽度为100%
margin: 0 auto; // 水平居中对齐
}

@ -1,4 +1,5 @@
<template>
<!-- 使用 a-popover 组件创建一个弹出框以下是对其配置属性的设置 -->
<a-popover
v-model="visible"
trigger="click"
@ -8,13 +9,21 @@
:arrowPointAtCenter="true"
:overlayStyle="{ width: '300px', top: '50px' }"
>
<!-- 定义弹出框内容的插槽 -->
<template slot="content">
<!-- 使用 a-spin 组件根据 loadding 变量的值来显示加载状态 -->
<a-spin :spinning="loadding">
<!-- 使用 a-tabs 组件创建选项卡 -->
<a-tabs>
<!-- 第一个选项卡通知的配置 -->
<a-tab-pane tab="通知" key="1">
<!-- 使用 a-list 组件创建列表 -->
<a-list>
<!-- 列表中的一个列表项 -->
<a-list-item>
<!-- a-list-item-meta 用于展示列表项中的元信息如标题描述等 -->
<a-list-item-meta title="你收到了 14 份新周报" description="一年前">
<!-- 使用 a-avatar 组件展示头像设置了背景色和头像图片来源 -->
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png"/>
</a-list-item-meta>
</a-list-item>
@ -30,17 +39,22 @@
</a-list-item>
</a-list>
</a-tab-pane>
<!-- 第二个选项卡消息的配置内容暂时为简单文本123 -->
<a-tab-pane tab="消息" key="2">
123
</a-tab-pane>
<!-- 第三个选项卡待办的配置内容暂时为简单文本123 -->
<a-tab-pane tab="待办" key="3">
123
</a-tab-pane>
</a-tabs>
</a-spin>
</template>
<!-- 点击此元素会触发 fetchNotice 方法同时添加了 header-notice 类名 -->
<span @click="fetchNotice" class="header-notice">
<!-- 使用 a-badge 组件展示一个带有数字的徽标这里数字为 12 -->
<a-badge count="12">
<!-- 使用 a-icon 组件展示一个铃铛图标设置了字体大小和内边距 -->
<a-icon style="font-size: 16px; padding: 4px" type="bell" />
</a-badge>
</span>
@ -52,38 +66,48 @@ export default {
name: 'HeaderNotice',
data () {
return {
// loadding false
loadding: false,
// visible a-popover false
visible: false
}
},
methods: {
fetchNotice () {
fetchNotice() {
//
if (!this.visible) {
this.loadding = true
// loadding true
this.loadding = true;
// loadding false
setTimeout(() => {
this.loadding = false
}, 2000)
this.loadding = false;
}, 2000);
} else {
this.loadding = false
// loadding false
this.loadding = false;
}
this.visible = !this.visible
//
this.visible = !this.visible;
}
}
}
</script>
<style lang="css">
.header-notice-wrapper {
top: 50px !important;
}
/* 针对类名为 header-notice-wrapper 的元素设置样式,强制将 top 值设为 50px覆盖可能存在的其他样式 */
.header-notice-wrapper {
top: 50px !important;
}
</style>
<style lang="less" scoped>
.header-notice{
display: inline-block;
transition: all 0.3s;
/* 对类名为 header-notice 的元素设置样式,使其显示为内联块元素,并设置过渡效果 */
.header-notice {
display: inline-block;
transition: all 0.3s;
span {
vertical-align: initial;
}
span {
// span
vertical-align: initial;
}
</style>
}
</style>

@ -1,2 +1,10 @@
// 从当前目录下的'./NoticeIcon'文件中导入名为NoticeIcon的模块或组件。
// './NoticeIcon'指的是与当前文件同一目录下的NoticeIcon文件可能是一个React组件
// 这里的导入路径是相对于当前文件的相对路径。
import NoticeIcon from './NoticeIcon'
export default NoticeIcon
// 导出NoticeIcon模块或组件使得其他文件可以通过import语句来引用它。
// 使用export default语法表示默认导出这意味着在导入时不需要使用花括号{}。
// 这使得其他文件可以通过类似import SomeName from './path/to/this/file'的方式导入NoticeIcon
// 其中SomeName可以是任意名称但导入的内容将是这里导出的NoticeIcon。
export default NoticeIcon

@ -1,27 +1,41 @@
<template>
<!-- 创建一个带有 page-header 类名的 div 容器作为页面头部的外层包裹元素 -->
<div class="page-header">
<!-- 创建一个带有 page-header-index-wide 类名的 div 容器可能用于页面头部的特定布局或样式控制 -->
<div class="page-header-index-wide">
<!-- 使用 s-breadcrumb 组件可能用于展示面包屑导航具体功能取决于该组件的实现 -->
<s-breadcrumb />
<!-- 创建一个名为 detail div 容器用于放置页面头部更详细的内容 -->
<div class="detail">
<!-- 条件判断如果路由元信息中的 hiddenHeaderContent false则展示以下内容 -->
<div class="main" v-if="!$route.meta.hiddenHeaderContent">
<!-- 创建一个 row 类名的 div 容器可能用于布局类似行的概念 -->
<div class="row">
<!-- 如果 logo 属性有值则展示对应的图片图片的源地址由 logo 属性绑定并且添加 logo 类名用于样式控制 -->
<img v-if="logo" :src="logo" class="logo"/>
<!-- 如果 title 属性有值则展示对应的标题内容使用双大括号进行数据绑定展示 title 属性的值并添加 title 类名用于样式控制 -->
<h1 v-if="title" class="title">{{ title }}</h1>
<!-- 创建一个名为 action div 容器用于放置具名插槽内容可能是一些操作按钮之类的元素 -->
<div class="action">
<slot name="action"></slot>
</div>
</div>
<!-- 再创建一个 row 类名的 div 容器继续放置页面头部的其他元素 -->
<div class="row">
<!-- 如果 avatar 属性有值则展示对应的头像元素使用 a-avatar 组件展示图片源地址由 avatar 属性绑定 -->
<div v-if="avatar" class="avatar">
<a-avatar :src="avatar" />
</div>
<!-- 如果有名为 content 的插槽内容则展示对应的插槽内容并添加 headerContent 类名用于样式控制 -->
<div v-if="this.$slots.content" class="headerContent">
<slot name="content"></slot>
</div>
<!-- 如果有名为 extra 的插槽内容则展示对应的插槽内容并添加 extra 类名用于样式控制 -->
<div v-if="this.$slots.extra" class="extra">
<slot name="extra"></slot>
</div>
</div>
<!-- 放置一个用于名为 pageMenu 的插槽内容的容器可能用于展示页面相关的菜单选项等 -->
<div>
<slot name="pageMenu"></slot>
</div>
@ -32,24 +46,29 @@
</template>
<script>
// '../../components/tools/Breadcrumb' Breadcrumb
import Breadcrumb from '../../components/tools/Breadcrumb'
export default {
name: 'PageHeader',
components: {
// Breadcrumb s-breadcrumb 便使
's-breadcrumb': Breadcrumb
},
props: {
// title true
title: {
type: [String, Boolean],
default: true,
required: false
},
// logo
logo: {
type: String,
default: '',
required: false
},
// avatar
avatar: {
type: String,
default: '',
@ -63,89 +82,131 @@ export default {
</script>
<style lang="less" scoped>
// page-header
.page-header {
//
background: #fff;
// 16px 32px 0
padding: 16px 32px 0;
// 1px#e8e8e8
border-bottom: 1px solid #e8e8e8;
.breadcrumb {
// 16px
margin-bottom: 16px;
}
.detail {
// 便
display: flex;
/*margin-bottom: 16px;*/
.avatar {
// 72px
flex: 0 1 72px;
// 24px 8px
margin: 0 24px 8px 0;
& > span {
// 72px使
border-radius: 72px;
//
display: block;
// 72px
width: 72px;
// 72px
height: 72px;
}
}
.main {
//
width: 100%;
//
flex: 0 1 auto;
.row {
// 便
display: flex;
//
width: 100%;
.avatar {
// 16px
margin-bottom: 16px;
}
}
.title {
// 20px
font-size: 20px;
// 500
font-weight: 500;
font-size: 20px;
line-height: 28px;
font-weight: 500;
// rgba
color: rgba(0, 0, 0, 0.85);
// 16px
margin-bottom: 16px;
//
flex: auto;
}
.logo {
// 28px
width: 28px;
// 28px
height: 28px;
// 4px
border-radius: 4px;
// 16px
margin-right: 16px;
}
.content,
.headerContent {
//
flex: auto;
// rgba
color: rgba(0, 0, 0, 0.45);
// 22px
line-height: 22px;
.link {
// 16px
margin-top: 16px;
// 24px
line-height: 24px;
a {
// 14px
font-size: 14px;
// 32px
margin-right: 32px;
}
}
}
.extra {
//
flex: 0 1 auto;
// 88px
margin-left: 88px;
// 242px
min-width: 242px;
//
text-align: right;
}
.action {
// 56px
margin-left: 56px;
// 266px
min-width: 266px;
//
flex: 0 1 auto;
//
text-align: right;
&:empty {
//
display: none;
}
}
@ -153,50 +214,69 @@ export default {
}
}
.mobile .page-header {
// mobile page-header
.mobile.page-header {
.main {
.row {
//
flex-wrap: wrap;
.avatar {
//
flex: 0 1 25%;
// 2% 8px
margin: 0 2% 8px 0;
}
.content,
.headerContent {
//
flex: 0 1 70%;
.link {
// 16px
margin-top: 16px;
// 24px
line-height: 24px;
a {
// 14px
font-size: 14px;
// 10px
margin-right: 10px;
}
}
}
.extra {
// 使
flex: 1 1 auto;
//
margin-left: 0;
// 0
min-width: 0;
//
text-align: right;
}
.action {
//
margin-left: unset;
// 266px
min-width: 266px;
//
flex: 0 1 auto;
//
text-align: left;
// 12px
margin-bottom: 12px;
&:empty {
//
display: none;
}
}
}
}
}
</style>
</style>

@ -1,2 +1,5 @@
import PageHeader from './PageHeader'
export default PageHeader
// 从当前目录下的 PageHeader 文件(通常是.js、.vue等相关的模块文件具体取决于项目配置中导入 PageHeader 模块(可能是一个组件、函数或者类等,具体要看 PageHeader 文件中导出的内容是什么)
import PageHeader from './PageHeader';
// 将导入的 PageHeader 模块原样导出,这样在其他模块中可以通过相应的导入语句引入这个 PageHeader方便复用该模块所代表的功能或组件等内容
export default PageHeader;

@ -1,10 +1,13 @@
import { Spin } from 'ant-design-vue'
// 'ant-design-vue' UI Spin Spin
import { Spin } from 'ant-design-vue';
export default {
name: 'PageLoading',
render () {
// render Spin DOM Vue 使 JSX
return (<div style={{ paddingTop: 100, textAlign: 'center' }}>
// 使 Spin size 'large'使
<Spin size="large" />
</div>)
}
}
}

@ -1,21 +1,32 @@
<template>
<!-- 创建一个带有 result 类名的 div 容器作为整个结果展示组件的外层包裹元素 -->
<div class="result">
<!-- 内部的一个 div 容器用于放置图标相关元素 -->
<div>
<a-icon :class="{ 'icon': true, [`${type}`]: true }" :type="localIsSuccess ? 'check-circle' : 'close-circle'"/>
<!-- 使用 a-icon 组件来展示图标通过动态绑定 class type 属性来根据不同情况显示相应图标 -->
<!-- 根据表达式判断如果 localIsSuccess true则显示成功图标check-circle否则显示失败图标close-circle -->
<!-- 同时根据 type 属性的值动态添加对应的类名success error用于样式控制 -->
<a-icon :class="{ 'icon': true, [`${type}`]: true }" :type="localIsSuccess? 'check-circle' : 'close-circle'"/>
</div>
<!-- 创建一个带有 title 类名的 div 容器用于展示标题内容 -->
<div class="title">
<!-- 使用具名插槽name="title"如果外部在使用该组件时传入了名为 title 的插槽内容则优先显示插槽内容否则显示组件内部绑定的 title 属性值 -->
<slot name="title">
{{ title }}
</slot>
</div>
<!-- 创建一个带有 description 类名的 div 容器用于展示描述性文字内容 -->
<div class="description">
<!-- 同样使用具名插槽name="description"若外部传入相应插槽内容则展示插槽内容否则展示组件内部绑定的 description 属性值 -->
<slot name="description">
{{ description }}
</slot>
</div>
<!-- 创建一个带有 extra 类名的 div 容器通过 v-if 指令判断如果组件使用时有默认插槽$slots.default 存在则展示默认插槽内容 -->
<div class="extra" v-if="$slots.default">
<slot></slot>
</div>
<!-- 创建一个带有 action 类名的 div 容器利用 v-if 指令判断当存在名为 action 的插槽$slots.action 存在展示 action 插槽内容 -->
<div class="action" v-if="$slots.action">
<slot name="action"></slot>
</div>
@ -23,33 +34,39 @@
</template>
<script>
// resultEnum 'success' 'error'
const resultEnum = ['success', 'error']
export default {
name: 'Result',
props: {
/** @Deprecated */
/** @Deprecated 表示该属性已被弃用,建议后续不再使用此属性,此处定义的 isSuccess 用于表示结果是否成功 */
isSuccess: {
type: Boolean,
default: false
},
// type resultEnum 'success'
type: {
type: String,
default: resultEnum[0],
validator (val) {
// type resultEnum
validator(val) {
return (val) => resultEnum.includes(val)
}
},
// title
title: {
type: String,
default: ''
},
// description
description: {
type: String,
default: ''
}
},
computed: {
// localIsSuccess type resultEnum 'success'
localIsSuccess: function () {
return this.type === resultEnum[0]
}
@ -58,52 +75,71 @@ export default {
</script>
<style lang="less" scoped>
.result {
text-align: center;
width: 72%;
margin: 0 auto;
padding: 24px 0 8px;
/* 对类名为 result 的元素设置样式 */
.result {
//
text-align: center;
// 72%
width: 72%;
//
margin: 0 auto;
// 24px 8px
padding: 24px 0 8px;
.icon {
font-size: 72px;
line-height: 72px;
margin-bottom: 24px;
}
.success {
color: #52c41a;
}
.error {
color: red;
}
.title {
font-size: 24px;
color: rgba(0, 0, 0, .85);
font-weight: 500;
line-height: 32px;
margin-bottom: 16px;
}
.description {
font-size: 14px;
line-height: 22px;
color: rgba(0, 0, 0, 0.45);
margin-bottom: 24px;
}
.extra {
background: #fafafa;
padding: 24px 40px;
border-radius: 2px;
text-align: left;
}
.action {
margin-top: 32px;
}
.icon {
// 72px 72px使 24px
font-size: 72px;
line-height: 72px;
margin-bottom: 24px;
}
.mobile {
.result {
width: 100%;
margin: 0 auto;
padding: unset;
}
.success {
// success 绿#52c41a
color: #52c41a;
}
.error {
// error
color: red;
}
.title {
// 24pxrgba 500 32px 16px
font-size: 24px;
color: rgba(0, 0, 0, .85);
font-weight: 500;
line-height: 32px;
margin-bottom: 16px;
}
.description {
// 14px 22pxrgba 24px
font-size: 14px;
line-height: 22px;
color: rgba(0, 0, 0, 0.45);
margin-bottom: 24px;
}
.extra {
// #fafafa 40px 2px
background: #fafafa;
padding: 24px 40px;
border-radius: 2px;
text-align: left;
}
.action {
// 32px便
margin-top: 32px;
}
</style>
}
.mobile {
.result {
// mobile result 100%unset
width: 100%;
margin: 0 auto;
padding: unset;
}
}
</style>

@ -1,2 +1,5 @@
import Result from './Result.vue'
export default Result
// 从当前目录下的 Result.vue 文件中导入名为 Result 的模块(通常在 Vue 项目里Result.vue 文件会定义一个 Vue 组件)
import Result from './Result.vue';
// 将导入的 Result 模块(也就是那个 Vue 组件)原样导出,这样在其他模块中就能通过相应的导入语句引入这个 Result 组件,便于在项目的其他地方复用该组件
export default Result;

@ -1,5 +1,7 @@
<template>
<!-- 创建一个带有 setting-drawer 类名的 div 容器并添加 ref 属性为 settingDrawer方便后续在 JavaScript 代码中通过 $refs 获取该元素 -->
<div class="setting-drawer" ref="settingDrawer">
<!-- 使用 a-drawer 组件可能来自 Ant Design Vue 等相关 UI 组件库创建一个抽屉式的侧边栏组件以下是对其属性的配置 -->
<a-drawer
width="300"
placement="right"
@ -9,18 +11,27 @@
:getContainer="() => $refs.settingDrawer"
:style="{}"
>
<!-- 创建一个名为 setting-drawer-index-content div 容器用于放置抽屉内的各种设置内容 -->
<div class="setting-drawer-index-content">
<!-- 创建一个 div 容器设置底部外边距为 24px用于对整体风格设置部分进行整体布局 -->
<div :style="{ marginBottom: '24px' }">
<!-- 创建一个 h3 标题元素展示整体风格设置的标题内容 -->
<h3 class="setting-drawer-index-title">整体风格设置</h3>
<!-- 创建一个名为 setting-drawer-index-blockChecbox div 容器用于放置与整体风格相关的选项元素设置为弹性布局display: flex方便内部元素排列 -->
<div class="setting-drawer-index-blockChecbox">
<!-- 使用 a-tooltip 组件用于展示提示信息的工具提示组件当鼠标悬停在对应元素上时显示提示内容 -->
<a-tooltip>
<!-- 定义 a-tooltip 组件的提示内容插槽 -->
<template slot="title">
暗色菜单风格
</template>
<!-- 创建一个名为 setting-drawer-index-item div 容器用于单个风格选项的展示添加点击事件点击时调用 handleMenuTheme 方法并传入 'dark' 参数 -->
<div class="setting-drawer-index-item" @click="handleMenuTheme('dark')">
<!-- 展示对应风格的图片设置图片的源地址 -->
<img src="https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg" alt="dark">
<!-- 创建一个名为 setting-drawer-index-selectIcon div 容器用于显示选中图标 navTheme 'dark' 时显示通过 v-if 指令进行条件渲染 -->
<div class="setting-drawer-index-selectIcon" v-if="navTheme === 'dark'">
<a-icon type="check"/>
</div>
@ -33,7 +44,7 @@
</template>
<div class="setting-drawer-index-item" @click="handleMenuTheme('light')">
<img src="https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg" alt="light">
<div class="setting-drawer-index-selectIcon" v-if="navTheme !== 'dark'">
<div class="setting-drawer-index-selectIcon" v-if="navTheme!== 'dark'">
<a-icon type="check"/>
</div>
</div>
@ -41,26 +52,34 @@
</div>
</div>
<!-- 再创建一个 div 容器设置底部外边距为 24px用于对主题色设置部分进行整体布局 -->
<div :style="{ marginBottom: '24px' }">
<h3 class="setting-drawer-index-title">主题色</h3>
<!-- 创建一个高度为 20px div 容器用于放置主题色相关的选项元素 -->
<div style="height: 20px">
<!-- 使用 v-for 指令遍历 colorList 数组为每个颜色选项创建一个 a-tooltip 组件用于展示颜色名称的提示信息 -->
<a-tooltip class="setting-drawer-theme-color-colorBlock" v-for="(item, index) in colorList" :key="index">
<template slot="title">
{{ item.key }}
</template>
<!-- 使用 a-tag 组件展示每个颜色选项设置颜色属性为当前颜色项的 color 添加点击事件点击时调用 changeColor 方法并传入当前颜色值 -->
<a-tag :color="item.color" @click="changeColor(item.color)">
<!-- 如果当前颜色项的 color 值与 primaryColor 相等则显示一个选中图标a-icon -->
<a-icon type="check" v-if="item.color === primaryColor"></a-icon>
</a-tag>
</a-tooltip>
</div>
</div>
<!-- 使用 a-divider 组件绘制一条分割线用于区分不同的设置板块 -->
<a-divider />
<!-- 创建一个 div 容器设置底部外边距为 24px用于对导航模式设置部分进行整体布局 -->
<div :style="{ marginBottom: '24px' }">
<h3 class="setting-drawer-index-title">导航模式</h3>
<!-- 创建一个名为 setting-drawer-index-blockChecbox div 容器用于放置与导航模式相关的选项元素设置为弹性布局方便内部元素排列 -->
<div class="setting-drawer-index-blockChecbox">
<a-tooltip>
<template slot="title">
@ -80,29 +99,35 @@
</template>
<div class="setting-drawer-index-item" @click="handleLayout('topmenu')">
<img src="https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg" alt="topmenu">
<div class="setting-drawer-index-selectIcon" v-if="layoutMode !== 'sidemenu'">
<div class="setting-drawer-index-selectIcon" v-if="layoutMode!== 'sidemenu'">
<a-icon type="check"/>
</div>
</div>
</a-tooltip>
</div>
<!-- 创建一个 div 容器设置顶部外边距为 24px用于放置导航模式相关的更多设置选项 -->
<div :style="{ marginTop: '24px' }">
<!-- 使用 a-list 组件可能用于展示列表形式的设置项设置 split 属性为 false表示列表项之间不显示分割线 -->
<a-list :split="false">
<a-list-item>
<!-- 使用 a-tooltip 组件为当前设置项添加提示信息鼠标悬停时显示 -->
<a-tooltip slot="actions">
<template slot="title">
该设定仅 [顶部栏导航] 时有效
</template>
<!-- 使用 a-select 组件创建一个下拉选择框设置尺寸为 small宽度为 80px默认值为 contentWidth添加 change 事件当选项改变时调用 handleContentWidthChange 方法 -->
<a-select size="small" style="width: 80px;" :defaultValue="contentWidth" @change="handleContentWidthChange">
<a-select-option value="Fixed">固定</a-select-option>
<a-select-option value="Fluid" v-if="layoutMode !== 'sidemenu'"></a-select-option>
<a-select-option value="Fluid" v-if="layoutMode!== 'sidemenu'"></a-select-option>
</a-select>
</a-tooltip>
<!-- 使用 a-list-item-meta 组件展示列表项的元信息这里主要是设置标题内容 -->
<a-list-item-meta>
<div slot="title">内容区域宽度</div>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<!-- 使用 a-switch 组件创建一个开关按钮设置尺寸为 small默认选中状态为 fixedHeader 的值添加 change 事件当开关状态改变时调用 handleFixedHeader 方法 -->
<a-switch slot="actions" size="small" :defaultChecked="fixedHeader" @change="handleFixedHeader" />
<a-list-item-meta>
<div slot="title">固定 Header</div>
@ -113,14 +138,14 @@
<a-list-item-meta>
<a-tooltip slot="title" placement="left">
<template slot="title">固定 Header 时可配置</template>
<div :style="{ opacity: !fixedHeader ? '0.5' : '1' }">下滑时隐藏 Header</div>
<div :style="{ opacity:!fixedHeader? '0.5' : '1' }">下滑时隐藏 Header</div>
</a-tooltip>
</a-list-item-meta>
</a-list-item>
<a-list-item >
<a-switch slot="actions" size="small" :disabled="(layoutMode === 'topmenu')" :defaultChecked="fixSiderbar" @change="handleFixSiderbar" />
<a-list-item-meta>
<div slot="title" :style="{ textDecoration: layoutMode === 'topmenu' ? 'line-through' : 'unset' }">固定侧边菜单</div>
<div slot="title" :style="{ textDecoration: layoutMode === 'topmenu'? 'line-through' : 'unset' }">固定侧边菜单</div>
</a-list-item-meta>
</a-list-item>
</a-list>
@ -128,6 +153,7 @@
</div>
<a-divider />
<!-- 创建一个 div 容器设置底部外边距为 24px用于对其他设置部分进行整体布局 -->
<div :style="{ marginBottom: '24px' }">
<h3 class="setting-drawer-index-title">其他设置</h3>
<div>
@ -149,11 +175,13 @@
</div>
<a-divider />
<div :style="{ marginBottom: '24px' }">
<!-- 使用 a-button 组件创建一个按钮添加点击事件点击时调用 doCopy 方法设置显示图标为 'copy'并设置按钮占满父元素宽度block 属性按钮文本为拷贝设置 -->
<a-button
@click="doCopy"
icon="copy"
block
>拷贝设置</a-button>
<!-- 使用 a-alert 组件创建一个警告提示框设置提示类型为 'warning'设置顶部外边距为 24px -->
<a-alert type="warning" :style="{ marginTop: '24px' }">
<span slot="message">
配置栏只在开发环境用于预览生产环境不会展现请手动修改配置文件
@ -162,8 +190,11 @@
</a-alert>
</div>
</div>
<!-- 创建一个名为 setting-drawer-index-handle div 容器用于控制抽屉的显示和隐藏添加点击事件点击时调用 toggle 方法 -->
<div class="setting-drawer-index-handle" @click="toggle">
<!-- 如果抽屉当前不可见visible false则显示设置图标a-icon -->
<a-icon type="setting" v-if="!visible"/>
<!-- 如果抽屉当前可见visible true则显示关闭图标a-icon -->
<a-icon type="close" v-else/>
</div>
</a-drawer>
@ -171,22 +202,31 @@
</template>
<script>
import { DetailList } from '../../components'
import SettingItem from './SettingItem'
import config from '../../config/defaultSettings'
import { updateTheme, updateColorWeak, colorList } from './settingConfig'
import { mixin, mixinDevice } from '../../utils/mixin'
// '../../components' DetailList
import { DetailList } from '../../components';
// SettingItem Vue SettingItem
import SettingItem from './SettingItem';
// '../../config/defaultSettings' config
import config from '../../config/defaultSettings';
// settingConfig updateThemeupdateColorWeakcolorList
import { updateTheme, updateColorWeak, colorList } from './settingConfig';
// '../../utils/mixin' mixin mixinDevice Vue
import { mixin, mixinDevice } from '../../utils/mixin';
export default {
components: {
// DetailList SettingItem 使使
DetailList,
SettingItem
},
mixins: [mixin, mixinDevice],
data () {
return {
// visible a-drawer true
visible: true,
// colorList colorList
colorList,
// Object.assign baseConfig config config
baseConfig: Object.assign({}, config)
}
},
@ -194,177 +234,45 @@ export default {
},
mounted () {
const vm = this
const vm = this;
// 使 setTimeout 16 visible false
setTimeout(() => {
vm.visible = false
}, 16)
//
if (this.primaryColor !== config.primaryColor) {
updateTheme(this.primaryColor)
vm.visible = false;
}, 16);
// primaryColor config primaryColor updateTheme primaryColor
if (this.primaryColor!== config.primaryColor) {
updateTheme(this.primaryColor);
}
if (this.colorWeak !== config.colorWeak) {
updateColorWeak(this.colorWeak)
// colorWeak config colorWeak updateColorWeak colorWeak
if (this.colorWeak!== config.colorWeak) {
updateColorWeak(this.colorWeak);
}
},
methods: {
showDrawer () {
this.visible = true
// visible true
this.visible = true;
},
onClose () {
this.visible = false
// visible false
this.visible = false;
},
toggle () {
this.visible = !this.visible
// visible
this.visible =!this.visible;
},
onColorWeak (checked) {
this.baseConfig.colorWeak = checked
this.$store.dispatch('ToggleWeak', checked)
updateColorWeak(checked)
// a-switch checked
// baseConfig colorWeak
this.baseConfig.colorWeak = checked;
// $store.dispatch 'ToggleWeak' Vuex 使 Vuex
this.$store.dispatch('ToggleWeak', checked);
// updateColorWeak
updateColorWeak(checked);
},
onMultiTab (checked) {
this.baseConfig.multiTab = checked
this.$store.dispatch('ToggleMultiTab', checked)
},
handleMenuTheme (theme) {
this.baseConfig.navTheme = theme
this.$store.dispatch('ToggleTheme', theme)
},
doCopy () {
const text = `export default {
primaryColor: '${this.baseConfig.primaryColor}', // primary color of ant design
navTheme: '${this.baseConfig.navTheme}', // theme for nav menu
layout: '${this.baseConfig.layout}', // nav menu position: sidemenu or topmenu
contentWidth: '${this.baseConfig.contentWidth}', // layout of content: Fluid or Fixed, only works when layout is topmenu
fixedHeader: ${this.baseConfig.fixedHeader}, // sticky header
fixSiderbar: ${this.baseConfig.fixSiderbar}, // sticky siderbar
autoHideHeader: ${this.baseConfig.autoHideHeader}, // auto hide header
colorWeak: ${this.baseConfig.colorWeak},
multiTab: ${this.baseConfig.multiTab},
production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true',
// vue-ls options
storageOptions: {
namespace: 'pro__',
name: 'ls',
storage: 'local',
}
}`
this.$copyText(text).then(message => {
console.log('copy', message)
this.$message.success('复制完毕')
}).catch(err => {
console.log('copy.err', err)
this.$message.error('复制失败')
})
},
handleLayout (mode) {
this.baseConfig.layout = mode
this.$store.dispatch('ToggleLayoutMode', mode)
//
//
this.handleFixSiderbar(false)
},
handleContentWidthChange (type) {
this.baseConfig.contentWidth = type
this.$store.dispatch('ToggleContentWidth', type)
},
changeColor (color) {
this.baseConfig.primaryColor = color
if (this.primaryColor !== color) {
this.$store.dispatch('ToggleColor', color)
updateTheme(color)
}
},
handleFixedHeader (fixed) {
this.baseConfig.fixedHeader = fixed
this.$store.dispatch('ToggleFixedHeader', fixed)
},
handleFixedHeaderHidden (autoHidden) {
this.baseConfig.autoHideHeader = autoHidden
this.$store.dispatch('ToggleFixedHeaderHidden', autoHidden)
},
handleFixSiderbar (fixed) {
if (this.layoutMode === 'topmenu') {
this.baseConfig.fixSiderbar = false
this.$store.dispatch('ToggleFixSiderbar', false)
return
}
this.baseConfig.fixSiderbar = fixed
this.$store.dispatch('ToggleFixSiderbar', fixed)
}
}
}
</script>
<style lang="less" scoped>
.setting-drawer-index-content {
.setting-drawer-index-blockChecbox {
display: flex;
.setting-drawer-index-item {
margin-right: 16px;
position: relative;
border-radius: 4px;
cursor: pointer;
img {
width: 48px;
}
.setting-drawer-index-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
padding-top: 15px;
padding-left: 24px;
height: 100%;
color: #1890ff;
font-size: 14px;
font-weight: 700;
}
}
}
.setting-drawer-theme-color-colorBlock {
width: 20px;
height: 20px;
border-radius: 2px;
float: left;
cursor: pointer;
margin-right: 8px;
padding-left: 0px;
padding-right: 0px;
text-align: center;
color: #fff;
font-weight: 700;
i {
font-size: 14px;
}
}
}
.setting-drawer-index-handle {
position: absolute;
top: 240px;
background: #1890ff;
width: 48px;
height: 48px;
right: 300px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
pointer-events: auto;
z-index: 1001;
text-align: center;
font-size: 16px;
border-radius: 4px 0 0 4px;
i {
color: rgb(255, 255, 255);
font-size: 20px;
}
}
</style>
// onColorWeak
// baseConfig multiTab
this.baseConfig.multiTab = checked;
// $store.dispatch 'ToggleMultiTab'
this.$store.dispatch('ToggleMulti

@ -1,21 +1,29 @@
<template>
<!-- 创建一个带有 setting-drawer-index-item 类名的 div 容器作为一个可复用的设置项组件的外层包裹元素 -->
<div class="setting-drawer-index-item">
<!-- 使用双括号插值语法展示 title 属性的值将其作为标题展示在 h3 元素中h3 元素添加了 setting-drawer-index-title 类名用于后续样式控制 -->
<h3 class="setting-drawer-index-title">{{ title }}</h3>
<!-- 使用默认插槽<slot>用于让使用该组件的父组件可以向此处插入自定义的内容增加组件的灵活性和复用性 -->
<slot></slot>
<!-- 使用 a-divider 组件绘制一条分割线通过 v-if 指令根据 divider 属性的值来决定是否显示该分割线 divider 属性为 true则显示分割线 -->
<a-divider v-if="divider"/>
</div>
</template>
<script>
export default {
name: 'SettingItem',
// props
props: {
title: {
// title String
type: String,
// title 使
default: ''
},
divider: {
// divider Boolean true false
type: Boolean,
// divider false线 true
default: false
}
}
@ -23,16 +31,17 @@ export default {
</script>
<style lang="less" scoped>
/* 对类名为 setting-drawer-index-item 的元素设置样式 */
.setting-drawer-index-item {
// 24px便
margin-bottom: 24px;
.setting-drawer-index-item {
margin-bottom: 24px;
.setting-drawer-index-title {
font-size: 14px;
color: rgba(0, 0, 0, .85);
line-height: 22px;
margin-bottom: 12px;
}
.setting-drawer-index-title {
// .setting-drawer-index-title 14pxrgba 22px 12px使
font-size: 14px;
color: rgba(0, 0, 0,.85);
line-height: 22px;
margin-bottom: 12px;
}
</style>
}
</style>

@ -1,2 +1,6 @@
import SettingDrawer from './SettingDrawer'
export default SettingDrawer
// 从当前目录下的“SettingDrawer”文件通常在 Vue 项目等环境中会是一个定义了相关组件、模块的文件,比如.vue 文件或者.js 文件等中导入名为“SettingDrawer”的模块。
// 具体这个模块代表的是一个组件、函数或者类等取决于“SettingDrawer”文件内部的定义情况。
import SettingDrawer from './SettingDrawer';
// 将导入的“SettingDrawer”模块原样导出这样在其他的模块或者文件中就可以通过相应的导入语句引入这个“SettingDrawer”方便复用其内部所定义的功能、组件等相关内容。
export default SettingDrawer;

@ -1,8 +1,12 @@
import { message } from 'ant-design-vue/es'
// 从 'ant-design-vue/es' 模块中导入名为'message' 的对象(在 Ant Design Vue 框架中message 通常用于展示各种提示信息,比如成功、失败、加载等提示消息)
import { message } from 'ant-design-vue/es';
// 原本可能用于导入项目中的默认配置信息,此处被注释掉了,可能暂时不需要或者后续再启用这个功能
// import defaultSettings from '../defaultSettings';
let lessNodesAppended
// 定义一个变量 lessNodesAppended用于标记是否已经向页面中添加了与 Less一种 CSS 预处理器)相关的节点元素,初始值为 false
let lessNodesAppended;
// 定义一个数组 colorList数组中的每个元素是一个对象用于表示不同的颜色选项。每个对象包含一个 key颜色名称用于展示给用户识别等和 color对应的十六进制颜色值属性例如可以用于主题颜色的选择列表等场景
const colorList = [
{
key: '薄暮', color: '#F5222D'
@ -28,68 +32,94 @@ const colorList = [
{
key: '酱紫', color: '#722ED1'
}
]
];
// 定义一个名为 updateTheme 的函数用于更新页面主题颜色接收一个表示主颜色primaryColor的参数
const updateTheme = primaryColor => {
// 以下代码块被注释掉了,原本的意图可能是在生产环境下不进行 Less 的编译操作,不过目前是处于注释状态,可根据实际需求决定是否启用
// Don't compile less in production!
/* if (process.env.NODE_ENV === 'production') {
return;
} */
// Determine if the component is remounted
// 如果传入的主颜色参数为空值,则直接返回,不进行后续主题更新操作
if (!primaryColor) {
return
return;
}
const hideMessage = message.loading('正在编译主题!', 0)
// 调用 message.loading 方法显示一个加载提示信息,提示内容为“正在编译主题!”,并且设置显示时长为 0可能表示一直显示直到手动隐藏常用于长时间操作的提示场景并将返回的隐藏函数赋值给 hideMessage 变量,方便后续操作完成后隐藏该提示
const hideMessage = message.loading('正在编译主题!', 0);
function buildIt () {
// 判断 window 对象上是否存在 less 属性less.js 库加载后会在 window 上添加相关属性和方法,用于操作 Less 样式编译等),如果不存在则直接返回,无法进行后续主题更新相关的 Less 操作
if (!window.less) {
return
return;
}
// 使用 setTimeout 设置一个延迟执行的定时器,延迟 200 毫秒后执行以下操作(可能是为了等待一些资源加载等情况)
setTimeout(() => {
// 通过 window.less 的 modifyVars 方法来修改 Less 变量,这里将 '@primary-color' 这个 Less 变量的值修改为传入的 primaryColor 参数值,用于改变主题颜色相关的样式变量定义
window.less
.modifyVars({
'@primary-color': primaryColor
})
// 如果修改成功,调用 hideMessage 函数隐藏之前显示的加载提示信息
.then(() => {
hideMessage()
hideMessage();
})
// 如果修改过程出现错误,先调用 message.error 方法显示一个错误提示消息“Failed to update theme”告知用户主题更新失败然后再调用 hideMessage 函数隐藏加载提示信息
.catch(() => {
message.error('Failed to update theme')
hideMessage()
})
}, 200)
message.error('Failed to update theme');
hideMessage();
});
}, 200);
}
// 判断是否已经添加过与 Less 相关的节点元素如果没有添加过lessNodesAppended 为 false
if (!lessNodesAppended) {
// insert less.js and color.less
const lessStyleNode = document.createElement('link')
const lessConfigNode = document.createElement('script')
const lessScriptNode = document.createElement('script')
lessStyleNode.setAttribute('rel', 'stylesheet/less')
lessStyleNode.setAttribute('href', '/color.less')
// 创建一个 link 元素,用于引入 Less 样式文件,这里将用于引入项目中的 'color.less' 文件(可能定义了基于 Less 的各种样式以及主题相关的变量等内容)
const lessStyleNode = document.createElement('link');
// 创建一个 script 元素,用于配置 Less 的相关运行环境设置,比如设置为异步加载、生产环境模式以及启用 JavaScript 与 Less 的交互等功能
const lessConfigNode = document.createElement('script');
// 创建一个 script 元素,用于引入 less.js 库的线上压缩版本(版本号为 3.8.1less.js 是用于在浏览器端编译 Less 样式的库
const lessScriptNode = document.createElement('script');
// 设置 link 元素的 rel 属性为'stylesheet/less',表示这是一个 Less 样式表的引用链接
lessStyleNode.setAttribute('rel', 'stylesheet/less');
// 设置 link 元素的 href 属性为 '/color.less',指定要引入的 Less 样式文件的路径
lessStyleNode.setAttribute('href', '/color.less');
// 通过 innerHTML 为 script 元素lessConfigNode设置内容定义了 window.less 对象的一些配置属性,比如设置为异步操作、指定环境为生产环境以及启用 JavaScript 功能等,这些配置会影响 less.js 在页面中的运行方式
lessConfigNode.innerHTML = `
window.less = {
async: true,
env: 'production',
javascriptEnabled: true
};
`
lessScriptNode.src = 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js'
lessScriptNode.async = true
`;
// 设置 script 元素lessScriptNode的 src 属性,指定引入 less.js 库的线上地址
lessScriptNode.src = 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js';
// 设置 script 元素lessScriptNode为异步加载模式这样不会阻塞页面其他资源的加载
lessScriptNode.async = true;
// 为 lessScriptNode 的 onload 事件绑定一个回调函数,当 less.js 库加载完成后会执行这个回调函数,在回调函数中调用 buildIt 函数来进行主题更新相关的 Less 变量修改操作,并且在执行完成后将 onload 事件的回调函数设置为 null避免重复执行
lessScriptNode.onload = () => {
buildIt()
lessScriptNode.onload = null
}
document.body.appendChild(lessStyleNode)
document.body.appendChild(lessConfigNode)
document.body.appendChild(lessScriptNode)
lessNodesAppended = true
buildIt();
lessScriptNode.onload = null;
};
// 将创建好的 link 元素lessStyleNode添加到页面的 body 元素中,使其生效,引入相关的 Less 样式文件
document.body.appendChild(lessStyleNode);
// 将配置 Less 运行环境的 script 元素lessConfigNode添加到页面的 body 元素中,使其配置生效
document.body.appendChild(lessConfigNode);
// 将引入 less.js 库的 script 元素lessScriptNode添加到页面的 body 元素中,开始加载 less.js 库
document.body.appendChild(lessScriptNode);
// 将 lessNodesAppended 标记变量设置为 true表示已经添加过相关的 Less 节点元素了,后续再次调用 updateTheme 函数时可以直接执行 buildIt 函数进行主题更新操作,无需重复添加节点元素
lessNodesAppended = true;
} else {
buildIt()
// 如果已经添加过 Less 相关的节点元素了,直接调用 buildIt 函数进行主题更新操作(即修改 Less 变量来更新主题颜色相关的样式)
buildIt();
}
}
};
// 定义一个名为 updateColorWeak 的函数用于更新页面是否应用弱色模式可能是为了满足一些特殊的视觉需求比如高对比度模式等接收一个表示是否启用弱色模式colorWeak的布尔值参数
const updateColorWeak = colorWeak => {
// document.body.className = colorWeak ? 'colorWeak' : '';
colorWeak ? document.body.classList.add('colorWeak') : document.body.classList.remove('colorWeak')
}
// 以下这行代码被注释掉了,原本的做法是通过直接设置 document.body 的 className 属性来添加或移除 'colorWeak' 类名,不过现在采用了更推荐的 classList 的方式来操作类名
// document.body.className = colorWeak? 'colorWeak' : '';
// 根据传入的 colorWeak 参数值,如果为 true则给 document.body 元素添加 'colorWeak' 类名,否则移除该类名,以此来切换页面的弱色模式相关的样式应用(通常会在 CSS 中定义了对应 'colorWeak' 类名的样式规则来实现弱色效果)
colorWeak? document.body.classList.add('colorWeak') : document.body.classList.remove('colorWeak');
};
export { updateTheme, colorList, updateColorWeak }
// 将 updateTheme、colorList、updateColorWeak 这三个函数或变量导出,方便在其他模块中引入并使用它们,例如在其他模块中可以调用 updateTheme 函数来更新主题颜色,使用 colorList 数组来展示颜色选择列表等
export { updateTheme, colorList, updateColorWeak };

@ -1,121 +1,166 @@
<template>
<!-- 创建一个 div 元素通过动态绑定 class 属性根据组件的不同属性状态来添加相应的类名prefixClslastClsblockClsgridCls 这些类名会根据组件内部的计算属性和传入的属性值来确定是否添加 -->
<div :class="[prefixCls, lastCls, blockCls, gridCls]">
<!-- 通过 v-if 指令判断 title 属性是否有值如果有值则创建一个带有 antd-pro-components-standard-form-row-index-label 类名的 div 容器用于展示标题内容 -->
<div v-if="title" class="antd-pro-components-standard-form-row-index-label">
<!-- span 元素内使用双括号插值语法展示 title 属性的值用于在页面上显示对应的标题文字 -->
<span>{{ title }}</span>
</div>
<!-- 创建一个带有 antd-pro-components-standard-form-row-index-content 类名的 div 容器用于放置通过插槽传入的内容 -->
<div class="antd-pro-components-standard-form-row-index-content">
<!-- 使用默认插槽<slot>允许外部组件在使用该组件时插入自定义的内容增强组件的复用性和灵活性 -->
<slot></slot>
</div>
</div>
</template>
<script>
// classes
const classes = [
'antd-pro-components-standard-form-row-index-standardFormRowBlock',
'antd-pro-components-standard-form-row-index-standardFormRowGrid',
'antd-pro-components-standard-form-row-index-standardFormRowLast'
]
];
export default {
name: 'StandardFormRow',
props: {
// prefixCls 'antd-pro-components-standard-form-row-index-standardFormRow'
prefixCls: {
type: String,
default: 'antd-pro-components-standard-form-row-index-standardFormRow'
},
// title undefined v-if
title: {
type: String,
default: undefined
},
// last true false
last: {
type: Boolean
},
// block
block: {
type: Boolean
},
// grid
grid: {
type: Boolean
}
},
computed: {
// lastCls this.last last true classes null
lastCls () {
return this.last ? classes[2] : null
return this.last? classes[2] : null;
},
// blockCls this.block classes true null
blockCls () {
return this.block ? classes[0] : null
return this.block? classes[0] : null;
},
// gridCls this.grid classes true null
gridCls () {
return this.grid ? classes[1] : null
return this.grid? classes[1] : null;
}
}
}
</script>
<style lang="less" scoped>
// index.less mixin便
@import '../index.less';
// antd-pro-components-standard-form-row-index-standardFormRow
.antd-pro-components-standard-form-row-index-standardFormRow {
// flexbox便
display: flex;
// 16px
margin-bottom: 16px;
// 16px
padding-bottom: 16px;
// 1px 线 @border-color-split index.less
border-bottom: 1px dashed @border-color-split;
/deep/ .ant-form-item {
// 使 /deep/ 穿 Vue .ant-form-item 24px
/deep/.ant-form-item {
margin-right: 24px;
}
/deep/ .ant-form-item-label label {
// 使 /deep/ .ant-form-item-label label 0 @text-color
/deep/.ant-form-item-label label {
margin-right: 0;
color: @text-color;
}
/deep/ .ant-form-item-label,
// .ant-form-item-label .ant-form-item-control 0 32px使
/deep/.ant-form-item-label,
.ant-form-item-control {
padding: 0;
line-height: 32px;
}
// antd-pro-components-standard-form-row-index-label
.antd-pro-components-standard-form-row-index-label {
//
flex: 0 0 auto;
// 24px
margin-right: 24px;
// @heading-color
color: @heading-color;
// @font-size-base
font-size: @font-size-base;
//
text-align: right;
& > span {
// span 使
display: inline-block;
// 32px
height: 32px;
// 32px使
line-height: 32px;
&::after {
// 使 ::after span
content: '';
}
}
}
// antd-pro-components-standard-form-row-index-content
.antd-pro-components-standard-form-row-index-content {
// 0
flex: 1 1 0;
/deep/ .ant-form-item:last-child {
// 使 /deep/ .ant-form-item:last-child 0
/deep/.ant-form-item:last-child {
margin-right: 0;
}
}
// antd-pro-components-standard-form-row-index-standardFormRowLast last
&.antd-pro-components-standard-form-row-index-standardFormRowLast {
// 使
margin-bottom: 0;
//
padding-bottom: 0;
//
border: none;
}
// antd-pro-components-standard-form-row-index-standardFormRowBlock blockCls block
&.antd-pro-components-standard-form-row-index-standardFormRowBlock {
/deep/ .ant-form-item,
// 使 /deep/ .ant-form-item div.ant-form-item-control-wrapper
/deep/.ant-form-item,
div.ant-form-item-control-wrapper {
display: block;
}
}
// antd-pro-components-standard-form-row-index-standardFormRowGrid gridCls grid
&.antd-pro-components-standard-form-row-index-standardFormRowGrid {
/deep/ .ant-form-item,
div.ant-form-item-control-wrapper {
display: block;
}
/deep/ .ant-form-item-label {
float: left;
}
// 使 /deep/ .ant-form-item div.ant-form-item-control-wrapper
/deep/.ant-form-item,
div.ant-form-item-control-wrapper {
display: block;
}
// 使 /deep/ .ant-form-item-label
/deep/.ant-form-item-label {
float: left;
}
}
}

@ -1,3 +1,6 @@
import StandardFormRow from './StandardFormRow'
// 从当前目录下的“StandardFormRow”文件在Vue项目等环境里通常这个文件会定义一个相关的组件、模块比如是一个.vue文件或者.js文件等中导入名为“StandardFormRow”的模块。
// 具体这个模块代表的是一个组件、函数或者类等取决于“StandardFormRow”文件内部的定义情况。
import StandardFormRow from './StandardFormRow';
export default StandardFormRow
// 将导入的“StandardFormRow”模块原样导出这样在其他的模块或者文件中就可以通过相应的导入语句引入这个“StandardFormRow”方便复用其内部所定义的功能、组件等相关内容。
export default StandardFormRow;

@ -1,75 +1,81 @@
import T from 'ant-design-vue/es/table/Table'
import get from 'lodash.get'
// 从 'ant-design-vue/es/table/Table' 模块中导入名为 'T' 的对象(推测这里的 'T' 很可能是 Ant Design Vue 框架中 Table 组件相关的定义或者配置对象,后续代码中会基于它进行一些扩展和功能整合)
import T from 'ant-design-vue/es/table/Table';
// 从 'lodash.get' 模块中导入名为 'get' 的函数lodash.get 常用于从嵌套的对象结构中安全地获取指定路径的值,在代码中可能用于获取表格行数据中特定字段的值等操作)
import get from 'lodash.get';
export default {
data () {
data() {
return {
// 定义一个数组 needTotalList用于存储需要进行总计统计的列相关信息可能后续会根据表格数据来计算每列的合计值等具体要看相关方法的使用
needTotalList: [],
// 定义一个数组 selectedRows用于存储当前选中的表格行数据当表格支持行选择功能时会用到
selectedRows: [],
// 定义一个数组 selectedRowKeys用于存储当前选中的表格行对应的唯一标识通常是每行数据的一个特定字段作为键值方便进行行的选中状态管理等操作
selectedRowKeys: [],
// 定义一个布尔值 localLoading用于标记表格数据是否正在加载中初始化为 false表示初始状态下没有正在加载数据
localLoading: false,
// 定义一个数组 localDataSource用于存储表格实际展示的数据初始为空数组后续会通过加载数据的方法来填充真实数据
localDataSource: [],
// 定义一个对象 localPagination通过复制 this.pagination可能是从父组件传入的分页相关配置对象来初始化用于管理表格的分页相关信息如当前页码、每页显示数量等在组件内部进行分页相关操作时会基于这个对象进行修改和维护
localPagination: Object.assign({}, this.pagination)
}
},
props: Object.assign({}, T.props, {
// 定义 rowKey 属性,类型可以是字符串或者函数,用于指定表格每行数据的唯一标识字段或生成唯一标识的函数,默认值为 'key',意味着如果外部没有传入该属性指定的标识方式,默认以每行数据中的 'key' 字段作为唯一标识
rowKey: {
type: [String, Function],
default: 'key'
},
// 定义 data 属性类型为函数且是必需属性required: true这个函数用于获取表格的数据外部组件需要传入一个符合要求的函数该函数接收一些参数比如分页、筛选、排序等条件参数并返回相应的数据通常是包含数据数组以及分页相关信息等的对象
data: {
type: Function,
required: true
},
// 定义 pageNum 属性,类型为数字,用于指定表格初始的页码,默认值为 1表示默认显示第一页数据
pageNum: {
type: Number,
default: 1
},
// 定义 pageSize 属性,类型为数字,用于指定每页显示的数据条数,默认值为 10即每页默认展示 10 条数据
pageSize: {
type: Number,
default: 10
},
// 定义 showSizeChanger 属性,类型为布尔值,用于控制是否显示每页显示数量的切换组件(例如可以切换每页显示 10 条、20 条等不同数量),默认值为 true表示默认显示该切换组件
showSizeChanger: {
type: Boolean,
default: true
},
// 定义 size 属性,类型为字符串,可能用于控制表格的尺寸大小等样式相关属性,默认值为 'default',具体的尺寸样式对应关系要看 Ant Design Vue 框架中 Table 组件的实现
size: {
type: String,
default: 'default'
},
/**
* alert: {
* show: true,
* clear: Function
* }
* 定义 alert 属性类型可以是对象或者布尔值默认值为 null当为对象时可能包含一些与表格数据提示相关的配置比如是否显示提示信息清除提示相关的回调函数等从注释中的示例结构来看当为布尔值时可能用于简单地控制提示功能的开启或关闭
*/
alert: {
type: [Object, Boolean],
default: null
},
// 定义 rowSelection 属性,类型为对象,默认值为 null用于配置表格行的选择功能相关设置比如可以设置是否支持多选、单选等选择模式以及相关的回调函数等具体取决于 Ant Design Vue 框架的 Table 组件支持的配置项)
rowSelection: {
type: Object,
default: null
},
/** @Deprecated */
/** @Deprecated 表示该属性已被弃用,建议后续不再使用此属性,这里的 showAlertInfo 用于控制是否显示某种提示信息(具体要看之前的业务逻辑,但既然被标记弃用,应该有替代的实现方式或者不再需要此功能了) */
showAlertInfo: {
type: Boolean,
default: false
},
// 定义 showPagination 属性,类型可以是字符串或者布尔值,用于控制是否显示分页组件以及分页相关的显示策略,默认值为 'auto',可能意味着会根据数据情况自动判断是否显示分页(比如数据条数是否超过每页显示数量等情况来决定)
showPagination: {
type: String | Boolean,
default: 'auto'
},
/**
* enable page URI mode
*
* e.g:
* /users/1
* /users/2
* /users/3?queryParam=test
* ...
* 定义 pageURI 属性类型为布尔值用于控制是否启用基于页面 URI统一资源标识符比如 URL 中的路径部分的分页模式默认值为 false开启后可能会根据页面 URL 中的参数来确定当前页码等分页信息例如不同的页码对应不同的 URL 路径 /users/1/users/2 等这种形式来表示不同页码的数据访问
*/
pageURI: {
type: Boolean,
@ -77,7 +83,8 @@ export default {
}
}),
watch: {
'localPagination.current' (val) {
// 监听 localPagination 对象的 current 属性变化(通常 current 表示当前页码),当页码发生变化时,如果 pageURI 为 true即启用了基于页面 URI 的分页模式),则通过 $router.push 方法来更新页面路由,将当前页码信息添加到路由参数中,实现页面 URL 与分页页码的同步更新
'localPagination.current'(val) {
this.pageURI && this.$router.push({
...this.$route,
name: this.$route.name,
@ -86,166 +93,216 @@ export default {
})
})
},
pageNum (val) {
// 监听 pageNum 属性变化,当 pageNum 改变时,将 localPagination 对象中的 current 属性更新为新的 pageNum 值,保持组件内部的分页相关数据的一致性
pageNum(val) {
Object.assign(this.localPagination, {
current: val
})
},
pageSize (val) {
// 监听 pageSize 属性变化,当 pageSize 改变时,相应地更新 localPagination 对象中的 pageSize 属性值,以适配每页显示数量的改变
pageSize(val) {
Object.assign(this.localPagination, {
pageSize: val
})
},
showSizeChanger (val) {
// 监听 showSizeChanger 属性变化,根据其值来更新 localPagination 对象中的 showSizeChanger 属性,用于控制每页显示数量切换组件的显示隐藏状态
showSizeChanger(val) {
Object.assign(this.localPagination, {
showSizeChanger: val
})
}
},
created () {
const { pageNo } = this.$route.params
const localPageNum = this.pageURI && (pageNo && parseInt(pageNo)) || this.pageNum
created() {
// 从当前页面路由的参数中获取 pageNo 参数值(前提是基于页面 URI 的分页模式启用且路由参数中有该值),并将其转换为整数(如果存在的话),否则使用传入的 pageNum 属性值作为初始页码
const {pageNo} = this.$route.params;
const localPageNum = this.pageURI && (pageNo && parseInt(pageNo)) || this.pageNum;
// 根据 showPagination 属性的值判断是否需要初始化 localPagination 对象,如果 showPagination 的值在 ['auto', true] 数组中(表示需要显示分页或者自动根据情况判断显示分页),则将 localPagination 对象更新为包含当前页码、每页显示数量以及每页显示数量切换组件显示状态等相关信息的对象
this.localPagination = ['auto', true].includes(this.showPagination) && Object.assign({}, this.localPagination, {
current: localPageNum,
pageSize: this.pageSize,
showSizeChanger: this.showSizeChanger
})
this.needTotalList = this.initTotalList(this.columns)
this.loadData()
});
// 调用 initTotalList 方法传入表格的列信息this.columns可能从父组件传入或者在组件内部其他地方定义初始化需要进行总计统计的列信息列表并将结果赋值给 needTotalList 属性
this.needTotalList = this.initTotalList(this.columns);
// 调用 loadData 方法开始加载表格数据,触发数据获取的流程
this.loadData();
},
methods: {
/**
* 表格重新加载方法
* 如果参数为 true, 则强制刷新到第一页
* @param Boolean bool
* 如果参数为 true则强制刷新到第一页重新获取数据进行展示
* @param Boolean bool 一个布尔类型的参数默认值为 false用于控制是否强制刷新到第一页
*/
refresh (bool = false) {
refresh(bool = false) {
// 如果传入的 bool 参数为 true就重置 localPagination 对象中的当前页码current为 1同时保持每页显示数量pageSize为组件当前设定的值this.pageSize
// 这里使用 Object.assign 进行对象属性的合并赋值,确保只更新需要改变的属性,而不影响 localPagination 中的其他属性。
bool && (this.localPagination = Object.assign({}, {
current: 1, pageSize: this.pageSize
}))
this.loadData()
current: 1,
pageSize: this.pageSize
}));
// 调用 loadData 方法,触发数据加载流程,根据更新后的分页设置(如果有强制刷新到第一页的操作)获取最新的数据并更新表格展示内容。
this.loadData();
},
/**
* 加载数据方法
* @param {Object} pagination 分页选项器
* @param {Object} filters 过滤条件
* @param {Object} sorter 排序条件
* 此方法用于根据传入的分页过滤排序等条件参数向数据源请求数据并处理返回的数据更新组件内部相关的数据状态如分页信息表格数据源等
* @param {Object} pagination 分页选项器包含分页相关的参数比如当前页码每页显示数量等信息由表格组件的分页交互操作产生
* @param {Object} filters 过滤条件包含用户设置的筛选条件信息用于从数据源筛选出符合条件的数据
* @param {Object} sorter 排序条件包含用户对表格列进行排序操作产生的相关条件比如排序字段排序顺序等信息
*/
loadData (pagination, filters, sorter) {
this.localLoading = true
loadData(pagination, filters, sorter) {
// 将 localLoading 属性设置为 true表示数据开始加载通常可以在页面上通过显示加载动画等方式提示用户正在获取数据。
this.localLoading = true;
// 构建一个 parameter 对象用于整合传递给获取数据的函数this.data的参数。
// 从 pagination 参数(如果存在)、组件内部的 localPagination 对象或者默认的 pageNum 和 pageSize 属性中获取当前页码和每页显示数量相关信息。
// 同时,根据 sorter 参数(如果存在排序相关信息)获取排序字段和排序顺序信息,以及合并 filters 参数中的过滤条件信息。
const parameter = Object.assign({
pageNo: (pagination && pagination.current) ||
pageNo: (pagination && pagination.current) ||
this.localPagination.current || this.pageNum,
pageSize: (pagination && pagination.pageSize) ||
pageSize: (pagination && pagination.pageSize) ||
this.localPagination.pageSize || this.pageSize
},
(sorter && sorter.field && {
sortField: sorter.field
}) || {},
(sorter && sorter.order && {
sortOrder: sorter.order
}) || {}, {
...filters
}
)
const result = this.data(parameter)
// 对接自己的通用数据接口需要修改下方代码中的 r.pageNo, r.totalCount, r.data
// eslint-disable-next-line
},
(sorter && sorter.field && {
sortField: sorter.field
}) || {},
(sorter && sorter.order && {
sortOrder: sorter.order
}) || {}, {
...filters
}
);
// 调用 this.data 函数(外部传入的用于获取表格数据的函数),传入构建好的 parameter 参数,获取数据结果。返回的结果通常是一个包含数据数组以及分页相关信息(如总记录数、当前页码等)的对象(具体结构取决于数据接口的返回约定)。
const result = this.data(parameter);
// 以下代码用于处理异步数据获取的情况(当返回结果是一个 Promise 对象时,说明是异步获取数据,比如通过网络请求获取后端接口返回的数据),需要对返回的 Promise 进行 then 操作来处理成功获取数据后的逻辑。
// eslint-disable-next-line这行注释是用于告诉 ESLint 代码检查工具忽略下一行代码可能存在的相关规则检查(可能是因为下一行代码的写法不符合某些 ESLint 默认规则,但此处是有意为之)。
if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
result.then(r => {
// 根据从接口返回的数据r 对象)更新 localPagination 对象中的相关分页信息,包括当前页码、总记录数、每页显示数量切换组件的显示状态以及每页显示数量等,保持组件内部的分页相关数据与接口返回的数据一致。
this.localPagination = Object.assign({}, this.localPagination, {
current: r.pageNo, // 返回结果中的当前分页数
total: r.totalCount, // 返回结果中的总记录数
showSizeChanger: this.showSizeChanger,
pageSize: (pagination && pagination.pageSize) ||
this.localPagination.pageSize
})
// 为防止删除数据后导致页面当前页面数据长度为 0 ,自动翻页到上一页
});
// 如果返回的数据数组长度为 0表示没有数据了可能是因为删除了所有数据等情况且当前页码大于 1则自动将当前页码减 1并再次调用 loadData 方法重新加载数据,避免出现空页面的情况,确保页面始终有数据展示(如果有上一页数据的话)。
if (r.data.length === 0 && this.localPagination.current > 1) {
this.localPagination.current--
this.loadData()
return
this.localPagination.current--;
this.loadData();
return;
}
// 这里用于判断接口是否有返回 r.totalCount 或 this.showPagination = false
// 当情况满足时,表示数据不满足分页大小,关闭 table 分页功能
(!this.showPagination || !r.totalCount && this.showPagination === 'auto') && (this.localPagination.hideOnSinglePage = true)
this.localDataSource = r.data // 返回结果中的数组数据
this.localLoading = false
// 这里用于判断接口是否有返回 r.totalCount总记录数或 this.showPagination = false 的情况。
// 当满足这些情况时,表示数据不满足分页大小(比如总记录数小于每页显示数量或者明确不需要分页显示),则将 localPagination 对象的 hideOnSinglePage 属性设置为 true关闭 table 分页功能(可能是隐藏分页组件等相关操作,具体要看组件中对该属性的使用方式)。
(!this.showPagination || !r.totalCount && this.showPagination === 'auto') && (this.localPagination.hideOnSinglePage = true);
// 将返回结果中的数据数组赋值给 localDataSource 属性,用于更新表格实际展示的数据,使得表格展示最新获取到的数据内容。
this.localDataSource = r.data; // 返回结果中的数组数据
// 将 localLoading 属性设置为 false表示数据加载完成通常可以相应地隐藏加载提示比如停止显示加载动画告知用户数据已加载完毕。
this.localLoading = false;
})
}
},
initTotalList (columns) {
const totalList = []
/**
* 初始化用于总计统计的列信息列表方法
* 遍历传入的表格列信息columns找出需要进行总计统计通过列对象中的 needTotal 属性判断的列为这些列创建新的对象添加一个初始值为 0 total 属性用于后续统计每列数据的总和最后返回包含这些需要统计列信息的数组
* @param columns 表格的列信息数组每个元素是一个列对象包含列的相关配置和属性
* @returns {Array} 返回一个数组数组元素为需要进行总计统计的列对象每个对象包含原列的信息以及新增的 total 属性初始值为 0
*/
initTotalList(columns) {
const totalList = [];
// 先判断 columns 是否存在且是一个数组类型,然后遍历 columns 数组中的每个列对象。
columns && columns instanceof Array && columns.forEach(column => {
// 如果列对象中有 needTotal 属性为 true表示该列需要进行总计统计就将该列信息复制一份并添加一个 total 属性初始化为 0放入 totalList 数组中。
if (column.needTotal) {
totalList.push({
...column,
total: 0
})
});
}
})
return totalList
});
return totalList;
},
/**
* 用于更新已选中的列表数据 total 统计
* @param selectedRowKeys
* @param selectedRows
* 用于更新已选中的列表数据 total 统计方法
* 根据传入的已选中的行键值数组selectedRowKeys和选中的行数据数组selectedRows更新组件内部记录选中信息的属性selectedRows selectedRowKeys然后遍历需要总计统计的列信息列表needTotalList计算每列在选中行中的数据总和更新每列对应的 total 属性值
* @param selectedRowKeys 已选中的表格行对应的唯一标识键值数组通常是每行数据的一个特定字段作为键值方便进行行的选中状态管理等操作
* @param selectedRows 已选中的表格行数据数组包含每行的详细数据内容
*/
updateSelect (selectedRowKeys, selectedRows) {
this.selectedRows = selectedRows
this.selectedRowKeys = selectedRowKeys
const list = this.needTotalList
updateSelect(selectedRowKeys, selectedRows) {
// 更新组件内部记录选中行数据的属性 selectedRows将传入的 selectedRows 参数赋值给它,保持数据的一致性。
this.selectedRows = selectedRows;
// 同样,更新组件内部记录选中行键值的属性 selectedRowKeys将传入的 selectedRowKeys 参数赋值给它,用于后续操作能准确获取选中行的相关信息。
this.selectedRowKeys = selectedRowKeys;
const list = this.needTotalList;
// 遍历 needTotalList 数组(之前初始化的需要总计统计的列信息列表),对于每个需要总计统计的列,执行以下操作来更新 total 属性值。
this.needTotalList = list.map(item => {
return {
...item,
// 使用 reduce 方法遍历选中的行数据数组selectedRows从每行数据中获取对应列的数据通过 lodash.get 函数安全获取,防止数据结构异常等情况)并累加起来,计算每列在选中行中的数据总和,作为该列的 total 值。
// 如果获取到的值无法转换为数字(通过 parseInt 转换失败,返回 NaN则将 total 值设置为 0确保数据的合理性。
total: selectedRows.reduce((sum, val) => {
const total = sum + parseInt(get(val, item.dataIndex))
return isNaN(total) ? 0 : total
const total = sum + parseInt(get(val, item.dataIndex));
return isNaN(total) ? 0 : total;
}, 0)
}
})
};
});
},
/**
* 清空 table 已选中项
* 清空 table 已选中项方法
* 如果组件启用了行选择功能通过判断 rowSelection 属性是否存在则调用 rowSelection onChange 方法传入空数组同时调用 updateSelect 方法传入空数组来清空选中的行相关数据以及总计统计信息实现清空选中项的功能
*/
clearSelected () {
clearSelected() {
if (this.rowSelection) {
this.rowSelection.onChange([], [])
this.updateSelect([], [])
// 调用 rowSelection 的 onChange 方法,传入空的选中行键值数组和空的选中行数据数组,通知相关的逻辑(可能是外部监听行选择变化的代码)选中项已被清空,触发相应的处理逻辑(比如更新界面上显示的选中数量等)。
this.rowSelection.onChange([], []);
// 调用 updateSelect 方法,传入空数组,清空组件内部记录的选中行相关数据以及总计统计信息,使组件状态回到未选中任何行的初始状态。
this.updateSelect([], []);
}
},
/**
* 处理交给 table 使用者去处理 clear 事件时内部选中统计同时调用
* @param callback
* @returns {*}
* 处理交给 table 使用者去处理 clear 事件时内部选中统计同时调用的方法
* 根据是否存在选中项以及传入的回调函数callback决定是否返回一个带有清空按钮的虚拟 DOM 元素 Vue 中使用类似 JSX 的语法点击该按钮会先执行传入的 callback 函数然后调用 clearSelected 方法清空选中项相关数据
* @param callback 一个回调函数通常由 table 使用者传入用于在点击清空按钮时执行额外的自定义逻辑比如可能涉及到外部状态的更新数据的保存等操作
* @returns {*} 如果没有选中项selectedRowKeys 长度小于等于 0则返回 null表示不渲染清空按钮否则返回包含清空按钮及相应点击事件处理逻辑的虚拟 DOM 元素
*/
renderClear (callback) {
if (this.selectedRowKeys.length <= 0) return null
renderClear(callback) {
if (this.selectedRowKeys.length <= 0) return null;
return (
<a style="margin-left: 24px" onClick={() => {
callback()
this.clearSelected()
// 点击“清空”按钮时,先执行传入的 callback 函数,让 table 使用者有机会处理自己的业务逻辑(比如保存当前选中状态等操作)。
callback();
// 然后调用 clearSelected 方法,清空组件内部选中项相关的数据以及统计信息,更新组件状态。
this.clearSelected();
}}>清空</a>
)
);
},
renderAlert () {
/**
* 绘制包含表格相关统计信息清空按钮等内容的 alert 组件方法
* 此方法用于构建一个带有统计信息和操作按钮的提示组件a-alert展示已选择的行数各统计列的总计信息以及清空按钮根据相关条件判断是否显示方便用户直观了解表格的选中状态和相关统计数据并提供操作入口来清空选中项
*/
renderAlert() {
// 绘制统计列数据
// 遍历 needTotalList 数组(包含需要进行总计统计的列信息),为每个列创建一个包含列标题和对应总计数据的 <span> 元素,用于展示每列的总计信息,最后返回一个包含这些 <span> 元素的数组(在 Vue 中使用类似 JSX 的语法构建虚拟 DOM 元素)。
const needTotalItems = this.needTotalList.map((item) => {
return (<span style="margin-right: 12px">
{item.title}总计 <a style="font-weight: 600">{!item.customRender ? item.total : item.customRender(item.total)}</a>
</span>)
})
{item.title}总计 <a
style="font-weight: 600">{!item.customRender ? item.total : item.customRender(item.total)}</a>
</span>)
});
// 绘制 清空 按钮
// 根据 alert 属性的不同情况来决定是否显示以及如何显示“清空”按钮。
// 如果 alert.clear 是一个布尔值且为 true表示需要显示“清空”按钮调用 renderClear 方法传入 clearSelected 函数(用于清空选中项)来获取对应的虚拟 DOM 元素(即带有点击事件处理逻辑的“清空”按钮)。
// 如果 alert 属性不为 null 且 alert.clear 是一个函数,表示用户自定义了清除逻辑,同样调用 renderClear 方法传入自定义的清除函数来获取“清空”按钮对应的虚拟 DOM 元素。
// 如果不满足以上条件,则将 clearItem 设置为 null表示不显示“清空”按钮。
const clearItem = (typeof this.alert.clear === 'boolean' && this.alert.clear) ? (
this.renderClear(this.clearSelected)
) : (this.alert !== null && typeof this.alert.clear === 'function') ? (
this.renderClear(this.alert.clear)
) : null
) : null;
// 绘制 alert 组件
// 使用 a-alert 组件构建一个提示框设置显示图标showIcon={true}并添加底部外边距style="margin-bottom: 16px"),在提示框的 message 插槽中放入已选择的行数信息、统计列的总计信息以及“清空”按钮(如果存在)的虚拟 DOM 元素,最后返回构建好的 a-alert 组件对应的虚拟 DOM 元素,用于在页面上展示表格相关的统计提示信息和操作按钮。
return (
<a-alert showIcon={true} style="margin-bottom: 16px">
<template slot="message">
@ -254,52 +311,68 @@ export default {
{clearItem}
</template>
</a-alert>
)
);
}
},
render () {
render() {
// 创建一个空对象来存储将要传递给表格的属性
const props = {}
// 获取当前组件data中的所有键名
const localKeys = Object.keys(this.$data)
// 判断是否显示alert条件包括alert对象存在且show属性为真同时rowSelection的selectedRowKeys已定义或者无条件显示alert
const showAlert = (typeof this.alert === 'object' && this.alert !== null && this.alert.show) && typeof this.rowSelection.selectedRowKeys !== 'undefined' || this.alert
// 遍历T.props中的每个键
Object.keys(T.props).forEach(k => {
// 根据T.props中的键名生成对应的本地键名首字母大写
const localKey = `local${k.substring(0, 1).toUpperCase()}${k.substring(1)}`
// 如果本地数据中存在这个键名则将其值赋给props对象中的对应键
if (localKeys.includes(localKey)) {
props[k] = this[localKey]
return props[k]
}
// 如果当前键是'rowSelection'
if (k === 'rowSelection') {
// 如果需要显示alert且rowSelection已定义
if (showAlert && this.rowSelection) {
// 如果需要使用alert重新绑定 rowSelection 事件
// 重新绑定rowSelection事件包括selectedRows, selectedRowKeys和onChange事件处理函数
props[k] = {
selectedRows: this.selectedRows,
selectedRowKeys: this.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
// 更新组件内部状态
this.updateSelect(selectedRowKeys, selectedRows)
// 如果原rowSelection中有onChange方法则调用它
typeof this[k].onChange !== 'undefined' && this[k].onChange(selectedRowKeys, selectedRows)
}
}
return props[k]
} else if (!this.rowSelection) {
// 如果没打算开启 rowSelection 则清空默认的选择项
// 如果没有定义rowSelection则将其置为null
props[k] = null
return props[k]
}
}
// 如果当前组件中有这个属性则将其添加到props对象中
this[k] && (props[k] = this[k])
return props[k]
})
// 使用JSX语法渲染<a-table>组件并传入props和scopedSlots
const table = (
<a-table {...{ props, scopedSlots: { ...this.$scopedSlots } }} onChange={this.loadData}>
{ Object.keys(this.$slots).map(name => (<template slot={name}>{this.$slots[name]}</template>)) }
<a-table {...{props, scopedSlots: {...this.$scopedSlots}}} onChange={this.loadData}>
{ // 遍历组件的插槽,并渲染它们
Object.keys(this.$slots).map(name => (<template slot={name}>{this.$slots[name]}</template>))
}
</a-table>
)
// 返回最终的渲染结果包括可能显示的alert和表格
return (
<div class="table-wrapper">
{ showAlert ? this.renderAlert() : null }
{ table }
{showAlert ? this.renderAlert() : null} // 如果需要显示alert则调用renderAlert方法渲染alert
{table} // 渲染表格
</div>
)
}

@ -1,17 +1,23 @@
import { Tag } from 'ant-design-vue'
const { CheckableTag } = Tag
// 'ant-design-vue' Tag Ant Design Vue Vue UI Tag
import { Tag } from 'ant-design-vue';
// Tag CheckableTag
const { CheckableTag } = Tag;
export default {
// 'TagSelectOption' Vue 使
name: 'TagSelectOption',
props: {
// prefixCls 'ant-pro-tag-select-option'便便使
prefixCls: {
type: String,
default: 'ant-pro-tag-select-option'
},
// value 便使
value: {
type: [String, Number, Object],
default: ''
},
// checked false true 便使
checked: {
type: Boolean,
default: false
@ -19,27 +25,40 @@ export default {
},
data () {
return {
// localChecked checked checked true false使 false便
localChecked: this.checked || false
}
},
watch: {
// checked checked localChecked checked 便
'checked' (val) {
this.localChecked = val
this.localChecked = val;
},
// '$parent.items' '$parent' 'items' 使 deep: true '$parent.items' handler
'$parent.items': {
handler: function (val) {
this.value && val.hasOwnProperty(this.value) && (this.localChecked = val[this.value])
// '$parent.items' value '$parent.items' value localChecked '$parent.items'
this.value && val.hasOwnProperty(this.value) && (this.localChecked = val[this.value]);
},
deep: true
}
},
render () {
const { $slots, value } = this
// $slots value $slots 使value 使
const { $slots, value } = this;
const onChange = (checked) => {
this.$emit('change', { value, checked })
}
return (<CheckableTag key={value} vModel={this.localChecked} onChange={onChange}>
{$slots.default}
</CheckableTag>)
// onChange $emit 'change' value checked 'change'
this.$emit('change', { value, checked });
};
return (
// 使 CheckableTag Ant Design Vue Tag
// key value Vue DOM
// vModel localChecked使
// onChange onChange
// CheckableTag $slots.default使
<CheckableTag key={value} vModel={this.localChecked} onChange={onChange}>
{$slots.default}
</CheckableTag>
)
}
}

@ -1,31 +1,41 @@
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import Option from './TagSelectOption.jsx'
import { filterEmpty } from '../../components/_util/util'
// 'ant-design-vue/es/_util/vue-types' PropTypes Vue props React PropTypes
import PropTypes from 'ant-design-vue/es/_util/vue-types';
// 'TagSelectOption.jsx' Option TagSelect
import Option from './TagSelectOption.jsx';
// '../../components/_util/util' filterEmpty
import { filterEmpty } from '../../components/_util/util';
export default {
// Option 使使 <Option> Option
Option,
name: 'TagSelect',
// v-model 'checked' 'change'使 Vue v-model 'change' 'checked' 'checked' 'change'
model: {
prop: 'checked',
event: 'change'
},
props: {
// prefixCls 'ant-pro-tag-select'便便
prefixCls: {
type: String,
default: 'ant-pro-tag-select'
},
// defaultValue PropTypes.array PropTypes null null使
defaultValue: {
type: PropTypes.array,
default: null
},
// value PropTypes.array null defaultValue
value: {
type: PropTypes.array,
default: null
},
// expandable false
expandable: {
type: Boolean,
default: false
},
// hideCheckAll false
hideCheckAll: {
type: Boolean,
default: false
@ -33,71 +43,108 @@ export default {
},
data () {
return {
// expand false
expand: false,
// localCheckAll false
localCheckAll: false,
// getItemsKey this.$slots.default filterEmpty getItemsKey
items: this.getItemsKey(filterEmpty(this.$slots.default)),
// value defaultValue value 使 defaultValue val null val
val: this.value || this.defaultValue || []
}
},
methods: {
/**
* 当选项的选中状态发生改变时调用的方法接收一个包含选中信息的对象checked参数用于更新组件内部的选中状态相关数据以及全选状态的判断
* @param checked 包含选中信息的对象可能包含类似 value选项的值checked是否选中的布尔值等属性具体结构取决于调用时传入的参数情况以及相关组件的约定
*/
onChange (checked) {
const key = Object.keys(this.items).filter(key => key === checked.value)
this.items[key] = checked.checked
const bool = Object.values(this.items).lastIndexOf(false)
// Object.keys items checked.value
const key = Object.keys(this.items).filter(key => key === checked.value);
// items checked.checked
this.items[key] = checked.checked;
// Object.values items false -1 false
const bool = Object.values(this.items).lastIndexOf(false);
if (bool === -1) {
this.localCheckAll = true
// localCheckAll true
this.localCheckAll = true;
} else {
this.localCheckAll = false
// localCheckAll false
this.localCheckAll = false;
}
},
/**
* 当点击全选按钮时调用的方法接收一个包含全选按钮选中状态信息的对象checked参数用于更新所有选项的选中状态以及组件内部的全选状态记录
* @param checked 包含全选按钮选中状态的对象通常包含 checked 属性布尔值表示全选按钮是否被选中用于统一设置所有选项的选中状态
*/
onCheckAll (checked) {
// items checked.checked
Object.keys(this.items).forEach(v => {
this.items[v] = checked.checked
})
this.localCheckAll = checked.checked
this.items[v] = checked.checked;
});
// localCheckAll checked.checked
this.localCheckAll = checked.checked;
},
/**
* 用于获取每个选项对应的键值对象根据传入的选项数组items创建一个新的对象对象的键为每个选项的特定值通常是用于唯一标识选项的值比如选项的 ID 值初始化为 false表示默认未选中状态方便后续根据选项的键来记录和查询其选中状态等信息
* @param items 选项数组数组元素可能是包含选项相关信息的虚拟 DOM 节点或者对象具体取决于组件插槽传入内容的结构和格式
* @returns {Object} 返回一个对象对象的键为选项的特定值值为布尔类型的选中状态初始化为 false
*/
getItemsKey (items) {
const totalItem = {}
const totalItem = {};
items.forEach(item => {
totalItem[item.componentOptions.propsData && item.componentOptions.propsData.value] = false
})
return totalItem
// item item.componentOptions.propsData.value 'value' totalItem false
totalItem[item.componentOptions.propsData && item.componentOptions.propsData.value] = false;
});
return totalItem;
},
// CheckAll Button
/**
* 用于渲染全选按钮的方法根据 hideCheckAll 属性判断是否需要隐藏全选按钮如果不隐藏hideCheckAll false则返回一个 <Option> 组件之前导入的组件作为全选按钮设置其 key 属性为 'total'选中状态为 localCheckAll 属性的值即当前组件内部记录的全选状态并绑定 onChange 事件为 onCheckAll 方法按钮显示文本为 'All'如果需要隐藏hideCheckAll true则返回 null表示不渲染全选按钮
*/
renderCheckAll () {
return !this.hideCheckAll && (<Option key={'total'} checked={this.localCheckAll} onChange={this.onCheckAll}>All</Option>) || null
return!this.hideCheckAll && (<Option key={'total'} checked={this.localCheckAll} onChange={this.onCheckAll}>All</Option>) || null;
},
// expandable
/**
* 用于处理组件可展开相关逻辑的方法目前函数体为空可能后续需要在这里添加代码来实现组件展开时的具体行为比如展开显示更多选项展开后加载更多数据等相关操作具体功能取决于组件的设计需求
*/
renderExpandable () {
},
// render option
/**
* 用于渲染选项的方法接收选项数组items参数主要功能是为每个选项添加 change 事件监听器当选项的选中状态改变时先触发组件内部的 onChange 方法更新内部状态再通过 $emit 触发 'change' 事件将选中状态变化通知给外部组件实现双向数据绑定或者让外部组件能响应选项变化等功能最后返回处理后的选项数组包含添加了事件监听器的虚拟 DOM 节点等用于在组件模板中进行渲染展示
* @param items 选项数组同前面提到的选项相关数组包含要渲染的各个选项的信息
*/
renderTags (items) {
const listeners = {
change: (checked) => {
this.onChange(checked)
this.$emit('change', checked)
this.onChange(checked);
this.$emit('change', checked);
}
}
};
return items.map(vnode => {
const options = vnode.componentOptions
options.listeners = listeners
return vnode
})
const options = vnode.componentOptions;
options.listeners = listeners;
return vnode;
});
}
},
render () {
const { $props: { prefixCls } } = this
// $props prefixCls
const { $props: { prefixCls } } = this;
const classString = {
// classString `${prefixCls}` true使 :class 便
[`${prefixCls}`]: true
}
const tagItems = filterEmpty(this.$slots.default)
};
const tagItems = filterEmpty(this.$slots.default);
return (
<div class={classString}>
{this.renderCheckAll()}
{this.renderTags(tagItems)}
</div>
)
);
}
}

@ -1,46 +1,59 @@
/**
* components util
* 工具组件集合
*/
/**
* 清理空值对象
* @param children
* @returns {*[]}
* 清理数组中的空值或空对象
* @param {Array} children - 要处理的数组默认为空数组
* @returns {Array} - 过滤后的数组
*/
export function filterEmpty (children = []) {
export function filterEmpty(children = []) {
// 使用Array.prototype.filter方法移除数组中的空值或空对象
return children.filter(c => c.tag || (c.text && c.text.trim() !== ''))
}
/**
* 获取字符串长度英文字符 长度1中文字符长度2
* @param {*} str
* 获取字符串的完整长度英文字符长度为1中文字符长度为2
* @param {string} str - 要处理的字符串默认为空字符串
* @returns {number} - 字符串的完整长度
*/
export const getStrFullLength = (str = '') =>
// 使用Array.prototype.split方法将字符串转换为字符数组
str.split('').reduce((pre, cur) => {
// 获取字符的Unicode编码
const charCode = cur.charCodeAt(0)
// 如果字符是ASCII字符英文字符则长度加1
if (charCode >= 0 && charCode <= 128) {
return pre + 1
}
// 如果字符是双字节字符中文字符则长度加2
return pre + 2
}, 0)
/**
* 截取字符串根据 maxLength 截取后返回
* @param {*} str
* @param {*} maxLength
* 根据最大长度截取字符串保留完整字符
* @param {string} str - 要截取的字符串默认为空字符串
* @param {number} maxLength - 最大长度默认无限制
* @returns {string} - 截取后的字符串
*/
export const cutStrByFullLength = (str = '', maxLength) => {
let showLength = 0
// 使用Array.prototype.split方法将字符串转换为字符数组
return str.split('').reduce((pre, cur) => {
// 获取字符的Unicode编码
const charCode = cur.charCodeAt(0)
// 如果字符是ASCII字符英文字符则长度加1
if (charCode >= 0 && charCode <= 128) {
showLength += 1
} else {
// 如果字符是双字节字符中文字符则长度加2
showLength += 2
}
// 如果当前长度小于等于最大长度,则继续累加字符
if (showLength <= maxLength) {
return pre + cur
}
// 如果超过最大长度,则返回当前累加的字符串
return pre
}, '')
}
}

@ -1,16 +1,19 @@
@import './index.less';
html {
// 设置 html 元素的溢出行为为自动,即当内容超出可视区域时会自动出现滚动条,方便用户查看全部内容。
overflow: auto;
}
body {
// 打开滚动条固定显示
// 强制显示垂直方向的滚动条,确保页面在垂直方向内容较多时可以滚动查看,避免内容被隐藏而无法访问。
overflow-y: scroll;
// 当 body 元素具有 "colorWeak" 类名时应用以下样式规则通过滤镜filter属性将页面颜色反相 80%,可能用于模拟色弱等视觉辅助功能下的页面显示效果。
&.colorWeak {
filter: invert(80%);
}
// 当 body 元素具有 "userLayout" 类名时,设置其溢出行为为自动,与 html 元素的 overflow:auto 类似,根据页面具体布局需求对该状态下的整体页面溢出进行控制。
&.userLayout {
overflow: auto;
}

@ -1,19 +1,35 @@
// pro components
import STable from '../components/Table'
import MultiTab from '../components/MultiTab'
import Result from '../components/Result'
import TagSelect from '../components/TagSelect'
import ExceptionPage from '../components/Exception'
import StandardFormRow from '../components/StandardFormRow'
import DescriptionList from '../components/DescriptionList'
// 以下是一系列从相对路径 '../components' 下导入不同组件的语句。
// 导入名为 STable 的组件,从命名推测该组件可能是一个自定义的表格组件,用于在项目中展示表格形式的数据,具体功能和样式取决于其组件内部的实现代码。
import STable from '../components/Table';
// 导入名为 MultiTab 的组件,大概率是实现多标签页切换相关功能的组件,方便在页面中切换不同的内容板块或者视图等,同样其具体行为由组件自身代码定义。
import MultiTab from '../components/MultiTab';
// 导入名为 Result 的组件,可能是用于展示操作结果、查询结果等相关信息的组件,比如成功、失败、提示等各类结果反馈展示场景会用到它。
import Result from '../components/Result';
// 导入名为 TagSelect 的组件,应该是一个用于选择标签(可能是文本标签、分类标签等)的交互组件,提供给用户进行相应的选择操作,具体的标签数据来源和选择交互逻辑在组件内部实现。
import TagSelect from '../components/TagSelect';
// 导入名为 ExceptionPage 的组件,从名字可推测是用于展示异常页面的组件,当系统出现错误、异常情况或者某些特定的不符合预期的状况时,会展示该组件对应的页面内容,告知用户相关情况。
import ExceptionPage from '../components/Exception';
// 导入名为 StandardFormRow 的组件,可能是用于构建表单中标准行的组件,比如包含输入框、标签等元素组成的一行表单结构,方便统一表单的样式和布局等方面的构建。
import StandardFormRow from '../components/StandardFormRow';
// 导入名为 DescriptionList 的组件,或许是用于展示描述列表信息的组件,像一些配置项及其对应说明、功能列表等以列表形式展示的内容可以使用它来呈现。
import DescriptionList from '../components/DescriptionList';
export {
// 将导入的 ExceptionPage 组件原样导出,使得在其他模块中可以通过导入该模块来使用 ExceptionPage 组件,用于处理异常页面展示相关的功能需求。
ExceptionPage,
// 导出 Result 组件,方便其他模块引入该组件来展示各种结果信息,实现结果反馈相关的功能逻辑。
Result,
// 导出 STable 组件,其他地方可以使用它来构建和展示表格数据,满足表格展示相关的业务场景需求。
STable,
// 导出 MultiTab 组件,以实现在不同模块中创建多标签页切换的页面功能,让用户能够方便地切换不同的内容视图等。
MultiTab,
// 导出 TagSelect 组件,供其他模块使用来提供标签选择的交互功能,满足根据标签筛选、分类等业务操作需求。
TagSelect,
// 导出 StandardFormRow 组件,有助于在其他模块构建表单时统一标准行的样式和结构,提高表单构建的效率和规范性。
StandardFormRow,
// 兼容写法,请勿继续使用
// 这里将导入的 DescriptionList 组件以一个别名 DetailList 进行导出,这可能是为了兼容旧的代码或者模块调用方式,
// 不过注释提醒了后续不要再继续使用这种写法,意味着未来可能会有更合适的方式来处理该组件的导出和使用,避免潜在的问题或不符合新的代码规范要求。
DescriptionList as DetailList
}
}

@ -1,4 +1,12 @@
@import "~ant-design-vue/lib/style/index";
/*
这是一条 CSS 的 `@import` 规则,用于引入外部的 CSS 样式文件。在这里,它引入了 `ant-design-vue` 库中 `lib/style/index` 路径下的样式文件。
`~` 符号通常在一些构建工具(比如 Webpack 等)中有特殊含义,它表示模块的相对路径解析方式,会根据模块的配置去准确找到对应的文件资源,通过导入这个文件,项目可以复用 `ant-design-vue` 库所提供的基础样式,如组件的默认样式、布局样式等,使得项目中的组件能够按照 `ant-design-vue` 的设计风格来呈现外观。
*/
// The prefix to use on all css classes from ant-pro.
@ant-pro-prefix : ant-pro;
@ant-pro-prefix : ant-pro;
/*
这是一条 CSS 变量(也叫自定义属性)的声明语句,定义了一个名为 `@ant-pro-prefix` 的变量,并将其值设置为 `ant-pro`。
在后续的 CSS 代码中(如果遵循相应的规范使用),这个变量可以作为类名等的前缀使用,目的通常是为了给项目中属于 `ant-pro` 相关的 CSS 类名添加一个统一的标识前缀,方便进行样式的管理、区分以及避免类名冲突等情况。例如,后续可能会基于这个前缀去构建一系列带有该前缀的自定义类名,用于特定的样式覆盖、扩展或者组件样式的个性化定制等操作,同时也使得整体样式结构更加清晰、有条理,一眼就能看出哪些样式是和 `ant-pro` 相关的。
*/

@ -1,10 +1,14 @@
<template>
<!-- 使用 Ant Design Vue 框架中的 a-breadcrumb 组件创建一个面包屑导航栏为其添加 "breadcrumb" 类名方便后续通过 CSS 对其样式进行定制化设置 -->
<a-breadcrumb class="breadcrumb">
<!-- 使用 v-for 指令循环遍历 breadList 数组中的每个元素item以及对应的索引index为每个元素创建一个 a-breadcrumb-item 组件并且通过 :key 绑定每个元素的唯一标识这里使用 item.name 作为唯一标识用于 Vue 的虚拟 DOM 渲染优化帮助 Vue 准确识别每个元素的变化情况 -->
<a-breadcrumb-item v-for="(item, index) in breadList" :key="item.name">
<!-- 使用 router-link 组件创建一个路由链接只有当 item.name 不等于当前组件的 name 属性值并且索引 index 不等于 1 时才显示为路由链接可能是为了排除特定的某个或某些面包屑项不做成可点击的链接形式通过 :to 属性绑定一个对象根据 item.path 的值来设置链接的目标路径如果 item.path 为空字符串则设置目标路径为根路径 '/'否则使用 item.path 本身的值作为目标路径 -->
<router-link
v-if="item.name != name && index != 1"
:to="{ path: item.path === '' ? '/' : item.path }"
v-if="item.name!= name && index!= 1"
:to="{ path: item.path === ''? '/' : item.path }"
>{{ item.meta.title }}</router-link>
<!-- 当不满足上面 router-link 的显示条件时即要么 item.name 等于当前组件的 name 属性值要么索引 index 等于 1则以普通的 <span> 元素形式显示该项面包屑的标题item.meta.title不具备路由跳转功能 -->
<span v-else>{{ item.meta.title }}</span>
</a-breadcrumb-item>
</a-breadcrumb>
@ -14,28 +18,36 @@
export default {
data () {
return {
// name 使
name: '',
// breadList
breadList: []
}
},
created () {
this.getBreadcrumb()
// getBreadcrumb
this.getBreadcrumb();
},
methods: {
getBreadcrumb () {
this.breadList = []
// breadList
this.breadList = [];
// breadList 'index' '/dashboard/' ''
// this.breadList.push({name: 'index', path: '/dashboard/', meta: {title: ''}})
this.name = this.$route.name
// name
this.name = this.$route.name;
// $route.matched item.name!== 'index' breadList
this.$route.matched.forEach(item => {
// item.name !== 'index' && this.breadList.push(item)
this.breadList.push(item)
})
// item.name!== 'index' && this.breadList.push(item)
this.breadList.push(item);
});
}
},
watch: {
// $route 退 getBreadcrumb
$route () {
this.getBreadcrumb()
this.getBreadcrumb();
}
}
}

@ -1,5 +1,10 @@
<script>
// 使
/* WARNING: 兼容老引入,请勿继续使用 */
import DescriptionList from '../../components/DescriptionList'
export default DescriptionList
</script>
// '../../components/DescriptionList' DescriptionList Vue DescriptionList
import DescriptionList from '../../components/DescriptionList';
// DescriptionList DescriptionList便
export default DescriptionList;
</script>

@ -1,27 +1,34 @@
<template>
<!-- 创建一个带有 "head-info" 类名的 div 容器同时通过动态绑定 class 属性根据组件传入的 center 属性值布尔类型来决定是否添加 "center" 类名以此实现不同的样式布局效果 -->
<div class="head-info" :class="center && 'center'">
<!-- 使用双括号插值语法展示 title 属性的值将其作为一个文本内容放在 span 元素内展示该文本通常用于显示标题相关的信息 -->
<span>{{ title }}</span>
<!-- 同样使用双括号插值语法展示 content 属性的值把它放置在 p 元素中展示这里的内容大概率是主要的正文信息字体等样式上会相对突出一些 -->
<p>{{ content }}</p>
<!-- 通过 v-if 指令根据 bordered 属性的值布尔类型来决定是否渲染 em 元素如果 bordered 属性为 true则渲染该元素可能用于添加一些装饰性的分割线等样式效果具体样式由后续的样式部分定义 -->
<em v-if="bordered"/>
</div>
</template>
<script>
export default {
name: 'HeadInfo',
props: {
// title
title: {
type: String,
default: ''
},
// content
content: {
type: String,
default: ''
},
// bordered em 线 false true
bordered: {
type: Boolean,
default: false
},
// center true false
center: {
type: Boolean,
default: true
@ -31,37 +38,58 @@ export default {
</script>
<style lang="less" scoped>
.head-info {
position: relative;
text-align: left;
padding: 0 32px 0 0;
min-width: 125px;
// "head-info"
.head-info {
// 便 em
position: relative;
// "center" center
text-align: left;
// 32px 0使
padding: 0 32px 0 0;
// 125px
min-width: 125px;
&.center {
text-align: center;
padding: 0 32px;
}
// "center" center true
&.center {
text-align: center;
padding: 0 32px;
}
span {
color: rgba(0, 0, 0, .45);
display: inline-block;
font-size: 14px;
line-height: 22px;
margin-bottom: 4px;
}
p {
color: rgba(0, 0, 0, .85);
font-size: 24px;
line-height: 32px;
margin: 0;
}
em {
background-color: #e8e8e8;
position: absolute;
height: 56px;
width: 1px;
top: 0;
right: 0;
}
span {
// span 使 rgba 0.45 使
color: rgba(0, 0, 0,.45);
// span 使便
display: inline-block;
// span 14px
font-size: 14px;
// span 22px
line-height: 22px;
// span 4px p 使
margin-bottom: 4px;
}
p {
// p 0.85 span
color: rgba(0, 0, 0,.85);
// p 24px使
font-size: 24px;
// p 32px
line-height: 32px;
// p 0使
margin: 0;
}
</style>
em {
// em #e8e8e8线
background-color: #e8e8e8;
// em .head-info
position: absolute;
// em 56px线
height: 56px;
// em 1px使线线
width: 1px;
// em 0使.head-info
top: 0;
// em 0使.head-info线
right: 0;
}
}
</style>

@ -1,26 +1,62 @@
<template>
<!-- 创建一个带有 "logo" 类名的 div 容器用于包裹整个 logo 相关的元素方便后续通过 CSS 对这个区域进行样式设置统一管理 logo 的布局和外观表现 -->
<div class="logo">
<!-- 使用 router-link 组件创建一个路由链接通过 :to 属性绑定一个对象指定链接的目标路由这里目标路由是名为 'dashboard' 的路由具体该路由对应的页面和功能需要看路由配置文件中的定义点击这个链接会跳转到对应的 'dashboard' 页面 -->
<router-link :to="{name:'dashboard'}">
<!-- 引入名为 LogoSvg 的组件用于展示 logo 的图标部分具体该组件如何渲染 SVG 图形以及样式表现要看其内部实现这里它接收一个 alt 属性用于设置图片的替代文本当图片无法正常显示时或者辅助设备读取时会显示该文本内容 -->
<LogoSvg alt="logo" />
<!-- 通过 v-if 指令根据 showTitle 属性布尔类型的值来决定是否渲染 h1 元素如果 showTitle true则渲染 h1 元素并展示 title 属性的值作为标题内容如果 showTitle false则不渲染 h1 元素即不显示标题部分以此实现根据条件控制标题是否显示的功能 -->
<h1 v-if="showTitle">{{ title }}</h1>
</router-link>
</div>
</template>
<script>
import LogoSvg from '../../assets/logo.svg?inline'
// '../../assets/logo.svg?inline' LogoSvg '?inline' SVG Vue 使 SVG 便使
import LogoSvg from '../../assets/logo.svg?inline';
export default {
name: 'Logo',
components: {
// LogoSvg 使使 <LogoSvg> SVG
LogoSvg
},
props: {
// title 'Online Exam' 'Online Exam'required: false
title: {
type: String,
default: 'Online Exam',
required: false
},
// showTitle h1 title true false required: false
showTitle: {
type: Boolean,
default: true,
required: false
}
}
}
</script>
<script>
// '../../assets/logo.svg?inline' LogoSvg Vue SVG '?inline' 使 Vue CLI SVG 使 Vue 便使
import LogoSvg from '../../assets/logo.svg?inline';
export default {
// 'Logo' Vue 便
name: 'Logo',
components: {
// `components` LogoSvg 使template使 `<LogoSvg>` SVG 使
LogoSvg
},
props: {
// `title` `String`使 'Online Exam' `title` 使 'Online Exam' `required: false`
title: {
type: String,
default: 'Online Exam',
required: false
},
// `showTitle` `Boolean` `true` `false` `<h1>` `required: false`使
showTitle: {
type: Boolean,
default: true,

@ -41,6 +41,7 @@
<script>
export default {
props: {
// visible false true v-model a-modal
visible: {
type: Boolean,
default: false
@ -48,42 +49,51 @@ export default {
},
data () {
return {
// stepLoading false true false
stepLoading: false,
// form null a-form :auto-form-create 便
form: null
}
},
methods: {
handleStepOk () {
const vm = this
this.stepLoading = true
const vm = this;
// "" stepLoading true使
this.stepLoading = true;
// this.form validateFields 6 err null values
this.form.validateFields((err, values) => {
if (!err) {
console.log('values', values)
// err null values 便 setTimeout 2 stepLoading false $emit 'success' values便
console.log('values', values);
setTimeout(() => {
vm.stepLoading = false
vm.$emit('success', { values })
}, 2000)
return
vm.stepLoading = false;
vm.$emit('success', { values });
}, 2000);
return;
}
this.stepLoading = false
this.$emit('error', { err })
})
// err null stepLoading false $emit 'error' err便
this.stepLoading = false;
this.$emit('error', { err });
});
},
handleCancel () {
this.visible = false
this.$emit('cancel')
// "" visible false $emit 'cancel'
this.visible = false;
this.$emit('cancel');
},
onForgeStepCode () {
// "?"
}
}
}
</script>
<style lang="less" scoped>
.step-form-wrapper {
margin: 0 auto;
width: 80%;
max-width: 400px;
}
// "step-form-wrapper"
.step-form-wrapper {
// auto使
margin: 0 auto;
// 80% 400px使
width: 80%;
max-width: 400px;
}
</style>

@ -1,20 +1,32 @@
<template>
<!-- 创建一个带有 "user-wrapper" 类名的 div 容器用于包裹整个用户相关菜单的结构方便后续通过 CSS 对这个区域进行样式设置统一管理其布局和外观表现 -->
<div class="user-wrapper">
<!-- "user-wrapper" 内部再创建一个带有 "content-box" 类名的 div 容器进一步细化布局结构后续相关的用户菜单交互组件等都放置在这个容器内 -->
<div class="content-box">
<!-- 使用 Ant Design Vue 库中的 a-dropdown 组件创建一个下拉菜单组件用于展示用户相关的操作选项如账户设置退出登录等点击后会弹出相应的菜单选项供用户选择操作 -->
<a-dropdown>
<!-- 创建一个带有 "action ant-dropdown-link user-dropdown-menu" 类名的 span 元素作为触发下拉菜单显示的操作区域它内部包含用户头像通过 a-avatar 组件展示和用户昵称通过双括号插值语法展示用户点击这个区域时会弹出下拉菜单 -->
<span class="action ant-dropdown-link user-dropdown-menu">
<!-- 使用 Ant Design Vue 库中的 a-avatar 组件展示用户头像设置其 class 属性为 "avatar"size 属性为 "small" 表示以小尺寸展示头像通过 :src 属性绑定一个函数从脚本部分可知是从全局变量获取头像地址的函数来动态获取并显示用户头像 -->
<a-avatar class="avatar" size="small" :src="avatar()"/>
<!-- 使用双括号插值语法展示用户昵称昵称的值同样是通过函数从全局变量获取昵称的函数动态获取得到具体函数的实现和数据来源看脚本部分代码 -->
<span>{{ nickname() }}</span>
</span>
<!-- 使用 slot="overlay" 插槽来定义下拉菜单弹出时显示的具体内容也就是具体的菜单选项列表将其包裹在带有 "user-dropdown-menu-wrapper" 类名的 a-menu 组件中使其呈现为一个菜单的样式结构 -->
<a-menu slot="overlay" class="user-dropdown-menu-wrapper">
<!-- 创建一个 a-menu-item 菜单子项组件设置 key 属性为 "1"用于在菜单中唯一标识这个选项方便 Vue 在进行虚拟 DOM 操作等场景下进行区分和管理 -->
<a-menu-item key="1">
<!-- 使用 router-link 组件创建一个路由链接通过 :to 属性绑定一个对象指定链接的目标路由为名为 'settings' 的路由具体该路由对应的页面和功能需要看路由配置文件中的定义点击这个选项会跳转到账户设置相关的页面在这个菜单子项内部还包含了一个 a-icon 组件用于展示设置图标以及一个 span 元素用于显示 "账户设置" 文本内容 -->
<router-link :to="{ name: 'settings' }">
<a-icon type="setting"/>
<span>账户设置</span>
</router-link>
</a-menu-item>
<!-- 使用 a-menu-divider 组件创建一个菜单分隔线用于在菜单选项之间进行视觉上的分隔使不同功能的选项分组更加清晰这里将账户设置选项和退出登录选项进行了分隔 -->
<a-menu-divider/>
<!-- 再创建一个 a-menu-item 菜单子项组件设置 key 属性为 "3"同样用于唯一标识这个选项用于展示退出登录相关的操作选项 -->
<a-menu-item key="3">
<!-- 使用普通的 <a> 标签创建一个链接这里使用 "javascript:;" 作为 href 属性值阻止默认的页面跳转行为因为实际的退出登录操作是通过绑定的 @click 事件来处理绑定 @click 事件为 handleLogout 方法点击这个选项会调用 handleLogout 方法来执行退出登录相关的逻辑在这个菜单子项内部同样包含了一个 a-icon 组件用于展示退出图标以及一个 span 元素用于显示 "退出登录" 文本内容 -->
<a href="javascript:;" @click="handleLogout">
<a-icon type="logout"/>
<span>退出登录</span>
@ -27,32 +39,37 @@
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
// Vuex mapActions mapGetters mapActions Vuex action 便 action mapGetters Vuex getters 使便 Vuex store
import { mapActions, mapGetters } from 'vuex';
export default {
name: 'UserMenu',
methods: {
...mapActions(['Logout']), // tokenlocalStorage
...mapGetters(['nickname', 'avatar']), //
// 使 Vuex Logout action this.Logout() action token localStorage 退
...mapActions(['Logout']),
// 使 Vuex nickname avatar getters this.nickname() this.avatar() 便
...mapGetters(['nickname', 'avatar']),
handleLogout () {
const that = this
const that = this;
// 使 this.$confirm Ant Design Vue "" "?"
this.$confirm({
title: '提示',
content: '真的要注销登录吗 ?',
content: '真的要注销登录吗?',
onOk () {
// this.Logout({}) mapActions 退 action Promise 退.then Promise resolve window.location.reload() Promise reject.catch 使 this.$message.error Ant Design Vue "" err.message
return that.Logout({}).then(() => {
window.location.reload()
window.location.reload();
}).catch(err => {
that.$message.error({
title: '错误',
description: err.message
})
})
});
});
},
onCancel () {
// 退
}
})
});
}
}
}

@ -12,22 +12,35 @@
* storageOptions: {} - Vue-ls 插件配置项 (localStorage/sessionStorage)
*
*/
export default {
// 定义 `primaryColor` 属性,其值为 `#1890FF`,这是一个十六进制颜色值,代表一种蓝色。从注释可知,它是 Ant Design 框架的主色调,通常用于按钮、重要提示、活跃状态等元素的颜色设置,使得整个项目在视觉上保持统一的主题色风格,方便用户识别和区分重要元素。
primaryColor: '#1890FF', // primary color of ant design
// 定义 `navTheme` 属性,其值为 `dark`用于设置导航菜单nav menu的主题风格这里表示采用深色主题。根据不同的业务需求和设计风格可以切换为其他主题比如 `light` 等),深色主题一般文字颜色较亮,背景色较深,在视觉上有较高的对比度,适用于多种场景,比如夜间模式或者需要突出显示菜单内容的情况。
navTheme: 'dark', // theme for nav menu
// 定义 `layout` 属性,其值为 `topmenu`用于指定导航菜单nav menu在页面布局中的位置这里表示导航菜单位于页面顶部topmenu除此之外还可能有 `sidemenu`(侧边栏菜单)等其他取值,通过该属性可以灵活调整菜单的布局位置,以适应不同的页面设计和用户操作习惯。
layout: 'topmenu', // nav menu position: sidemenu or topmenu
// 定义 `contentWidth` 属性,其值为 `Fixed`用于确定页面内容区域的布局方式这里表示采用固定宽度Fixed的布局方式与之相对的还有 `Fluid`(流式布局,宽度可自适应变化)。注释中提到该属性仅在 `layout` 为 `topmenu` 时生效,意味着在顶部菜单布局模式下,可以进一步选择内容区域是固定宽度还是自适应宽度,以实现更精准的页面布局控制。
contentWidth: 'Fixed', // layout of content: Fluid or Fixed, only works when layout is topmenu
// 定义 `fixedHeader` 属性,其值为 `true`表示页面的头部header是固定的也就是在页面滚动时头部会始终保持在页面顶部可见这种粘性头部sticky header的设计常用于方便用户随时访问头部的导航、搜索等功能无需滚动回顶部提升了用户操作的便捷性。
fixedHeader: true, // sticky header
// 定义 `fixSiderbar` 属性,其值为 `false`用于控制侧边栏siderbar是否固定这里设置为 `false` 表示侧边栏不会固定在页面的某个位置,而是会随着页面滚动等操作正常显示或隐藏,若设置为 `true`,则侧边栏会像粘性头部一样,在页面滚动过程中始终保持可见,常用于需要侧边栏始终展示重要信息或操作选项的场景。
fixSiderbar: false, // sticky siderbar
// 定义 `autoHideHeader` 属性,其值为 `false`,用于控制页面头部是否自动隐藏,当设置为 `true` 时,在满足一定条件(比如页面向下滚动一定距离等)下,头部会自动隐藏起来,以提供更多的页面可视空间,常用于移动端或者一些追求极简浏览体验的页面设计中,这里设置为 `false` 表示头部不会自动隐藏,始终保持显示状态。
autoHideHeader: false, // auto hide header
// 定义 `colorWeak` 属性,其值为 `false`,从字面意思推测可能用于控制是否开启针对色弱用户的颜色辅助模式,若设置为 `true`,则可能会对页面整体颜色进行调整(比如应用特定的颜色滤镜等方式),使其更便于色弱人群查看和识别页面内容,这里设置为 `false` 表示不开启该辅助模式。
colorWeak: false,
// 定义 `multiTab` 属性,其值为 `false`,可能用于表示是否启用多标签页功能,若设置为 `true`,页面可能会支持同时打开多个标签页来展示不同的内容页面,方便用户在多个相关内容之间快速切换,这里设置为 `false` 表示不启用该功能。
multiTab: false,
production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true',
// 定义 `production` 属性,其值通过一个表达式 `process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW!== 'true'` 来确定。在 JavaScript 项目中,`process.env.NODE_ENV` 通常用于区分开发环境、生产环境等不同的运行环境,这里表示只有当当前环境是生产环境(`'production'`)并且 `VUE_APP_PREVIEW` 环境变量不等于 `'true'` 时,该属性才为 `true`,可以基于这个属性在代码中针对不同环境做不同的逻辑处理,比如在生产环境下执行代码压缩、优化等操作,而在其他环境下执行调试相关的逻辑等。
production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW!== 'true',
// 定义 `storageOptions` 属性,它是一个对象,用于配置 `vue-ls` 相关的选项(`vue-ls` 可能是用于操作本地存储的库),以下是该对象内部属性的具体说明。
// vue-ls options, localStorage中默认的存储前缀
storageOptions: {
// 定义 `namespace` 属性,其值为 `'pro__'`它作为存储在本地存储localStorage中的键key的前缀使用通过添加前缀可以避免不同项目或者模块之间的键名冲突方便对存储的数据进行分类管理比如项目中所有和用户相关的数据存储键名可以都加上这个前缀来区分。
namespace: 'pro__', // key prefix
// 定义 `name` 属性,其值为 `'ls'`,这个属性可能用于在 Vue 实例中引用 `vue-ls` 相关功能时的变量名,比如可以通过 `Vue.[ls]` 或者 `this.[$ls]`(具体使用方式可能取决于 `vue-ls` 的 API 设计)来操作本地存储相关的功能,给开发者提供了一种统一、便捷的调用方式来处理数据存储和读取等操作。
name: 'ls', // name variable Vue.[ls] or this.[$ls],
// 定义 `storage` 属性,其值为 `'local'`用于指定数据存储的具体存储位置这里表示存储在本地存储localStorage除此之外还可能有 `'session'`(存储在会话存储 sessionStorage 中)、`'memory'`(可能是存储在内存中,通常用于临时数据存储等情况)等其他取值,根据业务需求可以灵活选择不同的存储位置来满足数据存储的生命周期、安全性等方面的要求。
storage: 'local' // storage name session, local, memory
}
}
}

@ -1,6 +1,18 @@
// eslint-disable-next-line
import { UserLayout, BasicLayout, RouteView, BlankLayout, PageView } from '../layouts'
import { examList, examAdmin, questionAdmin } from '../core/icons'
// eslint-disable-next-line
import { UserLayout, BasicLayout, RouteView, BlankLayout, PageView } from '../layouts';
// 上面这行代码导入了一些布局组件,从相对路径 '../layouts' 引入不同的布局相关组件。
// 'eslint-disable-next-line' 注释表示禁用下一行代码的 ESLint 检查规则,可能是当前导入语句不符合某些 ESLint 配置的规范,但又需要保留这样的写法,所以暂时禁用检查。
// 导入的各个组件用途推测如下:
// UserLayout可能是用于用户相关页面如登录、注册等的布局组件提供特定的页面结构和样式呈现方式。
// BasicLayout大概率是整个应用的基础布局组件包含了如头部、侧边栏、主体内容区等常见的页面布局结构作为大多数页面的基础框架。
// RouteView可能是用于根据路由配置来展示不同视图内容的组件起到路由与具体页面视图之间的衔接作用根据路由的切换来动态加载相应的子组件内容。
// BlankLayout也许是一种空白的、极简的布局组件用于一些不需要太多页面装饰或者特定场景下只展示核心内容的页面布局。
// PageView可能是针对具体页面内容展示的一种布局包装组件用于规范页面内容在整体布局中的呈现形式等。
import { examList, examAdmin, questionAdmin } from '../core/icons';
// 从相对路径 '../core/icons' 导入一些图标相关的资源,这些图标资源可能是用于在页面菜单、按钮等地方进行可视化展示,增强界面的交互性和直观性,
// 例如 'examList'、'examAdmin'、'questionAdmin' 等分别代表不同功能对应的图标,具体图标形式(可能是 SVG、字体图标等取决于其定义和实现方式。
export const asyncRouterMap = [
@ -28,6 +40,12 @@ export const asyncRouterMap = [
}
]
},
// 以下是对 'dashboard' 路由配置的详细注释:
// 'path':表示该路由的路径,'/dashboard' 表明访问以 '/dashboard' 开头的 URL 时会匹配到这个路由配置。
// 'name':路由的名称,用于在代码中方便地引用这个路由,例如在编程式导航等场景下可以通过路由名称来进行跳转操作,这里取名为 'dashboard'。
// 'component':指定该路由对应的组件,这里使用 RouteView 组件,它会根据子路由情况来展示具体内容,并且由于设置了 'hideChildrenInMenu' 为 true意味着它的子路由对应的菜单项不会直接显示在菜单中可能是通过其他方式来触发展示这些子页面比如点击某个按钮等
// 'meta':是一个包含额外元数据的对象,用于传递一些路由相关的自定义信息,这里设置了 'title' 为 '首页',用于在页面标题或者菜单显示中展示相应的名称;'keepAlive' 为 true表示该页面组件在切换路由等情况下会被缓存下次再次访问时可以更快地展示'icon' 为 'home',指定了在菜单等地方显示的图标;'permission' 为 ['dashboard'],用于定义访问该路由需要具备的权限,可用于权限验证逻辑,只有拥有 'dashboard' 权限的用户才能访问此路由对应的页面。
// 内部的子路由 '/dashboard/home' 配置:同样有自己的 'path'、'name'、'component' 和 'meta' 属性,它对应的组件是从 '../views/Home' 动态导入的,展示的页面标题是 '简介',同样也是被缓存且需要 'dashboard' 权限才能访问。
{
path: '/exam-card',
@ -45,6 +63,8 @@ export const asyncRouterMap = [
}
]
},
// 对于 '/exam-card' 路由配置的注释类似上面,它对应的布局组件是 PageView通过图标 'examList' 来标识,代表的页面标题是 '考试卡片',访问需要 'exam-card' 权限,其内部子路由 '/list/exam-card' 对应的组件是展示 '考试卡片列表' 的页面,同样有缓存和权限要求。
{
path: '/question-admin',
name: 'question-admin',
@ -62,6 +82,8 @@ export const asyncRouterMap = [
}
]
},
// '/question-admin' 路由配置也是类似逻辑,使用 PageView 布局,图标是 'questionAdmin',代表 '问题管理' 页面,权限要求是 'question-admin',内部子路由展示 '问题列表' 页面且设置了强制显示菜单项的相关属性,同样支持缓存和有对应权限要求。
// list
{
path: '/list/exam-table-list',
@ -80,6 +102,8 @@ export const asyncRouterMap = [
}
]
},
// 这部分是关于 '考试管理' 相关路由配置,布局采用 PageView图标为 'examAdmin',权限为 'exam-table-list',子路由展示具体的 '考试列表' 页面,同样有强制显示菜单项以及缓存、权限相关设置。
{
path: '/exam-record-list',
name: 'exam-record-list',
@ -96,6 +120,8 @@ export const asyncRouterMap = [
}
]
},
// 针对 '我的考试' 相关路由配置,布局是 PageView图标用 'user' 表示,权限要求是 'exam-record-list',内部子路由展示 '我参与过的考试列表' 页面,具备缓存机制和相应的权限要求。
// account
{
path: '/account',
@ -130,14 +156,21 @@ export const asyncRouterMap = [
}
]
}
// 'account' 路由配置用于个人相关页面,整体使用 RouteView 组件,设置为隐藏菜单('hidden' 为 true但又在页面中是有必要存在的可能通过其他方式来访问相关子页面。
// 其内部的 '/account/settings' 子路由对应个人设置相关内容,页面标题是 '个人设置',还隐藏了头部('hideHeader' 为 true内部进一步细分了 '基本设置' 和 '个性化设置' 等子路由,这些子路由大多也设置为隐藏状态,并且有相应的权限要求和缓存设置等,通过动态导入不同的组件来展示具体的设置页面内容。
]
},
{
// 所有访问不到的路径最终都会落到404里
path: '*', redirect: '/404', hidden: true
}
// 这个通配符路由配置表示当用户访问的路径在前面定义的路由中都无法匹配到时,会自动重定向到 '/404' 页面,并且该路由设置为隐藏,不会在菜单等地方显示出来,用于处理页面不存在等异常情况的路由跳转。
]
/**
* 基础路由不在主菜单上展示独立的路由
* @type { *[] }
*/
/**
* 基础路由不在主菜单上展示独立的路由
* @type { *[] }
@ -166,6 +199,8 @@ export const constantRouterMap = [
}
]
},
// 对于 '/user' 路由配置,它使用 UserLayout 布局组件,整体设置为隐藏('hidden' 为 true意味着不会在主菜单等常规地方展示主要用于用户相关的基础操作如登录、注册以及注册结果展示等功能。
// 内部的子路由分别对应不同的用户操作页面,通过动态导入相应的组件来实现具体页面内容的展示,并且使用 'webpackChunkName' 注释来指定 webpack 打包时的代码块名称,方便代码的按需加载和管理。
{
path: '/test',
@ -189,11 +224,14 @@ export const constantRouterMap = [
}
]
},
// '/test' 路由配置使用 BlankLayout 布局,先重定向到 '/test/home',其内部有多个子路由分别对应不同的测试页面,通过动态导入不同的组件来展示如 'Home'、'SummerNoteDemo'、'BootStrapTableDemo' 等页面内容,可能用于开发过程中的功能测试或者示例展示等场景。
{
path: '/404',
component: () => import(/* webpackChunkName: "fail" */ '../views/exception/404')
},
// 这个路由配置用于展示 404 页面,当用户访问的路径不存在或者出现错误时,会通过动态导入 '../views/exception/404' 组件来展示相应的 404 错误页面,同样使用 'webpackChunkName' 来指定打包的代码块名称。
{
path: '/exam/:id',
component: () => import(/* webpackChunkName: "fail" */ '../views/list/ExamDetail')
@ -202,4 +240,5 @@ export const constantRouterMap = [
path: '/exam/record/:exam_id/:record_id',
component: () => import(/* webpackChunkName: "fail" */ '../views/list/ExamRecordDetail')
}
]
// 这两个路由配置带有动态参数(':id'、':exam_id'、':record_id'),分别用于展示考试详情和考试记录详情相关页面,通过传入不同的参数值来获取对应的具体内容,也是通过动态导入相应组件来实现页面展示,并且有指定的打包代码块名称用于代码管理。
]

@ -1,5 +1,8 @@
import Vue from 'vue'
import store from '../store/'
// 导入 Vue 框架,这是整个 Vue 项目的核心库,用于创建 Vue 实例、组件以及使用 Vue 提供的各种功能、指令、插件等,后续代码中的相关操作都依赖于这个导入的 Vue 库。
import Vue from 'vue';
// 导入项目中的 Vuex 状态管理库的 store 实例,通常 store 用于存储整个应用的共享状态数据,并且可以通过提交 mutations 等方式来修改这些状态数据,这里导入的 store 会在后续代码中用于更新相关的配置状态。
import store from '../store/';
// 从 '../store/mutation-types' 文件中导入一系列常量,这些常量大概率是用于标识 Vuex store 中不同的 mutation 类型名称,方便在代码中清晰、统一地调用相应的 mutation 来更新状态,每个常量都对应着特定的功能配置相关的状态修改操作,比如主题切换、布局模式改变等。
import {
ACCESS_TOKEN,
DEFAULT_COLOR,
@ -12,21 +15,45 @@ import {
DEFAULT_FIXED_SIDEMENU,
DEFAULT_CONTENT_WIDTH_TYPE,
DEFAULT_MULTI_TAB
} from '../store/mutation-types'
import config from '../config/defaultSettings'
} from '../store/mutation-types';
// 导入项目的默认配置对象 'config'这个对象中包含了如导航菜单主题navTheme、页面布局layout、头部是否固定fixedHeader等各种默认的配置项后续代码会根据本地存储中的数据与这些默认配置进行对比和设置以此来初始化应用的各项配置状态。
import config from '../config/defaultSettings';
// 定义一个名为 'Initializer' 的函数,从命名推测它的作用是用于初始化应用的一些配置相关的状态数据,可能在应用启动阶段被调用,确保各项配置按照预期进行设置。
export default function Initializer () {
store.commit('SET_SIDEBAR_TYPE', Vue.ls.get(SIDEBAR_TYPE, true))
store.commit('TOGGLE_THEME', Vue.ls.get(DEFAULT_THEME, config.navTheme))
store.commit('TOGGLE_LAYOUT_MODE', Vue.ls.get(DEFAULT_LAYOUT_MODE, config.layout))
store.commit('TOGGLE_FIXED_HEADER', Vue.ls.get(DEFAULT_FIXED_HEADER, config.fixedHeader))
store.commit('TOGGLE_FIXED_SIDERBAR', Vue.ls.get(DEFAULT_FIXED_SIDEMENU, config.fixSiderbar))
store.commit('TOGGLE_CONTENT_WIDTH', Vue.ls.get(DEFAULT_CONTENT_WIDTH_TYPE, config.contentWidth))
store.commit('TOGGLE_FIXED_HEADER_HIDDEN', Vue.ls.get(DEFAULT_FIXED_HEADER_HIDDEN, config.autoHideHeader))
store.commit('TOGGLE_WEAK', Vue.ls.get(DEFAULT_COLOR_WEAK, config.colorWeak))
store.commit('TOGGLE_COLOR', Vue.ls.get(DEFAULT_COLOR, config.primaryColor))
store.commit('TOGGLE_MULTI_TAB', Vue.ls.get(DEFAULT_MULTI_TAB, config.multiTab))
store.commit('SET_TOKEN', Vue.ls.get(ACCESS_TOKEN))
// 通过调用 store 的 'commit' 方法提交一个名为 'SET_SIDEBAR_TYPE' 的 mutation用于设置侧边栏类型SIDEBAR_TYPE的状态值。
// 这里使用 'Vue.ls.get' 方法从本地存储(可能是通过 Vue 相关的存储插件实现的本地存储操作中获取对应键SIDEBAR_TYPE的值如果获取不到则使用默认值 'true',然后将获取到的值作为参数传递给 mutation以此来更新侧边栏类型的状态。
store.commit('SET_SIDEBAR_TYPE', Vue.ls.get(SIDEBAR_TYPE, true));
// 提交名为 'TOGGLE_THEME' 的 mutation用于切换应用的主题如亮色主题、暗色主题等
// 从本地存储中获取键为 'DEFAULT_THEME' 的值如果不存在则使用配置文件config中定义的默认主题config.navTheme再将获取到的值传递给 mutation 来更新主题相关的状态。
store.commit('TOGGLE_THEME', Vue.ls.get(DEFAULT_THEME, config.navTheme));
// 提交 'TOGGLE_LAYOUT_MODE' mutation用于切换页面布局模式例如侧边栏布局、顶部菜单布局等不同的布局形式
// 从本地存储获取 'DEFAULT_LAYOUT_MODE' 的值若不存在则取配置文件里的默认布局config.layout接着将该值传入 mutation 来更新布局模式的状态。
store.commit('TOGGLE_LAYOUT_MODE', Vue.ls.get(DEFAULT_LAYOUT_MODE, config.layout));
// 提交 'TOGGLE_FIXED_HEADER' mutation用于控制页面头部是否固定显示例如在页面滚动时头部是否始终保持在顶部可见
// 通过 'Vue.ls.get' 获取本地存储中 'DEFAULT_FIXED_HEADER' 的值若没有则采用配置里的默认设置config.fixedHeader再以此值来更新头部固定相关的状态。
store.commit('TOGGLE_FIXED_HEADER', Vue.ls.get(DEFAULT_FIXED_HEADER, config.fixedHeader));
// 提交 'TOGGLE_FIXED_SIDERBAR' mutation目的是控制侧边栏是否固定即在页面滚动时侧边栏是否保持在固定位置可见
// 从本地存储获取 'DEFAULT_FIXED_SIDEMENU' 的值不存在时使用配置中的默认值config.fixSiderbar并将该值用于更新侧边栏固定相关的状态。
store.commit('TOGGLE_FIXED_SIDERBAR', Vue.ls.get(DEFAULT_FIXED_SIDEMENU, config.fixSiderbar));
// 提交 'TOGGLE_CONTENT_WIDTH' mutation用于切换页面内容区域的宽度模式比如固定宽度或者自适应宽度等
// 先从本地存储获取 'DEFAULT_CONTENT_WIDTH_TYPE' 的值若获取不到则使用配置里的默认宽度设置config.contentWidth随后用该值去更新内容宽度相关的状态。
store.commit('TOGGLE_CONTENT_WIDTH', Vue.ls.get(DEFAULT_CONTENT_WIDTH_TYPE, config.contentWidth));
// 提交 'TOGGLE_FIXED_HEADER_HIDDEN' mutation用于控制页面头部是否自动隐藏例如当页面滚动到一定程度时头部自动隐藏起来以增加可视空间
// 使用 'Vue.ls.get' 拿到本地存储中 'DEFAULT_FIXED_HEADER_HIDDEN' 的值若缺失就用配置里的默认设置config.autoHideHeader再用这个值更新头部自动隐藏相关的状态。
store.commit('TOGGLE_FIXED_HEADER_HIDDEN', Vue.ls.get(DEFAULT_FIXED_HEADER_HIDDEN, config.autoHideHeader));
// 提交 'TOGGLE_WEAK' mutation可能用于切换是否开启针对色弱用户的颜色辅助模式比如调整页面颜色显示以便色弱人群更好地查看内容
// 从本地存储获取 'DEFAULT_COLOR_WEAK' 的值没有的话就取配置中的默认值config.colorWeak最后用该值来更新颜色辅助模式相关的状态。
store.commit('TOGGLE_WEAK', Vue.ls.get(DEFAULT_COLOR_WEAK, config.colorWeak));
// 提交 'TOGGLE_COLOR' mutation大概是用于切换应用的主色调之类的颜色相关设置例如按钮颜色、重要提示颜色等与主色调相关的元素颜色
// 通过 'Vue.ls.get' 从本地存储获取 'DEFAULT_COLOR' 的值若不存在则采用配置里的默认主色调config.primaryColor再把该值传递给 mutation 来更新颜色相关的状态。
store.commit('TOGGLE_COLOR', Vue.ls.get(DEFAULT_COLOR, config.primaryColor));
// 提交 'TOGGLE_MULTI_TAB' mutation可能用于控制是否启用多标签页功能允许用户同时打开多个页面标签进行切换浏览等操作
// 从本地存储获取 'DEFAULT_MULTI_TAB' 的值若取不到就用配置里的默认设置config.multiTab然后用此值更新多标签页相关的状态。
store.commit('TOGGLE_MULTI_TAB', Vue.ls.get(DEFAULT_MULTI_TAB, config.multiTab));
// 提交 'SET_TOKEN' mutation用于设置访问令牌ACCESS_TOKEN相关的状态值从本地存储获取对应的令牌值如果有并将其传递给 mutation 来更新令牌相关的状态,令牌通常用于用户认证、接口访问授权等方面的功能。
store.commit('SET_TOKEN', Vue.ls.get(ACCESS_TOKEN));
// last step
}
// 这里的注释 'last step' 可能表示这是初始化配置相关操作的最后一步,不过目前代码中没有进一步的具体操作体现,如果还有后续需要在初始化完成后执行的逻辑,可以添加在这之后。
}

@ -14,21 +14,34 @@ import store from '../../store'
*
* @see https://github.com/sendya/ant-design-pro-vue/pull/53
*/
// 创建一个名为 'action' 的 Vue 自定义指令,通过 Vue.directive 方法来定义。
// 自定义指令可以让开发者在 Vue 模板中对 DOM 元素进行更细粒度的操作控制,比如根据特定条件来显示、隐藏元素或者修改元素的其他属性等。
const action = Vue.directive('action', {
// 'inserted' 是自定义指令的一个钩子函数,当被绑定的元素插入到 DOM 中时会触发该函数,在这里可以进行相关的初始化操作或者基于元素的初始状态进行一些逻辑处理。
inserted: function (el, binding, vnode) {
// 从指令的绑定参数binding.arg中获取操作名称actionName这个名称可能用于后续判断该元素对应的操作是否有权限执行等逻辑具体含义取决于业务场景中对这个指令的使用方式。
const actionName = binding.arg
// 从 Vuex 的 store 中获取当前用户的角色信息roles这里假设 'roles' 是一个包含了用户角色相关数据的对象,比如角色名称、角色所拥有的权限列表等,通常是通过 Vuex 的 getters 来方便地获取全局状态中的数据。
const roles = store.getters.roles
// 获取当前路由vnode.context.$route元数据meta中的权限信息permission这个权限信息可能是在定义路由时设置的用于表示访问该路由所需要的权限其值的类型可能是字符串或者数组等具体格式取决于项目中的路由配置方式。
const elVal = vnode.context.$route.meta.permission
// 对获取到的权限信息elVal进行处理如果它是一个字符串类型则将其转换为只包含该字符串的数组方便后续统一进行包含关系的判断等操作如果本身就是数组类型则直接使用确保 'permissionId' 是一个数组形式的权限标识集合。
const permissionId = elVal instanceof String && [elVal] || elVal
// 遍历用户角色所拥有的权限列表roles.permissions这里每个权限对象p可能包含了如 'permissionId'(权限标识)和 'actionList'(该权限下可执行的操作列表)等属性,以下是针对每个权限对象进行的逻辑判断。
roles.permissions.forEach(p => {
// 判断当前遍历到的权限标识p.permissionId是否不在传入的权限标识集合permissionId如果不在则说明当前权限不匹配直接跳过本次循环继续检查下一个权限对象。
if (!permissionId.includes(p.permissionId)) {
return
}
if (p.actionList && !p.actionList.includes(actionName)) {
// 如果当前权限对象的 'actionList' 属性存在(即不是 null 或 undefined表示有对应的操作列表并且当前指令所对应的操作名称actionName不在这个操作列表中说明当前用户虽然有该路由的访问权限但没有执行这个具体操作的权限需要对元素进行隐藏处理。
if (p.actionList &&!p.actionList.includes(actionName)) {
// 尝试获取元素的父节点el.parentNode如果父节点存在则从父节点中移除当前元素el这是一种从 DOM 中移除元素的方式;
// 如果获取父节点失败(可能元素已经没有父节点,比如它本身就是根元素等情况),则通过设置元素的 'display' 属性为 'none' 来隐藏元素,使其在页面上不可见,达到根据权限控制元素显示隐藏的目的。
el.parentNode && el.parentNode.removeChild(el) || (el.style.display = 'none')
}
})
}
})
// 将定义好的 'action' 自定义指令导出,方便在其他 Vue 组件或者模块中进行导入和使用,使得整个项目中可以基于这个指令来实现根据权限动态控制元素显示隐藏等功能。
export default action

@ -6,10 +6,22 @@
* 自定义图标加载表
* 所有图标均从这里加载方便管理
*/
import bxAnaalyse from '../assets/icons/bx-analyse.svg?inline' // path to your '*.svg?inline' file.
import examList from '../assets/icons/exam-list.svg?inline' // path to your '*.svg?inline' file.
import examAdmin from '../assets/icons/exam-admin.svg?inline' // path to your '*.svg?inline' file.
import questionAdmin from '../assets/icons/question-admin.svg?inline' // path to your '*.svg?inline' file.
import mine from '../assets/icons/mine.svg?inline' // path to your '*.svg?inline' file.
// 导入名为 `bxAnaalyse` 的 SVG 图标文件,这里使用了一种特殊的导入方式,即在文件名后添加 `?inline`。
// 这种方式通常在一些构建工具(如 Vue 项目中配合 webpack 等)的配置下,会将 SVG 文件内容以内联的形式引入,即将 SVG 代码直接嵌入到 JavaScript 代码中,而不是以外部文件引用的方式加载。
// `../assets/icons/bx-analyse.svg?inline` 表示该 SVG 文件的相对路径,从当前文件所在目录向上一级(`../`),然后进入 `assets/icons` 目录找到对应的 `bx-analyse.svg` 文件进行内联式导入。
import bxAnaalyse from '../assets/icons/bx-analyse.svg?inline'; // path to your '*.svg?inline' file.
export { bxAnaalyse, examList, examAdmin, questionAdmin, mine }
// 与上面类似,导入名为 `examList` 的 SVG 图标文件,同样是通过内联的方式从指定的相对路径 `../assets/icons/exam-list.svg?inline` 引入,方便在后续代码中直接使用该 SVG 图标资源,比如在组件中作为图标展示在按钮、菜单等地方。
import examList from '../assets/icons/exam-list.svg?inline'; // path to your '*.svg?inline' file.
// 导入名为 `examAdmin` 的 SVG 图标文件,按内联导入的规则,从 `../assets/icons/exam-admin.svg?inline` 路径获取对应的 SVG 文件内容,使其可在代码中按需使用,可能用于与考试管理相关的界面元素上展示对应的图标形象。
import examAdmin from '../assets/icons/exam-admin.svg?inline'; // path to your '*.svg?inline' file.
// 导入 `questionAdmin` SVG 图标文件,从相对路径 `../assets/icons/question-admin.svg?inline` 以内联方式引入,该图标大概率会用于和问题管理相关的界面部分进行可视化展示,增强界面的直观性和交互性。
import questionAdmin from '../assets/icons/question-admin.svg?inline'; // path to your '*.svg?inline' file.
// 导入 `mine` SVG 图标文件,按照内联导入的要求,从 `../assets/icons/mine.svg?inline` 这个相对路径位置引入 SVG 文件内容,可能用于代表与“我的”相关功能(如个人中心、我的记录等)的界面图标展示。
import mine from '../assets/icons/mine.svg?inline'; // path to your '*.svg?inline' file.
// 使用 `export` 关键字将导入的这几个 SVG 图标资源(`bxAnaalyse`、`examList`、`examAdmin`、`questionAdmin`、`mine`)导出,使得其他模块可以通过导入这个模块来获取这些图标资源,进而在整个项目的不同组件、页面等地方进行复用,用于构建具有图标展示的界面元素,提升用户界面的可视化效果和用户体验。
export { bxAnaalyse, examList, examAdmin, questionAdmin, mine };

@ -1,20 +1,32 @@
import Vue from 'vue'
import VueStorage from 'vue-ls'
import config from '../config/defaultSettings'
// 导入 Vue 框架,它是整个 Vue 项目的核心库,后续的各种插件使用、组件创建以及功能实现等都依赖于这个基础库,通过导入 Vue才能在代码中使用 Vue 提供的各种 API、指令、组件等相关功能。
import Vue from 'vue';
// 导入 'vue-ls' 库并将其重命名为 'VueStorage''vue-ls' 通常用于在 Vue 项目中方便地操作本地存储(如 localStorage 和 sessionStorage可以实现数据的存储、读取以及设置存储相关的配置等功能后续会通过 Vue.use() 方法将其注册为 Vue 的插件来使用。
import VueStorage from 'vue-ls';
// 导入项目的默认配置对象 'config',这个对象应该包含了项目中各种功能模块相关的默认设置参数,例如可能涉及页面布局、主题样式、存储相关配置等方面的默认值,后续代码会基于这些默认配置来进行一些初始化或者插件配置等操作。
import config from '../config/defaultSettings';
// base library
import '../core/lazy_lib/components_use'
import Viser from 'viser-vue'
// 导入 '../core/lazy_lib/components_use' 文件,从文件名推测可能是用于懒加载组件相关的逻辑处理,也许是定义了哪些组件需要延迟加载以及如何加载的相关代码,不过具体功能取决于该文件内部的实际实现内容。
import '../core/lazy_lib/components_use';
// 导入 'viser-vue' 库并将其重命名为 'Viser''viser-vue' 大概率是一个基于 Vue 的可视化图表库,通过导入它可以在 Vue 项目中方便地创建各种类型的图表(如柱状图、折线图等),后续会将其注册为 Vue 插件来进行使用,以实现数据可视化展示的功能。
import Viser from 'viser-vue';
// ext library
import VueClipboard from 'vue-clipboard2'
import PermissionHelper from '../utils/helper/permission'
import './directives/action'
// 导入 'vue-clipboard2' 库并将其重命名为 'VueClipboard''vue-clipboard2' 是一个用于在 Vue 项目中实现复制功能的插件,比如可以方便地实现复制文本内容到剪贴板的操作,常用于一些需要用户复制信息(如链接、代码片段等)的场景,这里导入后会对其进行相关配置并注册为 Vue 插件使用。
import VueClipboard from 'vue-clipboard2';
// 导入 'PermissionHelper' 模块,从命名推测它是用于处理权限相关帮助函数或类的模块,可能包含了如权限验证、权限判断等功能相关的代码,用于辅助项目中实现各种权限控制相关的逻辑,后续会将其注册为 Vue 插件融入到 Vue 项目中使用。
import PermissionHelper from '../utils/helper/permission';
// 导入 './directives/action' 文件,从文件名推测这是一个自定义指令的定义文件,自定义指令可以让开发者在 Vue 模板中对 DOM 元素进行更细粒度的操作控制,例如根据特定条件来显示、隐藏元素或者修改元素的属性等,不过这里只是导入,具体的指令定义和功能取决于该文件内部代码实现。
VueClipboard.config.autoSetContainer = true
// 配置 'VueClipboard' 插件的 'autoSetContainer' 属性为 'true',这个属性设置可能是用于控制是否自动设置复制操作对应的容器相关行为,具体含义取决于 'vue-clipboard2' 插件的 API 文档,将其设置为 'true' 大概率是开启某种默认的、方便的容器设置机制,以便更好地实现复制功能。
VueClipboard.config.autoSetContainer = true;
Vue.use(Viser)
// 使用 Vue.use() 方法将 'Viser' 注册为 Vue 的插件,注册后在 Vue 组件中就可以方便地使用 'viser-vue' 库提供的图表相关组件和功能,例如通过在组件的模板中添加对应的标签来创建各种可视化图表,以此实现数据可视化展示的业务需求。
Vue.use(Viser);
Vue.use(VueStorage, config.storageOptions)
Vue.use(VueClipboard)
Vue.use(PermissionHelper)
// 使用 Vue.use() 方法将 'VueStorage' 注册为 Vue 的插件,并传入 'config.storageOptions' 作为配置参数,这样 'vue-ls' 库就能按照项目的默认存储配置(例如存储的前缀、存储类型等参数,定义在 'config.storageOptions' 中)来操作本地存储,方便在项目中进行数据的持久化存储和读取操作。
Vue.use(VueStorage, config.storageOptions);
// 使用 Vue.use() 方法将 'VueClipboard' 注册为 Vue 的插件,注册成功后,在 Vue 组件中就可以利用 'vue-clipboard2' 提供的 API 来实现复制文本到剪贴板等相关操作,提升用户在使用页面过程中的交互便利性。
Vue.use(VueClipboard);
// 使用 Vue.use() 方法将 'PermissionHelper' 注册为 Vue 的插件,之后在整个 Vue 项目中就可以借助该模块提供的权限相关功能来进行权限控制,比如判断用户是否有权限访问某个页面、执行某个操作等,确保系统的安全性和数据的访问控制符合业务要求。
Vue.use(PermissionHelper);

@ -1,25 +1,43 @@
import Vue from 'vue'
import VueStorage from 'vue-ls'
import config from '../config/defaultSettings'
// 导入 Vue 框架,它是整个 Vue 项目构建的基础,提供了创建组件、管理数据、响应式系统等核心功能,后续所有基于 Vue 的插件、组件及功能拓展都依赖于此基础库来运行和交互。
import Vue from 'vue';
// 导入 'vue-ls' 库并将其重命名为 'VueStorage''vue-ls' 主要用于在 Vue 项目里方便地操作本地存储(例如 localStorage 和 sessionStorage能够进行数据的存储、读取以及按照特定配置管理存储相关的操作后续会通过 Vue.use() 方法把它注册为 Vue 插件来融入项目使用。
import VueStorage from 'vue-ls';
// 导入项目默认配置对象 'config',该对象中包含了众多项目相关的默认设置信息,像页面布局、样式主题、存储相关的各种参数设定等,会在后续的插件初始化等操作中作为配置依据来使用,确保各功能模块按照既定的默认规则运行。
import config from '../config/defaultSettings';
// base library
import Antd from 'ant-design-vue'
import Viser from 'viser-vue'
import VueCropper from 'vue-cropper'
import 'ant-design-vue/dist/antd.less'
// 导入 'ant-design-vue' 库并将其重命名为 'Antd''ant-design-vue' 是一套基于 Vue 的高质量 UI 组件库,提供了丰富的、样式美观且功能实用的 UI 组件(如按钮、表单、菜单等),通过将其注册为 Vue 插件,就能在项目的 Vue 组件中方便地使用这些组件来搭建页面,提升开发效率以及保证页面的一致性和美观性。
import Antd from 'ant-design-vue';
// 导入 'viser-vue' 库并将其重命名为 'Viser''viser-vue' 通常是一个用于在 Vue 项目中实现数据可视化的库,支持创建各种各样的图表(比如柱状图、折线图、饼图等),注册为 Vue 插件后,开发者可以在 Vue 组件的模板里轻松运用其组件和功能来展示可视化数据,满足业务上数据展示和分析的需求。
import Viser from 'viser-vue';
// 导入 'vue-cropper' 库并将其重命名为 'VueCropper''vue-cropper' 大概率是一个用于在 Vue 项目里实现图片裁剪功能的插件,方便对图片进行裁剪操作,例如用户上传头像需要裁剪合适尺寸、处理图片素材等场景下会发挥作用,后续通过注册为 Vue 插件可在组件中调用相关裁剪功能。
import VueCropper from 'vue-cropper';
// 引入 'ant-design-vue' 库对应的样式文件 'antd.less',确保项目能正确应用 'ant-design-vue' 组件库的默认样式,使得 UI 组件按照其设计风格正常显示,若缺少此样式文件引入,组件可能会出现样式缺失或错乱的情况。
import 'ant-design-vue/dist/antd.less';
// ext library
import VueClipboard from 'vue-clipboard2'
import PermissionHelper from '../utils/helper/permission'
// import '../components/use'
import './directives/action'
// 导入 'vue-clipboard2' 库并将其重命名为 'VueClipboard''vue-clipboard2' 是一个专为 Vue 项目打造的实现复制功能的插件,它可以方便地帮助用户将指定的文本内容复制到系统剪贴板中,常用于需要分享链接、复制代码等操作的业务场景,在这里导入后会进行相关配置并注册为 Vue 插件以供使用。
import VueClipboard from 'vue-clipboard2';
// 导入 'PermissionHelper' 模块,从命名来看它应该是用于辅助处理项目中权限相关逻辑的,里面可能包含了如权限判断、权限验证、根据权限控制页面或组件显示隐藏等功能的代码,后续注册为 Vue 插件后就能在整个项目中便捷地运用这些权限相关功能来保障系统安全和数据访问控制。
import PermissionHelper from '../utils/helper/permission';
// 这里原本导入 '../components/use',但被注释掉了,从文件名推测可能是用于组件相关的某种使用逻辑,也许是统一管理组件的使用方式、初始化组件等功能的代码,不过目前暂时未被启用,若后续有需求可取消注释并根据其内部具体实现来使用。
// import '../components/use';
// 导入 './directives/action' 文件,推测这是一个自定义指令的定义文件,自定义指令在 Vue 中可用于对 DOM 元素进行更细致、特定的操作控制,比如基于某些条件动态地改变元素的显示状态、添加样式等,不过具体功能取决于该文件内部的代码实现内容。
import './directives/action';
VueClipboard.config.autoSetContainer = true
// 配置 'VueClipboard' 插件的 'autoSetContainer' 属性为 'true',这个属性设置很可能与复制操作时如何自动设置复制内容对应的容器有关,将其设为 'true' 也许是开启一种自动适配、默认设置容器的机制,方便更顺利地实现复制功能,具体含义需参照 'vue-clipboard2' 插件的文档说明来准确理解。
VueClipboard.config.autoSetContainer = true;
Vue.use(Antd)
Vue.use(Viser)
// 使用 Vue.use() 方法将 'Antd'(即 'ant-design-vue')注册为 Vue 的插件,注册完成后,在项目的各个 Vue 组件中就可以像使用普通组件一样直接调用 'ant-design-vue' 提供的各种 UI 组件了,能够快速搭建出符合设计风格的页面界面,简化了 UI 开发流程。
Vue.use(Antd);
// 使用 Vue.use() 方法将 'Viser'(即 'viser-vue')注册为 Vue 的插件,这样一来,在 Vue 组件里就能方便地利用其提供的图表组件和功能来创建可视化图表了,便于对业务数据进行直观的展示和分析,满足项目中数据可视化相关的需求。
Vue.use(Viser);
Vue.use(VueStorage, config.storageOptions)
Vue.use(VueClipboard)
Vue.use(PermissionHelper)
Vue.use(VueCropper)
// 使用 Vue.use() 方法将 'VueStorage'(即 'vue-ls')注册为 Vue 的插件,并传入 'config.storageOptions' 作为配置参数,这使得 'vue-ls' 能依据项目预先设定好的存储相关配置(例如存储的前缀、存储类型是本地存储还是会话存储等信息都在 'config.storageOptions' 里定义)来准确地进行本地存储的操作,方便实现数据的持久化存储和读取。
Vue.use(VueStorage, config.storageOptions);
// 使用 Vue.use() 方法将 'VueClipboard' 注册为 Vue 插件,成功注册后,在 Vue 组件中就能利用 'vue-clipboard2' 插件提供的 API 来轻松实现文本复制到剪贴板的功能了,提升了用户在页面交互过程中的便利性,比如方便用户复制重要信息等。
Vue.use(VueClipboard);
// 使用 Vue.use() 方法将 'PermissionHelper' 注册为 Vue 插件,此后在整个 Vue 项目里就能借助它所提供的权限相关功能来进行权限控制了,例如判断用户是否有权限访问某个页面、执行某个操作等,以此确保系统的安全性以及数据访问符合业务设定的权限规则。
Vue.use(PermissionHelper);
// 使用 Vue.use() 方法将 'VueCropper' 注册为 Vue 插件,注册之后,在 Vue 组件中就可以调用 'vue-cropper' 提供的图片裁剪功能了,方便在需要对图片进行裁剪处理的业务场景中使用,比如用户上传图片时裁剪至合适尺寸等操作。
Vue.use(VueCropper);

@ -1,6 +1,8 @@
<template>
<!-- 使用 `a-layout` 组件构建页面布局并根据 `device` 变量的值动态添加类名与下面的样式设置配合来实现不同设备下的布局样式适配例如可能根据是移动端桌面端等设备呈现不同的外观和布局形式 -->
<a-layout :class="['layout', device]">
<!-- SideMenu -->
<!-- 当设备处于移动端通过 `isMobile()` 方法判断显示侧边栏抽屉组件`a-drawer`用于在移动端展示侧边栏菜单提供更好的交互体验和节省屏幕空间 -->
<a-drawer
v-if="isMobile()"
placement="left"
@ -9,6 +11,7 @@
:visible="collapsed"
@close="drawerClose"
>
<!-- 在抽屉内部使用 `side-menu` 组件来展示侧边栏菜单设置菜单模式为 `inline`内联模式可能表示菜单项水平排列等样式特点传入 `menus` 数据用于生成具体的菜单项同时传递主题折叠状态等相关属性用于控制菜单的外观和交互行为 -->
<side-menu
mode="inline"
:menus="menus"
@ -19,6 +22,7 @@
></side-menu>
</a-drawer>
<!-- 当设备不是移动端且满足 `isSideMenu()` 条件时推测是桌面端等支持侧边栏显示的情况显示侧边栏菜单`side-menu`组件同样传入相关属性来配置菜单的展示形式主题折叠状态等这里的折叠状态由 `collapsed` 变量控制 -->
<side-menu
v-else-if="isSideMenu()"
mode="inline"
@ -28,8 +32,10 @@
:collapsible="true"
></side-menu>
<!-- 使用另一个 `a-layout` 组件来进一步构建页面主体部分的布局通过动态绑定类名和内联样式来控制布局样式例如根据不同的 `layoutMode` `contentWidth` 来呈现不同的页面布局风格同时设置最小高度为视口高度`100vh`确保页面内容区域能占满整个屏幕高度 -->
<a-layout :class="[layoutMode, `content-width-${contentWidth}`]" :style="{ paddingLeft: contentPaddingLeft, minHeight: '100vh' }">
<!-- layout header -->
<!-- 使用 `global-header` 组件来展示页面头部传入如布局模式菜单数据主题折叠状态设备类型等属性通过这些属性来控制头部的显示内容和交互行为例如根据不同的布局模式显示不同的菜单样式根据折叠状态显示或隐藏某些元素等同时监听 `toggle` 事件用于处理头部相关的交互操作如展开/收起侧边栏等 -->
<global-header
:mode="layoutMode"
:menus="menus"
@ -40,19 +46,24 @@
/>
<!-- layout content -->
<a-layout-content :style="{ height: '100%', margin: '24px 24px 0', paddingTop: fixedHeader ? '64px' : '0' }">
<!-- 使用 `a-layout-content` 组件来承载页面的主体内容部分设置内容区域的高度为 `100%`占满父容器高度设置外边距以及根据 `fixedHeader` 变量的值来决定顶部的内边距若头部固定则设置顶部内边距为 `64px`否则为 `0`确保内容区域在页面中的布局位置和样式符合设计要求 -->
<a-layout-content :style="{ height: '100%', margin: '24px 24px 0', paddingTop: fixedHeader? '64px' : '0' }">
<!-- 当启用多标签页功能`multiTab` `true`显示 `multi-tab` 组件可能用于实现同时展示多个页面标签并可切换查看不同页面内容的功能方便用户在多个相关页面之间快速切换浏览 -->
<multi-tab v-if="multiTab"></multi-tab>
<!-- 使用 Vue 的过渡效果`transition`组件包裹 `route-view` 组件当路由切换时通过名为 `page-transition` 的过渡效果来实现页面切换的动画效果比如淡入淡出缩放等动画表现提升页面切换的视觉体验 -->
<transition name="page-transition">
<route-view />
</transition>
</a-layout-content>
<!-- layout footer -->
<!-- 使用 `a-layout-footer` 组件来展示页面底部内部包含 `global-footer` 组件用于展示如版权信息联系方式等页面底部相关的通用内容 -->
<a-layout-footer>
<global-footer />
</a-layout-footer>
<!-- Setting Drawer (show in development mode) -->
<!-- 当处于非生产环境`!production`显示 `setting-drawer` 组件从命名推测可能是用于在开发阶段展示一些设置选项的抽屉式组件方便开发人员进行调试配置等操作在正式上线的生产环境中则不显示该组件 -->
<setting-drawer v-if="!production"></setting-drawer>
</a-layout>
</a-layout>
@ -60,20 +71,26 @@
</template>
<script>
import { triggerWindowResizeEvent } from '../utils/util'
import { mapState, mapActions } from 'vuex'
import { mixin, mixinDevice } from '../utils/mixin'
import config from '../config/defaultSettings'
// '../utils/util' `triggerWindowResizeEvent`
import { triggerWindowResizeEvent } from '../utils/util';
// 'vuex' `mapState` `mapActions` `mapState` Vuex 便使`mapActions` Vuex actions mutations 便
import { mapState, mapActions } from 'vuex';
// '../utils/mixin' `mixin` `mixinDevice`Vue mixin使
import { mixin, mixinDevice } from '../utils/mixin';
// `config`
import config from '../config/defaultSettings';
import RouteView from './RouteView'
import MultiTab from '../components/MultiTab'
import SideMenu from '../components/Menu/SideMenu'
import GlobalHeader from '../components/GlobalHeader'
import GlobalFooter from '../components/GlobalFooter'
import SettingDrawer from '../components/SettingDrawer'
// 使 UI
import RouteView from './RouteView';
import MultiTab from '../components/MultiTab';
import SideMenu from '../components/Menu/SideMenu';
import GlobalHeader from '../components/GlobalHeader';
import GlobalFooter from '../components/GlobalFooter';
import SettingDrawer from '../components/SettingDrawer';
export default {
name: 'BasicLayout',
// `mixin` `mixinDevice` 使
mixins: [mixin, mixinDevice],
components: {
RouteView,
@ -85,75 +102,92 @@ export default {
},
data () {
return {
// `config` `production` 使
production: config.production,
// `collapsed` `false``false`
collapsed: false,
// `menus`
menus: []
}
},
computed: {
...mapState({
//
// 使 `mapState` Vuex `state.permission.addRouters` `mainMenu` 便
mainMenu: state => state.permission.addRouters
}),
contentPaddingLeft () {
// `fixSidebar``isMobile()``contentPaddingLeft`
if (!this.fixSidebar || this.isMobile()) {
return '0'
return '0';
}
// `sidebarOpened` `true` `256px`
if (this.sidebarOpened) {
return '256px'
return '256px';
}
return '80px'
return '80px';
}
},
watch: {
sidebarOpened (val) {
this.collapsed = !val
// `sidebarOpened` `collapsed` `sidebarOpened` `true` `collapsed` `false`
this.collapsed =!val;
}
},
created () {
this.menus = this.mainMenu.find(item => item.path === '/').children
this.collapsed = !this.sidebarOpened
// `mainMenu` Vuex `'/'` `children` `menus`
this.menus = this.mainMenu.find(item => item.path === '/').children;
// `sidebarOpened` `collapsed`
this.collapsed =!this.sidebarOpened;
},
mounted () {
const userAgent = navigator.userAgent
const userAgent = navigator.userAgent;
// `userAgent` `Edge` Edge Edge
if (userAgent.indexOf('Edge') > -1) {
this.$nextTick(() => {
this.collapsed = !this.collapsed
// `collapsed`
this.collapsed =!this.collapsed;
setTimeout(() => {
this.collapsed = !this.collapsed
}, 16)
})
// 16 `collapsed` Edge `collapsed`
this.collapsed =!this.collapsed;
}, 16);
});
}
},
methods: {
...mapActions(['setSidebar']),
toggle () {
this.collapsed = !this.collapsed
this.setSidebar(!this.collapsed)
triggerWindowResizeEvent()
// `toggle` / `collapsed` `setSidebar` `mapActions` Vuex action `collapsed` Vuex `triggerWindowResizeEvent`便
this.collapsed =!this.collapsed;
this.setSidebar(!this.collapsed);
triggerWindowResizeEvent();
},
paddingCalc () {
let left = ''
let left = '';
// `sidebarOpened``isMobile()``fixSidebar` `256px``80px` `0`
if (this.sidebarOpened) {
left = this.isDesktop() ? '256px' : '80px'
left = this.isDesktop()? '256px' : '80px';
} else {
left = (this.isMobile() && '0') || ((this.fixSidebar && '80px') || '0')
left = (this.isMobile() && '0') || ((this.fixSidebar && '80px') || '0');
}
return left
return left;
},
menuSelect () {
// `menuSelect` `!isDesktop()``collapsed` `false`便
if (!this.isDesktop()) {
this.collapsed = false
this.collapsed = false;
}
},
drawerClose () {
this.collapsed = false
// `drawerClose` `collapsed` `false`便
this.collapsed = false;
}
}
}
</script>
<style lang="less">
// '../components/global.less'
@import url('../components/global.less');
/*
@ -165,17 +199,20 @@ export default {
* these styles.
*/
// `page-transition` enter `0`
.page-transition-enter {
opacity: 0;
}
// `page-transition` leaveactive `0` `page-transition-enter`
.page-transition-leave-active {
opacity: 0;
}
.page-transition-enter .page-transition-container,
.page-transition-leave-active .page-transition-container {
// `page-transition` enterleaveactive `.page-transition-container` `transform` `1.1`
.page-transition-enter.page-transition-container,
.page-transition-leave-active.page-transition-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>
</style>

@ -1,11 +1,13 @@
<template>
<!-- 最外层使用一个 `<div>` 元素作为容器这是一种常见的做法用于包裹内部的其他元素方便对整体内容进行布局和样式控制同时避免一些样式冲突等问题 -->
<div>
<!-- 使用 `<router-view>` 组件这是 Vue Router 中的核心组件它用于根据当前的路由配置来动态渲染相应的组件内容例如当用户访问不同的路由路径时该组件所在的位置就会显示对应路由所配置的组件实现页面的动态切换和展示不同的功能页面这里它占据了整个 `<div>` 容器的空间用于展示路由对应的页面内容 -->
<router-view />
</div>
</template>
<script>
// 使 ES6
// `name` `'BlankLayout'` Vue 便
export default {
name: 'BlankLayout'
}

@ -1,12 +1,20 @@
<template>
<div :style="!$route.meta.hiddenHeaderContent ? 'margin: -24px -24px 0px;' : null">
<!-- pageHeader , route meta :true on hide -->
<!-- 根据路由元数据`$route.meta` `hiddenHeaderContent` 属性的值来动态设置外层 `<div>` 的内联样式
如果 `hiddenHeaderContent` `false`则设置顶部和左右边距为 `-24px`可能是为了让内部的 `page-header` 组件能覆盖部分外层容器的默认边距实现特定的布局效果若为 `true`则不设置任何样式`null` -->
<div :style="!$route.meta.hiddenHeaderContent? 'margin: -24px -24px 0px;' : null">
<!-- pageHeader, route meta :true on hide -->
<!-- 当路由元数据中的 `hiddenHeaderContent` 属性为 `false` 即不隐藏头部内容显示 `page-header` 组件
该组件接收 `title`页面标题`logo`可能是页面 logo 图标`avatar`可能是用户头像相关等属性来展示相应的头部信息以下是其内部插槽及各部分功能的详细说明 -->
<page-header v-if="!$route.meta.hiddenHeaderContent" :title="pageTitle" :logo="logo" :avatar="avatar">
<!-- 名为 `action` 的插槽用于在 `page-header` 组件的特定位置插入自定义的操作按钮等元素具体插入的内容由使用该组件的父组件通过 `<slot>` 传递进来 -->
<slot slot="action" name="action"></slot>
<!-- 名为 `content` 的插槽用于在 `page-header` 组件中插入自定义的头部内容部分同样具体内容由父组件决定若父组件没有传递对应内容通过 `this.$slots.headerContent` 判断且存在 `description` 数据时会显示下方默认的内容结构 -->
<slot slot="content" name="headerContent"></slot>
<div slot="content" v-if="!this.$slots.headerContent && description">
<!-- 显示一段描述信息`description`设置字体大小为 `14px`颜色为半透明黑色用于在头部提供一些辅助说明文字 -->
<p style="font-size: 14px;color: rgba(0,0,0,.65)">{{ description }}</p>
<div class="link">
<!-- 使用 `v-for` 指令循环遍历 `linkList` 数组用于生成一组链接元素每个链接包含一个图标通过 `a-icon` 组件展示图标类型由 `link.icon` 指定和对应的文字标题`link.title`点击链接可跳转到 `link.href` 指定的地址方便在头部展示相关的导航链接等信息 -->
<template v-for="(link, index) in linkList">
<a :key="index" :href="link.href">
<a-icon :type="link.icon" />
@ -15,12 +23,14 @@
</template>
</div>
</div>
<!-- 名为 `extra` 的插槽用于在 `page-header` 组件中插入额外的元素默认情况下包含一个用于展示图片`extraImage`的结构 `extraImage` 有值则会显示对应的图片图片会根据样式设置进行布局展示 -->
<slot slot="extra" name="extra">
<div class="extra-img">
<img v-if="typeof extraImage !== 'undefined'" :src="extraImage"/>
<img v-if="typeof extraImage!== 'undefined'" :src="extraImage"/>
</div>
</slot>
<div slot="pageMenu">
<!-- `search` 属性为 `true` 显示搜索框组件`a-input-search`用于在页面头部提供搜索功能设置了宽度占位符大小以及回车键触发的按钮文本等样式和属性方便用户输入内容进行搜索操作 -->
<div class="page-menu-search" v-if="search">
<a-input-search
style="width: 80%; max-width: 522px;"
@ -29,6 +39,7 @@
enterButton="搜索"
/>
</div>
<!-- `tabs` 属性存在且其 `items` 属性有值时显示选项卡组件`a-tabs`用于在页面头部展示多个可切换的选项卡通过 `v-for` 循环生成每个选项卡面板`a-tab-pane`每个面板的标题由 `item.title` 指定同时绑定了选项卡切换的回调函数`@change` 事件触发 `tabs.callback`以及设置活动选项卡的 `activeKey`方便用户切换不同的页面视图等 -->
<div class="page-menu-tabs" v-if="tabs && tabs.items">
<!-- @change="callback" :activeKey="activeKey" -->
<a-tabs :tabBarStyle="{margin: 0}" :activeKey="tabs.active()" @change="tabs.callback">
@ -39,6 +50,7 @@
</page-header>
<div class="content">
<div class="page-header-index-wide">
<!-- 使用插槽`<slot>`来插入内容若启用了多标签页功能`multiTab` `true`通过 Vuex 状态获取则使用 `<keep-alive>` 组件包裹 `<router-view>`这样在切换路由时对应的页面组件会被缓存下次访问能更快地展示 `multiTab` `false`则直接使用 `<router-view>`用于根据路由配置动态展示相应的页面内容`ref="content"` 用于在组件内部通过 `this.$refs.content` 来获取该元素的引用方便后续操作 -->
<slot>
<!-- keep-alive -->
<keep-alive v-if="multiTab">
@ -50,25 +62,30 @@
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import PageHeader from '../components/PageHeader'
// 'vuex' `mapState` Vuex 便使`multiTab`
import { mapState } from 'vuex';
// `PageHeader` 使
import PageHeader from '../components/PageHeader';
export default {
name: 'PageView',
// `components` `PageHeader` 使使 `<page-header>` Vue 使
components: {
PageHeader
},
props: {
// `avatar` `null` `avatar` `null`
avatar: {
type: String,
default: null
},
// `title` `true` `false`
title: {
type: [String, Boolean],
default: true
},
// `logo` logo `null` `avatar` logo logo
logo: {
type: String,
default: null
@ -76,40 +93,52 @@ export default {
},
data () {
return {
// `pageTitle` `null` `title` `page-header`
pageTitle: null,
// `description` `null` `headerContent`
description: null,
// `linkList` `href``icon``title`
linkList: [],
// `extraImage` `extra`
extraImage: '',
// `search` `false` `true`
search: false,
// `tabs` `items`
tabs: {}
}
},
computed: {
...mapState({
// 使 `mapState` Vuex `state.app.multiTab` `multiTab` 便`<router-view>`使 `<keep-alive>`
multiTab: state => state.app.multiTab
})
},
mounted () {
this.getPageMeta()
// `getPageMeta`
this.getPageMeta();
},
updated () {
this.getPageMeta()
// `getPageMeta`
this.getPageMeta();
},
methods: {
getPageMeta () {
// eslint-disable-next-line
this.pageTitle = (typeof(this.title) === 'string' || !this.title) ? this.title : this.$route.meta.title
// `pageTitle` `title` `title` `false`使 `title` `pageTitle``this.$route.meta` `title`
this.pageTitle = (typeof(this.title) === 'string' ||!this.title)? this.title : this.$route.meta.title;
const content = this.$refs.content
const content = this.$refs.content;
if (content) {
if (content.pageMeta) {
Object.assign(this, content.pageMeta)
// `this.$refs.content` `pageMeta` 使 `Object.assign` `this`
Object.assign(this, content.pageMeta);
} else {
this.description = content.description
this.linkList = content.linkList
this.extraImage = content.extraImage
this.search = content.search === true
this.tabs = content.tabs
// `pageMeta` `content` `description``linkList``extraImage``search``tabs`
this.description = content.description;
this.linkList = content.linkList;
this.extraImage = content.extraImage;
this.search = content.search === true;
this.tabs = content.tabs;
}
}
}
@ -118,49 +147,64 @@ export default {
</script>
<style lang="less" scoped>
.content {
margin: 24px 24px 0;
.link {
margin-top: 16px;
&:not(:empty) {
margin-bottom: 16px;
.content {
// `content` `24px` `0`使
margin: 24px 24px 0;
.link {
// `link` `16px`使
margin-top: 16px;
&:not(:empty) {
// `link` `16px`使
margin-bottom: 16px;
}
a {
// `<a>` `32px`
margin-right: 32px;
// `24px` `24px`
height: 24px;
line-height: 24px;
// 使便
display: inline-block;
i {
// `<i>` `a-icon` `24px`使 `8px``vertical-align: middle`
font-size: 24px;
margin-right: 8px;
vertical-align: middle;
}
a {
margin-right: 32px;
span {
// `<span>` `24px`便
height: 24px;
line-height: 24px;
display: inline-block;
i {
font-size: 24px;
margin-right: 8px;
vertical-align: middle;
}
span {
height: 24px;
line-height: 24px;
display: inline-block;
vertical-align: middle;
}
vertical-align: middle;
}
}
}
.page-menu-search {
text-align: center;
margin-bottom: 16px;
}
.page-menu-tabs {
margin-top: 48px;
}
}
.page-menu-search {
// `page-menu-search` 使
text-align: center;
// `16px`
margin-bottom: 16px;
}
.page-menu-tabs {
// `page-menu-tabs` `48px`使
margin-top: 48px;
}
.extra-img {
margin-top: -60px;
text-align: center;
width: 195px;
.extra-img {
// `extra-img` `-60px`
margin-top: -60px;
//
text-align: center;
// `195px`
width: 195px;
img {
width: 100%;
}
img {
// `<img>` `100%`使 `extra-img`
width: 100%;
}
}
.mobile {
.extra-img{

@ -1,32 +1,47 @@
<script>
// 使使 ES6
export default {
name: 'RouteView',
// `props` `keepAlive`
props: {
// `keepAlive` `Boolean` `true` `false`
// `true` `keepAlive` 使 `true`
keepAlive: {
type: Boolean,
default: true
}
},
data () {
// `data`
return {}
},
render () {
// `this` `$route` `meta` `$store` `getters` 便使 Vuex `getters`
// `$route` `meta` `$store` Vuex `getters`
const { $route: { meta }, $store: { getters } } = this
// `<keep-alive>` `<router-view>` DOM `<keep-alive>` Vue 使
const inKeep = (
<keep-alive>
<router-view />
</keep-alive>
)
// `<keep-alive>` `<router-view>` DOM
const notKeep = (
<router-view />
)
// multiTab multiTab
//
// return meta.keepAlive ? inKeep : notKeep
// 使 `<keep-alive>` `inKeep``notKeep`
// `multiTab` `getters` `multiTab` `multiTab` `multiTab` `true`
// 使 `return meta.keepAlive? inKeep : notKeep` `keepAlive`
if (!getters.multiTab && meta.keepAlive === false) {
return notKeep
}
return this.keepAlive || getters.multiTab || meta.keepAlive ? inKeep : notKeep
// `<keep-alive>` `inKeep``notKeep`
// 1. `props` `keepAlive` `true`
// 2. `getters` `multiTab` `true`
// 3. `meta` `keepAlive` `true`
return this.keepAlive || getters.multiTab || meta.keepAlive? inKeep : notKeep
}
}
</script>
</script>

@ -1,26 +1,36 @@
<template>
<!-- 使用 `<div>` 元素作为整个布局的根容器并设置 `id` `userLayout`同时通过动态绑定类名`:class`添加 `user-layout-wrapper` 类以及根据 `device` 变量其值应该会根据不同设备进行变化比如区分移动端桌面端等来动态添加相应的设备相关类名用于后续样式的适配实现不同设备下的特定布局效果 -->
<div id="userLayout" :class="['user-layout-wrapper', device]">
<!-- 使用 `<div>` 元素创建一个 `container` 类名的容器用于包裹页面的主要内容部分包括头部主体内容通过 `<route-view>` 展示不同路由对应的页面内容以及底部等方便对整体内容进行统一的布局和样式控制 -->
<div class="container">
<!-- 创建一个 `top` 类名的 `<div>` 元素用于放置页面的顶部相关内容通常可以包含如页面标题 logo 以及一些简短的描述信息等使其在视觉上处于页面的上方位置符合常见的页面布局结构 -->
<div class="top">
<!-- 使用 `<div>` 元素创建 `header` 类名的部分用于展示页面头部的具体元素例如 logo 和标题等是整个页面头部区域的核心部分可呈现出品牌标识和页面主要名称等关键信息 -->
<div class="header">
<!-- 使用 `<a>` 标签创建一个链接指向根路径`/`当用户点击时会跳转到对应的页面内部包含一个 `<img>` 标签用于展示 logo 图片图片路径为相对路径 `../assets/logo.svg`并设置 `class` `logo` `alt` 属性用于图片描述在图片无法正常显示时显示 `logo` 文字提示以及一个 `<span>` 标签用于展示页面标题文字 `Online Exam`整体构成了页面头部的主要视觉元素和品牌标识展示 -->
<a href="/">
<img src="../assets/logo.svg" class="logo" alt="logo">
<span class="title">Online Exam</span>
</a>
</div>
<!-- 使用 `<div>` 元素创建 `desc` 类名的部分用于展示一段描述性文字这里明确写出了该在线考试系统是基于 `SpringBoot + Vue` 实现的用于向用户简单介绍系统的技术实现背景等信息增强页面的可读性和对系统的初步了解 -->
<div class="desc">
基于SpringBoot+Vue实现的在线考试系统
</div>
</div>
<!-- 使用 `<route-view>` 组件这是 Vue Router 中的核心组件它会根据当前的路由配置动态渲染相应的组件内容在这里它占据了页面主体部分的空间用于展示与用户相关的不同功能页面比如登录注册等页面具体取决于路由配置实现页面的动态切换和功能展示 -->
<route-view></route-view>
<!-- 使用 `<div>` 元素创建 `footer` 类名的部分用于展示页面底部的相关内容通常包含如版权信息友情链接联系信息等是整个页面底部区域的核心部分用于提供一些辅助性通用性的页面信息展示 -->
<div class="footer">
<!-- 使用 `<div>` 元素创建 `links` 类名的容器用于放置一组链接元素这些链接分别指向不同的外部资源如代码仓库关于作者的页面以及可以联系作者的邮箱链接等方便用户获取更多相关信息查看代码或者与作者进行沟通交流 -->
<div class="links">
<a href="https://github.com/19920625lsg/spring-boot-online-exam" target="_blank">代码仓</a>
<a href="https://19920625lsg.github.io" target="_blank">关于我</a>
<a href="mailto:liangshanguang2@gmail.com">联系我</a>
</div>
<!-- 使用 `<div>` 元素创建 `copyright` 类名的部分用于展示版权声明信息明确指出版权所有者以及版权年份等信息告知用户该页面内容的版权归属情况符合法律规范和常规的页面版权展示要求 -->
<div class="copyright">
Copyright &copy; 2020 Liang Shan Guang
</div>
@ -28,99 +38,117 @@
</div>
</div>
</template>
<script>
import RouteView from './RouteView'
import { mixinDevice } from '../utils/mixin'
// `./RouteView` `RouteView` `<route-view>` 使使
import RouteView from './RouteView';
// `../utils/mixin` `mixinDevice`Vue `mixin` `mixinDevice` `UserLayout` 使
import { mixinDevice } from '../utils/mixin';
export default {
name: 'UserLayout',
// `components` `RouteView` 使使 `<route-view>` Vue 使便
components: { RouteView },
// `mixinDevice` 使
mixins: [mixinDevice],
data () {
// `data`
return {}
},
mounted () {
document.body.classList.add('userLayout')
// `mounted` `document.body` `classList` `userLayout` `body` `.userLayout` 使
document.body.classList.add('userLayout');
},
beforeDestroy () {
document.body.classList.remove('userLayout')
// `beforeDestroy` `document.body` `userLayout` 使
document.body.classList.remove('userLayout');
}
}
</script>
<style lang="less" scoped>
#userLayout.user-layout-wrapper {
height: 100%;
&.mobile {
.container {
.main {
max-width: 368px;
width: 98%;
}
}
}
// `id` `userLayout` `user-layout-wrapper` `100%`使
#userLayout.user-layout-wrapper {
height: 100%;
// `mobile` JavaScript `.container` `.main` `368px` `98%`使
&.mobile {
.container {
width: 100%;
min-height: 100%;
background: #f0f2f5 url(../assets/background.svg) no-repeat 50%;
background-size: 100%;
padding: 110px 0 144px;
position: relative;
a {
text-decoration: none;
.main {
max-width: 368px;
width: 98%;
}
}
}
.top {
text-align: center;
.header {
height: 44px;
line-height: 44px;
.badge {
position: absolute;
display: inline-block;
line-height: 1;
vertical-align: middle;
margin-left: -12px;
margin-top: -10px;
opacity: 0.8;
}
.container {
// `100%`使 `100%`
width: 100%;
min-height: 100%;
// `#f0f2f5` `../assets/background.svg`使`no-repeat``50%` `100%`
background: #f0f2f5 url(../assets/background.svg) no-repeat 50%;
background-size: 100%;
// `110px` `144px`使
padding: 110px 0 144px;
// `relative` `absolute` 便
a {
// `<a>`线使线
text-decoration: none;
}
.logo {
height: 44px;
vertical-align: top;
margin-right: 16px;
border-style: none;
}
.top {
// `center`使 `top` logo
.header {
// `44px` `44px` `header` 便
.badge {
// `absolute`使 `.header` `.top` `relative` 便
// `display: inline-block`使便
// `1`使`vertical-align: middle``margin-left: -12px``margin-top: -10px`使 `0.8`使
position: absolute;
display: inline-block;
line-height: 1;
vertical-align: middle;
margin-left: -12px;
margin-top: -10px;
opacity: 0.8;
}
.title {
font-size: 33px;
color: rgba(0, 0, 0, .85);
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-weight: 600;
position: relative;
top: 2px;
}
.logo {
// `logo` `44px`使 `header` `top` `logo` `16px` `none` `logo`
height: 44px;
vertical-align: top;
margin-right: 16px;
border-style: none;
}
.desc {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
margin-top: 12px;
margin-bottom: 40px;
.title {
// `33px`使`rgba(0, 0, 0,.85)`使`font-family`使 `600`使 `2px``position: relative``top: 2px`使
font-size: 33px;
color: rgba(0, 0, 0,.85);
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-weight: 600;
position: relative;
top: 2px;
}
}
.main {
min-width: 260px;
width: 368px;
margin: 0 auto;
.desc {
// `14px`使便`rgba(0, 0, 0, 0.45)` `12px`使 `40px`使
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
margin-top: 12px;
margin-bottom: 40px;
}
}
.main {
// `260px` `368px``margin: 0 auto`使`.container`
min-width: 260px;
width: 368px;
margin: 0 auto;
}
.footer {
position: absolute;

@ -1,7 +1,12 @@
import UserLayout from './UserLayout'
import BlankLayout from './BlankLayout'
import BasicLayout from './BasicLayout'
import RouteView from './RouteView'
import PageView from './PageView'
// 从当前目录下相对路径为 './UserLayout' 的文件中导入 `UserLayout` 组件。从命名推测,`UserLayout` 组件可能是用于构建与用户相关页面(比如登录、注册、个人信息等页面)的布局组件,它有着特定的页面结构和样式呈现方式,方便在这些用户相关功能模块中统一页面布局风格。
import UserLayout from './UserLayout';
// 从相对路径 './BlankLayout' 的文件导入 `BlankLayout` 组件。`BlankLayout` 大概率是一种较为简洁、空白的布局组件,可能用于那些不需要过多页面装饰元素,只专注展示核心内容的页面场景,例如一些特定的功能演示页面或者临时占位页面等,提供了一个极简的页面布局框架。
import BlankLayout from './BlankLayout';
// 从 './BasicLayout' 文件导入 `BasicLayout` 组件。`BasicLayout` 通常作为整个应用的基础布局组件存在,它往往包含了如头部、侧边栏、主体内容区等常见的页面布局结构,为大多数页面提供一个通用的、标准的布局框架,其他页面可以基于这个框架进行具体功能模块的内容填充和样式定制。
import BasicLayout from './BasicLayout';
// 从 './RouteView' 文件导入 `RouteView` 组件。`RouteView` 组件很可能是起到衔接路由与具体页面视图之间关系的作用,它能够根据路由的切换动态地加载并展示相应的子组件内容,是实现页面根据不同路由路径呈现不同视图的关键组件之一,便于构建多页面应用的路由驱动的页面展示逻辑。
import RouteView from './RouteView';
// 从 './PageView' 文件导入 `PageView` 组件。`PageView` 组件或许是针对具体页面内容展示的一种布局包装组件,用于规范页面内容在整体布局中的呈现形式,保证不同页面在内容展示方面具有一致性的布局风格,比如统一的页面边距、内容对齐方式等,同时也方便对页面内容进行整体的样式控制和调整。
export { UserLayout, BasicLayout, BlankLayout, RouteView, PageView }
// 使用 `export` 关键字将导入的这些组件(`UserLayout`、`BasicLayout`、`BlankLayout`、`RouteView`、`PageView`)进行导出,使得其他模块可以通过导入这个模块来获取这些组件,进而在整个项目中进行复用,用于构建不同功能需求的页面布局和视图展示,满足多样化的业务场景和页面设计要求。
export { UserLayout, BasicLayout, BlankLayout, RouteView, PageView };

@ -1,16 +1,34 @@
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import 'bootstrap-table/dist/bootstrap-table.min.css'
import '@fortawesome/fontawesome-free/css/all.min.css'
import Vue from 'vue'
import 'bootstrap'
import 'tableexport.jquery.plugin/libs/FileSaver/FileSaver.min.js'
import 'tableexport.jquery.plugin/tableExport.min.js'
import 'bootstrap-table/dist/bootstrap-table'
import BootstrapTable from 'bootstrap-table/dist/bootstrap-table-vue.esm'
import 'bootstrap-table/dist/extensions/export/bootstrap-table-export'
import 'bootstrap-table/dist/extensions/toolbar/bootstrap-table-toolbar.min'
import jQuery from 'jquery'
window.jQuery = jQuery
window.$ = jQuery
Vue.component('BootstrapTable', BootstrapTable)
// 导入 Bootstrap 框架的最小化样式文件CSS这个文件包含了 Bootstrap 提供的各种基础样式类,用于构建页面的基本布局、排版以及常见的 UI 组件(如按钮、表单、导航栏等)样式,使得页面能够呈现出符合 Bootstrap 风格的外观,后续在项目中可以直接使用这些预定义的样式类来快速搭建页面。
import 'bootstrap/dist/css/bootstrap.min.css';
// 导入 Bootstrap-Vue 组件库对应的样式文件CSSBootstrap-Vue 是基于 Vue 和 Bootstrap 进行整合的库,这个样式文件用于确保使用 Bootstrap-Vue 组件时能正确显示相应的样式,使组件的外观符合预期,与 Bootstrap 风格相匹配,为基于 Vue 构建的页面提供了丰富且美观的 UI 组件样式支持。
import 'bootstrap-vue/dist/bootstrap-vue.css';
// 导入 Bootstrap-Table 组件对应的最小化样式文件CSSBootstrap-Table 用于在页面中以 Bootstrap 风格展示表格数据,该样式文件定义了表格及其相关元素(表头、单元格、分页等)的样式,方便在项目中创建出美观且功能实用的表格样式,增强数据展示的可读性和交互性。
import 'bootstrap-table/dist/bootstrap-table.min.css';
// 导入 Font Awesome 图标库的全部最小化样式文件CSSFont Awesome 提供了大量的矢量图标,通过这个文件引入后,就可以在页面中使用相应的图标类(如 `fas fa-user` 表示用户图标等)来展示各种图标,丰富页面的可视化元素,提升用户界面的交互性和美观度。
import '@fortawesome/fontawesome-free/css/all.min.css';
// 导入 Vue 框架的核心库,它是整个 Vue 项目构建的基础,提供了创建组件、管理数据、响应式系统等核心功能,后续所有基于 Vue 的插件、组件及功能拓展都依赖于此基础库来运行和交互。
import Vue from 'vue';
// 导入 Bootstrap 框架的 JavaScript 相关代码,这使得项目能够使用 Bootstrap 提供的各种 JavaScript 功能,比如一些组件的交互效果(如模态框的弹出、下拉菜单的展开等),将其整合进整个项目中,实现更丰富的前端交互功能。
import 'bootstrap';
// 导入 FileSaver.min.js 文件,它属于 tableexport.jquery.plugin 库的一部分,通常用于在浏览器端实现将数据保存为文件的功能,例如将表格数据导出为 Excel 等文件格式时,会借助这个库来处理文件保存相关的操作,方便用户进行数据的本地保存。
import 'tableexport.jquery.plugin/libs/FileSaver/FileSaver.min.js';
// 导入 tableExport.min.js 文件,同样来自 tableexport.jquery.plugin 库,它可能是用于实现表格数据导出功能的核心文件,提供了具体的导出逻辑和接口,与 FileSaver.min.js 等配合,能够将页面中的表格数据按照一定格式(如 CSV、Excel 等)导出到本地,满足用户获取数据副本的需求。
import 'tableexport.jquery.plugin/tableExport.min.js';
// 导入 Bootstrap-Table 组件的 JavaScript 相关代码,用于在项目中实例化和使用 Bootstrap-Table 组件,使其能够在页面上展示表格数据,并具备如排序、筛选、分页等丰富的表格交互功能,方便对大量数据进行展示和操作。
import 'bootstrap-table/dist/bootstrap-table';
// 导入 Bootstrap-Table 组件的 Vue 版本(以 ESM 模块格式),并将其重命名为 BootstrapTable这样在 Vue 项目中就可以像使用普通 Vue 组件一样使用它,通过在组件模板中添加对应的标签来展示表格,利用其提供的各种属性、事件和插槽等功能来定制表格的展示和交互行为,方便在 Vue 环境下更便捷地操作和展示表格数据。
import BootstrapTable from 'bootstrap-table/dist/bootstrap-table-vue.esm';
// 导入 Bootstrap-Table 的导出扩展功能相关代码,用于为 Bootstrap-Table 组件添加数据导出的功能,比如可以通过表格上的导出按钮将表格数据导出为指定格式的文件,增强了表格组件的实用性,满足用户对于数据导出的常见需求。
import 'bootstrap-table/dist/extensions/export/bootstrap-table-export';
// 导入 Bootstrap-Table 的工具栏扩展功能的最小化代码文件,该扩展可能为表格添加工具栏相关的功能,例如在表格上方或下方显示一些操作按钮(如刷新、添加行、删除行等,具体取决于扩展的实现),方便用户对表格进行更多的操作和管理,提升用户与表格交互的便捷性。
import 'bootstrap-table/dist/extensions/toolbar/bootstrap-table-toolbar.min';
// 导入 jQuery 库jQuery 是一个广泛使用的 JavaScript 库,提供了简洁的 API 用于操作 DOM、处理事件、实现动画效果等很多前端插件和库如 Bootstrap 等)都依赖它来实现一些功能,在这里导入后将其挂载到全局的 `window` 对象上,方便在项目中全局使用 jQuery 的功能,并且确保其他依赖 jQuery 的库能够正常工作。
import jQuery from 'jquery';
// 将导入的 jQuery 库挂载到全局的 `window` 对象的 `jQuery` 属性上,使得在整个项目的任何地方(包括其他 JavaScript 文件、HTML 页面中的内联脚本等)都可以通过 `window.jQuery` 来访问和使用 jQuery 库,遵循了一些基于 jQuery 的插件和库对 jQuery 引用的常规做法,保证兼容性和正常调用。
window.jQuery = jQuery;
// 同时,也将 jQuery 库挂载到全局的 `window` 对象的 `$` 属性上,这是一种常见的用法,因为很多 jQuery 相关的代码习惯使用 `$` 符号来调用 jQuery 的方法和功能(如 `$(document).ready()` 等),这样在项目中可以方便地使用这种简洁的写法来操作 jQuery提高开发效率。
window.$ = jQuery;
// 使用 Vue.component 方法将导入的 BootstrapTable即 Bootstrap-Table 的 Vue 组件版本)注册为全局 Vue 组件,注册后在整个 Vue 项目的任何组件中都可以直接使用 `<BootstrapTable>` 标签来创建表格,无需在每个使用的组件内再次导入,方便复用和统一管理表格组件的使用,提高代码的可维护性和开发效率。
Vue.component('BootstrapTable', BootstrapTable);

@ -1,7 +1,18 @@
// 引入summernote参考https://blog.csdn.net/qq_24734285/article/details/80246093
import 'bootstrap/dist/js/bootstrap.bundle.min'
import 'bootstrap/dist/css/bootstrap.css'
import 'font-awesome/css/font-awesome.css'
import 'summernote'
import 'summernote/dist/lang/summernote-zh-CN'
import 'summernote/dist/summernote.css'
// 引入bootstrap的JavaScript压缩包bundle.min版本bootstrap.bundle.min文件包含了Bootstrap框架的所有核心JavaScript功能以及其依赖的Popper.js用于实现一些弹出框、下拉菜单等组件的定位功能
// 导入这个文件后就能在项目中使用Bootstrap提供的各种交互性组件和功能比如模态框的弹出、导航栏的响应式切换、下拉菜单的展示与交互等为页面添加丰富的交互效果和动态功能。
import 'bootstrap/dist/js/bootstrap.bundle.min';
// 引入Bootstrap框架的CSS样式文件这个文件定义了Bootstrap的各种UI组件如按钮、表单、网格系统、卡片等的默认样式通过引入它页面中的元素可以应用这些预定义样式类来快速呈现出Bootstrap风格的外观方便进行页面布局搭建和UI设计确保组件具有统一且美观的样式表现。
import 'bootstrap/dist/css/bootstrap.css';
// 引入Font Awesome图标库的CSS样式文件Font Awesome提供了大量的矢量图标通过引入该样式文件就可以在HTML元素上使用相应的图标类例如<i class="fa fa-user"></i>表示用户图标)来展示各种图标,丰富页面的可视化元素,提升用户界面的交互性和可读性,使页面能更直观地传达信息。
import 'font-awesome/css/font-awesome.css';
// 引入Summernote编辑器库Summernote是一个基于JavaScript的富文本编辑器它可以让用户在网页上像使用Word一样编辑文本内容支持文本的格式化如加粗、斜体、下划线等、插入图片、列表、链接等丰富的功能通过导入这个库后续可以在项目中实例化并使用Summernote编辑器为用户提供便捷的富文本编辑体验。
import 'summernote';
// 引入Summernote的中文语言包summernote-zh-CN这使得Summernote编辑器能够以中文显示各种提示信息、菜单选项等内容提升用户在使用编辑器时的本地化体验方便中文用户更直观地理解和操作编辑器的各项功能。
import 'summernote/dist/lang/summernote-zh-CN';
// 引入Summernote编辑器的CSS样式文件该样式文件定义了Summernote编辑器在页面中呈现的外观样式比如文本编辑区域的大小、颜色、按钮的样式等确保编辑器在页面上能以符合其设计的样式展示出来并且与页面整体风格相协调给用户良好的视觉感受和编辑操作界面。
import 'summernote/dist/summernote.css';

@ -1,12 +1,21 @@
import Vue from 'vue'
import Router from 'vue-router'
import { constantRouterMap } from '../config/router.config'
// 导入Vue框架的核心库它是整个Vue项目构建的基础后续很多功能的实现如创建组件、使用插件、构建页面等都依赖于Vue提供的各种API和机制是必不可少的核心依赖项。
import Vue from 'vue';
// 导入Vue Router库Vue Router是Vue.js官方的路由管理器用于在单页面应用SPA中实现页面之间的路由切换通过定义不同的路由路径以及对应的组件能够让用户在访问不同的URL时加载相应的页面内容实现页面导航功能。
import Router from 'vue-router';
// 从 '../config/router.config' 文件中导入 'constantRouterMap'从命名推测它应该是一个包含了常量路由信息的数组或者对象里面定义了应用的一些基础的、不会动态变化的路由配置例如首页路由、登录路由等固定的路由规则每个路由配置通常包含路径path、对应的组件component以及其他一些如路由元数据meta等相关信息。
import { constantRouterMap } from '../config/router.config';
Vue.use(Router)
// 使用Vue.use()方法将Vue Router插件注册到Vue实例中这样在整个Vue项目里就能使用Vue Router提供的路由相关功能了例如在组件中使用 <router-view> 展示路由对应的组件,使用 <router-link> 来创建路由链接等这是启用Vue Router功能的必要步骤。
Vue.use(Router);
// 导出一个新创建的Vue Router实例作为整个项目的路由配置对象其他模块可以导入这个路由配置来挂载到Vue根实例上从而让整个应用具备路由功能。
export default new Router({
// 设置路由的模式为 'history'这种模式下URL看起来更像传统的多页面应用的URL没有 '#' 符号它使用浏览器的历史记录API来实现路由切换使得页面的URL更加美观、易读且符合搜索引擎优化SEO的要求但需要服务器端进行相应的配置来支持避免出现刷新页面找不到资源等问题。
mode: 'history',
// 设置路由的基础路径它的值通常来自于项目的构建环境配置process.env.BASE_URL例如在项目部署到特定的子路径下时这个基础路径就会发挥作用确保路由能够正确地匹配到对应的资源和页面保证路由功能的正常运行。
base: process.env.BASE_URL,
// 定义滚动行为函数,这里返回的是一个对象 { y: 0 }意味着当路由切换时页面在垂直方向y轴上的滚动位置会被重置为0也就是每次切换路由后页面都会滚动到顶部当然可以根据实际需求修改这个函数来实现更复杂的滚动行为控制比如根据不同路由保存或恢复滚动位置等。
scrollBehavior: () => ({ y: 0 }),
// 将前面导入的 'constantRouterMap' 作为路由配置的 routes 属性值也就是把预定义好的那些常量路由信息添加到路由配置中让Vue Router知道有哪些路由路径以及对应的组件等信息以便在用户访问不同URL时能够正确地匹配并加载相应的页面组件。
routes: constantRouterMap
})
})

@ -1,15 +1,42 @@
// 定义一个名为 getters 的对象,在 Vuex 中getters 用于从 store 的状态state中派生数据
// 类似于计算属性,它可以根据已有的 state 数据进行加工处理后返回新的数据供组件获取使用。
const getters = {
// 定义名为 'device' 的 getter 函数,它接收 state 参数(代表整个应用的状态对象),
// 然后返回 state.app.device 的值,也就是从全局状态中的 'app' 模块下获取 'device' 属性的值,
// 这个值可能用于表示当前应用运行所在的设备类型等相关信息,方便组件获取使用。
device: state => state.app.device,
// 定义名为 'theme' 的 getter 函数,作用是从全局状态中的 'app' 模块下获取 'theme' 属性的值,
// 该值可能对应着应用当前所使用的主题相关设置,组件可以通过这个 getter 来获取主题信息。
theme: state => state.app.theme,
// 定义名为 'color' 的 getter 函数,它会返回 state.app.color 的值,从 'app' 模块下获取与应用颜色相关的设置值,
// 例如可能是用于设置界面主体颜色之类的,组件可以据此来展示相应颜色风格的界面。
color: state => state.app.color,
// 定义名为 'token' 的 getter 函数,通过接收 state 参数,返回 state.user.token 的值,
// 这里是从全局状态中的 'user' 模块下获取用户登录的令牌token信息组件可以利用这个 getter 来判断用户是否登录等情况。
token: state => state.user.token,
// 定义名为 'avatar' 的 getter 函数,用于获取 state.user.avatar 的值,即从 'user' 模块下拿到用户头像相关信息,
// 方便组件在需要展示用户头像的地方使用该值进行头像显示。
avatar: state => state.user.avatar,
// 定义名为 'nickname' 的 getter 函数,它返回 state.user.name 的值,也就是从 'user' 模块下获取用户的姓名(可能作为昵称使用),
// 供组件用于显示用户称呼等相关用途。
nickname: state => state.user.name,
// 定义名为 'welcome' 的 getter 函数,从 state.user.welcome 中获取值,这个值可能是针对用户的欢迎语之类的相关内容,
// 组件可以获取该值来展示相应的欢迎信息给用户。
welcome: state => state.user.welcome,
// 定义名为 'roles' 的 getter 函数,其作用是返回 state.user.roles 的值,从 'user' 模块下拿到用户所拥有的角色列表信息,
// 组件在需要根据用户角色进行权限判断或者展示不同角色对应的功能等场景时可以使用该值。
roles: state => state.user.roles,
// 定义名为 'userInfo' 的 getter 函数,会返回 state.user.info 的值,从 'user' 模块下获取完整的用户信息对象,
// 包含了各种详细的用户相关数据,组件若需要全面了解用户情况时可通过这个 getter 获取。
userInfo: state => state.user.info,
// 定义名为 'addRouters' 的 getter 函数,它从 state.permission.addRouters 中获取值,
// 这里是从全局状态中的 'permission' 模块下获取动态添加的路由信息,组件可以利用这个值来进行路由相关的操作或者展示等。
addRouters: state => state.permission.addRouters,
// 定义名为 'multiTab' 的 getter 函数,返回 state.app.multiTab 的值,从 'app' 模块下获取与多标签功能相关的设置信息,
// 组件可以根据该值来判断是否启用多标签等相关功能操作。
multiTab: state => state.app.multiTab
}
export default getters
// 将定义好的 getters 对象导出,使得在其他模块(比如 Vue 组件等)中可以导入并使用这些 getters
// 方便获取 Vuex 存储中经过加工处理后的各种派生数据。
export default getters

@ -1,27 +1,40 @@
// 从 'vue' 库中导入 Vue 构造函数,它是整个 Vue.js 框架的核心,后续很多功能的实现都依赖于它。
import Vue from 'vue'
// 从 'vuex' 库中导入 VuexVuex 是 Vue.js 应用的状态管理模式和库,用于集中管理应用的各种状态数据、处理状态变更以及相关的异步操作等。
import Vuex from 'vuex'
// 从 './modules/app' 文件中导入名为 'app' 的模块这个模块应该是定义了与应用本身相关的一些状态、变更mutations、动作actions等内容用于管理应用层面的状态逻辑。
import app from './modules/app'
// 从 './modules/user' 文件中导入名为 'user' 的模块,该模块大概率是围绕用户相关的状态数据(如用户信息、登录状态等)以及对应的状态操作逻辑来进行定义的,方便在整个应用中统一管理用户相关状态。
import user from './modules/user'
// 从 './modules/permission' 文件中导入名为 'permission' 的模块,通常会包含权限相关的状态定义、权限验证与变更等操作的逻辑,用于处理应用中的权限管理相关事宜。
import permission from './modules/permission'
// 从 './getters' 文件中导入名为 'getters' 的对象,在 Vuex 中getters 用于定义从状态state中派生出来的数据获取方法就像计算属性一样方便组件获取经过处理后的状态数据。
import getters from './getters'
Vue.use(Vuex)
// 创建一个新的 Vuex.Store 实例,这是整个应用的状态管理中心,它接收一个配置对象来定义相关的状态管理细节。
export default new Vuex.Store({
// modules 属性用于将各个子模块整合到这个根状态管理实例中,这里将之前导入的 'app'、'user'、'permission' 模块添加进来,
// 每个子模块都可以有自己独立的 state、mutations、actions 和 getters方便对不同功能模块的状态进行分类管理避免所有状态逻辑都堆积在根模块中。
modules: {
app,
user,
permission
},
// state 属性用于定义整个应用的根状态数据,这里暂时为空,不过也可以根据实际需求在这里定义一些全局通用的基础状态值,
// 但更常见的是将具体的状态分散到各个子模块中,如上述导入的 'app'、'user'、'permission' 模块里去定义各自相关的状态。
state: {
},
// mutations 属性用于定义直接修改状态的同步方法集合,在 Vuex 中,只有通过 mutations 才能修改状态,以此保证状态变更的可追踪性,
// 这里暂时为空,实际应用中会根据具体业务逻辑来定义各种不同的 mutations 方法用于更新相应的状态数据。
mutations: {
},
// actions 属性用于定义可以包含异步操作并且通过提交 mutations 来间接修改状态的方法集合,常用于处理诸如 API 调用等异步业务逻辑,
// 这里也暂时为空,后续会根据具体需求添加相应的 actions 方法来触发对应的 mutations 完成状态变更。
actions: {
},
// 将之前导入的 getters 对象添加到这里,使得在整个应用中组件可以通过 Vuex 实例访问到这些定义好的派生数据获取方法,
// 从而方便地获取经过处理后的各种状态数据。
getters
})
})

@ -1,4 +1,7 @@
// 从 'vue' 库中导入 Vue 构造函数,它是 Vue.js 框架的核心,后续很多功能会依赖它来实现
import Vue from 'vue'
// 从 '../../store/mutation-types' 文件中解构导入多个常量,这些常量通常用于标识 Vuex 中 mutations 的类型
// 例如用于区分不同状态变更操作的名称,方便在代码中统一管理和调用相关的状态修改逻辑
import {
SIDEBAR_TYPE,
DEFAULT_THEME,
@ -14,109 +17,181 @@ import {
const app = {
state: {
// 表示侧边栏是否显示,初始值为 true意味着默认侧边栏是显示状态
sidebar: true,
// 用于标识当前应用运行所在的设备类型,初始值为 'desktop',表示默认认为是桌面设备
device: 'desktop',
// 应用的主题相关状态,初始为空字符串,后续可通过相应操作来设置具体的主题值
theme: '',
// 应用的布局模式相关状态,初始为空字符串,可根据业务需求设置不同的布局模式
layout: '',
// 应用内容宽度相关状态,初始为空字符串,用来确定内容区域的宽度设置情况
contentWidth: '',
// 表示头部是否固定,初始值为 false即默认头部不是固定状态
fixedHeader: false,
// 表示侧边栏是否固定,初始值为 false意味着默认侧边栏不是固定的
fixSiderbar: false,
// 表示头部是否自动隐藏,初始值为 false即默认头部不会自动隐藏
autoHideHeader: false,
// 应用的颜色相关设置,初始值为 null后续会根据操作来设置具体颜色值
color: null,
// 可能与颜色弱化(比如淡色模式等)相关的状态标识,初始值为 false
weak: false,
// 用于标识是否启用多标签功能,初始值为 true默认启用多标签
multiTab: true
},
mutations: {
// 用于设置侧边栏的显示类型,接收当前状态对象 state 和要设置的类型值 type
SET_SIDEBAR_TYPE: (state, type) => {
// 将 state 中的 sidebar 属性更新为传入的 type 值,改变侧边栏的显示状态
state.sidebar = type
// 使用 Vue.ls.set 方法(可能是自定义的本地存储操作)将 SIDEBAR_TYPE 对应的存储值设为 type
// 这样可以将侧边栏类型的设置持久化或者在其他地方能获取到该设置值
Vue.ls.set(SIDEBAR_TYPE, type)
},
// 用于关闭侧边栏的方法,该方法操作 state 来改变侧边栏状态
CLOSE_SIDEBAR: (state) => {
// 先通过 Vue.ls.set 将 SIDEBAR_TYPE 对应的存储值设为 true具体含义由业务逻辑决定
Vue.ls.set(SIDEBAR_TYPE, true)
// 然后将 state 中的 sidebar 属性设置为 false实现关闭侧边栏的效果
state.sidebar = false
},
// 用于切换设备类型的方法,接收要切换到的设备值 device更新 state 中的 device 属性来反映设备类型变化
TOGGLE_DEVICE: (state, device) => {
state.device = device
},
// 用于切换应用主题的方法,接收要设置的主题值 theme
TOGGLE_THEME: (state, theme) => {
// setStore('_DEFAULT_THEME', theme)
// 原本可能有个 setStore 方法用于存储主题相关设置(当前被注释掉了)
// 现在使用 Vue.ls.set 方法将 DEFAULT_THEME 对应的存储值设为 theme实现主题设置的持久化等操作
Vue.ls.set(DEFAULT_THEME, theme)
// 同时更新 state 中的 theme 属性,使应用内部的主题状态改变为传入的 theme 值
state.theme = theme
},
// 用于切换应用布局模式的方法,接收要设置的布局模式值 layout
TOGGLE_LAYOUT_MODE: (state, layout) => {
// 通过 Vue.ls.set 将 DEFAULT_LAYOUT_MODE 对应的存储值设为 layout可能用于后续获取布局模式设置等
Vue.ls.set(DEFAULT_LAYOUT_MODE, layout)
// 更新 state 中的 layout 属性,使应用的布局模式改变为传入的 layout 值
state.layout = layout
},
// 用于切换头部是否固定的方法,接收表示固定与否的布尔值 fixed
TOGGLE_FIXED_HEADER: (state, fixed) => {
// 使用 Vue.ls.set 将 DEFAULT_FIXED_HEADER 对应的存储值设为 fixed持久化头部固定状态设置
Vue.ls.set(DEFAULT_FIXED_HEADER, fixed)
// 更新 state 中的 fixedHeader 属性,改变头部固定的实际状态
state.fixedHeader = fixed
},
// 用于切换侧边栏是否固定的方法,接收表示固定与否的布尔值 fixed
TOGGLE_FIXED_SIDERBAR: (state, fixed) => {
// 通过 Vue.ls.set 将 DEFAULT_FIXED_SIDEMENU 对应的存储值设为 fixed用于存储侧边栏固定状态设置
Vue.ls.set(DEFAULT_FIXED_SIDEMENU, fixed)
// 更新 state 中的 fixSiderbar 属性,改变侧边栏固定的实际状态
state.fixSiderbar = fixed
},
// 用于切换头部是否自动隐藏的方法,接收表示隐藏与否的布尔值 show
TOGGLE_FIXED_HEADER_HIDDEN: (state, show) => {
// 使用 Vue.ls.set 将 DEFAULT_FIXED_HEADER_HIDDEN 对应的存储值设为 show持久化头部隐藏状态设置
Vue.ls.set(DEFAULT_FIXED_HEADER_HIDDEN, show)
// 更新 state 中的 autoHideHeader 属性,改变头部自动隐藏的实际状态
state.autoHideHeader = show
},
// 用于切换应用内容宽度类型的方法,接收要设置的内容宽度类型值 type
TOGGLE_CONTENT_WIDTH: (state, type) => {
// 通过 Vue.ls.set 将 DEFAULT_CONTENT_WIDTH_TYPE 对应的存储值设为 type方便后续获取该设置
Vue.ls.set(DEFAULT_CONTENT_WIDTH_TYPE, type)
// 更新 state 中的 contentWidth 属性,改变应用内容宽度的实际类型
state.contentWidth = type
},
// 用于切换应用颜色的方法,接收要设置的颜色值 color
TOGGLE_COLOR: (state, color) => {
// 使用 Vue.ls.set 将 DEFAULT_COLOR 对应的存储值设为 color持久化颜色设置
Vue.ls.set(DEFAULT_COLOR, color)
// 更新 state 中的 color 属性,改变应用的实际颜色设置
state.color = color
},
// 用于切换颜色弱化相关状态的方法,接收表示弱化与否的布尔值 flag
TOGGLE_WEAK: (state, flag) => {
// 通过 Vue.ls.set 将 DEFAULT_COLOR_WEAK 对应的存储值设为 flag持久化颜色弱化状态设置
Vue.ls.set(DEFAULT_COLOR_WEAK, flag)
// 更新 state 中的 weak 属性,改变颜色弱化的实际状态
state.weak = flag
},
// 用于切换多标签相关状态的方法,接收表示启用与否的布尔值 bool
TOGGLE_MULTI_TAB: (state, bool) => {
// 使用 Vue.ls.set 将 DEFAULT_MULTI_TAB 对应的存储值设为 bool持久化多标签设置
Vue.ls.set(DEFAULT_MULTI_TAB, bool)
// 更新 state 中的 multiTab 属性,改变多标签功能的实际启用状态
state.multiTab = bool
}
},
actions: {
// 名为 setSidebar 的 action 方法,用于触发 SET_SIDEBAR_TYPE 这个 mutation 来设置侧边栏类型
// 接收一个 type 参数,会将其传递给对应的 mutation 方法
setSidebar ({ commit }, type) {
commit('SET_SIDEBAR_TYPE', type)
},
// 名为 CloseSidebar 的 action 方法,用于触发 CLOSE_SIDEBAR 这个 mutation 来关闭侧边栏
CloseSidebar ({ commit }) {
commit('CLOSE_SIDEBAR')
},
// 名为 ToggleDevice 的 action 方法,用于触发 TOGGLE_DEVICE 这个 mutation 来切换设备类型
// 接收要切换到的设备值 device并传递给对应的 mutation 方法
ToggleDevice ({ commit }, device) {
commit('TOGGLE_DEVICE', device)
},
// 名为 ToggleTheme 的 action 方法,用于触发 TOGGLE_THEME 这个 mutation 来切换应用主题
// 接收要设置的主题值 theme并传递给对应的 mutation 方法
ToggleTheme ({ commit }, theme) {
commit('TOGGLE_THEME', theme)
},
// 名为 ToggleLayoutMode 的 action 方法,用于触发 TOGGLE_LAYOUT_MODE 这个 mutation 来切换应用布局模式
// 接收要设置的布局模式值 mode并传递给对应的 mutation 方法
ToggleLayoutMode ({ commit }, mode) {
commit('TOGGLE_LAYOUT_MODE', mode)
},
// 名为 ToggleFixedHeader 的 action 方法,用于切换头部是否固定的状态
// 接收表示固定与否的布尔值 fixedHeader先做一些额外逻辑判断如果不是固定头部则设置头部隐藏为 false
// 再触发 TOGGLE_FIXED_HEADER 这个 mutation 来更新头部固定相关状态
ToggleFixedHeader ({ commit }, fixedHeader) {
if (!fixedHeader) {
commit('TOGGLE_FIXED_HEADER_HIDDEN', false)
}
commit('TOGGLE_FIXED_HEADER', fixedHeader)
},
// 名为 ToggleFixSiderbar 的 action 方法,用于触发 TOGGLE_FIXED_SIDERBAR 这个 mutation 来切换侧边栏是否固定的状态
// 接收表示固定与否的布尔值 fixSiderbar并传递给对应的 mutation 方法
ToggleFixSiderbar ({ commit }, fixSiderbar) {
commit('TOGGLE_FIXED_SIDERBAR', fixSiderbar)
},
// 名为 ToggleFixedHeaderHidden 的 action 方法,用于触发 TOGGLE_FIXED_HEADER_HIDDEN 这个 mutation 来切换头部是否自动隐藏的状态
// 接收表示隐藏与否的布尔值 show并传递给对应的 mutation 方法
ToggleFixedHeaderHidden ({ commit }, show) {
commit('TOGGLE_FIXED_HEADER_HIDDEN', show)
},
// 名为 ToggleContentWidth 的 action 方法,用于触发 TOGGLE_CONTENT_WIDTH 这个 mutation 来切换应用内容宽度类型
// 接收要设置的内容宽度类型值 type并传递给对应的 mutation 方法
ToggleContentWidth ({ commit }, type) {
commit('TOGGLE_CONTENT_WIDTH', type)
},
// 名为 ToggleColor 的 action 方法,用于触发 TOGGLE_COLOR 这个 mutation 来切换应用颜色相关状态
// 接收要设置的颜色值 color并传递给对应的 mutation 方法
ToggleColor ({ commit }, color) {
commit('TOGGLE_COLOR', color)
},
// 名为 ToggleWeak 的 action 方法,用于触发 TOGGLE_WEAK 这个 mutation 来切换颜色弱化相关状态
// 接收表示弱化与否的布尔值 weakFlag并传递给对应的 mutation 方法
ToggleWeak ({ commit }, weakFlag) {
commit('TOGGLE_WEAK', weakFlag)
},
// 名为 ToggleMultiTab 的 action 方法,用于触发 TOGGLE_MULTI_TAB 这个 mutation 来切换多标签相关状态
// 接收表示启用与否的布尔值 bool并传递给对应的 mutation 方法
ToggleMultiTab ({ commit }, bool) {
commit('TOGGLE_MULTI_TAB', bool)
}
}
}
export default app
// 将定义好的 app 对象导出,使得在其他 JavaScript 文件中可以通过导入该模块来使用 app 中定义的 state、mutations 和 actions 等内容
// 进而构建 Vuex 相关的应用状态管理逻辑等
export default app

@ -1,76 +1,107 @@
// 从 '../../config/router.config' 文件中解构导入两个路由相关的配置,
// asyncRouterMap 可能是包含需要根据权限等条件动态加载的路由信息,
// constantRouterMap 可能是一些固定的、无需动态判断权限就始终存在的路由信息
import { asyncRouterMap, constantRouterMap } from '../../config/router.config'
/**
* 过滤账户是否拥有某一个权限并将菜单从加载列表移除
* 该函数用于判断给定的用户权限列表是否包含当前路由所要求的权限以此决定该路由是否应被加载显示
*
* @param permission
* @param route
* @returns {boolean}
* @param permission 用户所拥有的权限列表通常是一个数组每个元素代表一种权限
* @param route 当前要检查权限的路由对象包含了路由相关的各种元信息等
* @returns {boolean} 返回一个布尔值表示用户是否拥有该路由对应的权限拥有则返回 true否则返回 false
*/
function hasPermission (permission, route) {
// 检查路由对象是否有元信息meta并且元信息中是否定义了权限permission要求
if (route.meta && route.meta.permission) {
let flag = false
// 遍历用户的权限列表
for (let i = 0, len = permission.length; i < len; i++) {
// 检查当前路由要求的权限列表中是否包含用户所拥有的当前权限
flag = route.meta.permission.includes(permission[i])
// 如果包含,说明用户有访问该路由的权限,直接返回 true
if (flag) {
return true
}
}
// 遍历完用户权限列表后,如果都不包含当前路由要求的权限,则返回 false表示无权限访问该路由
return false
}
// 如果路由没有定义权限要求,默认返回 true即允许访问该路由
return true
}
/**
* 单账户多角色时使用该方法可过滤角色不存在的菜单
* 该函数用于判断给定角色是否在当前路由所要求的角色列表中以此决定该路由是否对该角色可见
* 注意这里虽然有 eslint-disable-next-line 注释可能是为了暂时忽略某些 ESLint 规则检查但代码规范方面可能需要后续优化
*
* @param roles
* @param route
* @returns {*}
* @param roles 角色相关的对象可能包含角色的唯一标识等信息这里使用了 roles.id 来进行判断推测是角色的标识属性
* @param route 当前要检查角色匹配的路由对象包含了路由相关的各种元信息等
* @returns {*} 返回一个布尔值表示当前角色是否符合该路由对角色的要求符合则返回 true否则返回 false若路由未定义角色要求则默认返回 true
*/
// eslint-disable-next-line
function hasRole(roles, route) {
// 检查路由对象是否有元信息meta并且元信息中是否定义了角色roles要求
if (route.meta && route.meta.roles) {
// 检查当前路由要求的角色列表中是否包含传入的角色标识roles.id返回相应的布尔值结果
return route.meta.roles.includes(roles.id)
} else {
// 如果路由没有定义角色要求,默认返回 true即允许该角色访问该路由
return true
}
}
function filterAsyncRouter (routerMap, roles) {
// 使用数组的 filter 方法遍历传入的路由列表routerMap对每个路由进行权限过滤操作
const accessedRouters = routerMap.filter(route => {
// 调用 hasPermission 函数检查当前路由是否有权限被访问,传入用户角色的权限列表和当前路由对象
if (hasPermission(roles.permissionList, route)) {
// 如果当前路由有子路由children并且子路由长度大于 0说明需要对子路由也进行权限过滤
if (route.children && route.children.length) {
// 递归调用 filterAsyncRouter 函数,传入子路由列表和用户角色,对子路由进行权限过滤并更新子路由列表
route.children = filterAsyncRouter(route.children, roles)
}
// 如果当前路由有权限访问,返回 true表示该路由应被包含在最终的可访问路由列表中
return true
}
// 如果当前路由没有权限访问,返回 false表示该路由应从可访问路由列表中移除
return false
})
// 返回经过权限过滤后的可访问路由列表
return accessedRouters
}
const permission = {
state: {
// 初始状态下routers 属性被设置为 constantRouterMap即那些固定的、无需动态判断权限就始终存在的路由信息
routers: constantRouterMap,
// 初始时 addRouters 为空数组,后续会用于添加根据权限动态筛选出来的路由信息
addRouters: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
// 将传入的根据权限筛选后的路由列表routers赋值给 state.addRouters更新动态添加的路由信息
state.addRouters = routers
// 将固定路由列表constantRouterMap和动态筛选后的路由列表合并后赋值给 state.routers更新总的路由信息
state.routers = constantRouterMap.concat(routers)
}
},
actions: {
GenerateRoutes ({ commit }, data) {
// 返回一个 Promise 对象,用于处理异步操作,比如等待路由筛选等操作完成
return new Promise(resolve => {
// 从传入的数据对象中解构出 roles 属性,它包含了用户角色相关信息,用于后续路由筛选依据
const { roles } = data
// 调用 filterAsyncRouter 函数,传入 asyncRouterMap动态路由列表和 roles用户角色信息进行路由权限筛选
const accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
// 调用 SET_ROUTERS 这个 mutation将筛选后的路由列表传递进去用于更新路由相关状态
commit('SET_ROUTERS', accessedRouters)
// 调用 resolve 函数,将 Promise 的状态置为已完成,表示路由生成操作结束
resolve()
})
}
}
}
export default permission
// 将定义好的 permission 对象导出,方便在其他模块中引入使用,以实现基于权限的路由管理相关逻辑,
// 例如在 Vuex 中结合使用来控制前端应用中不同用户角色能访问的路由情况等。
export default permission

@ -1,117 +1,150 @@
// 从 'vue' 库中导入 Vue 构造函数,后续可能会基于 Vue 相关的功能或插件来进行操作,比如使用 Vue.ls 进行本地存储相关操作
import Vue from 'vue'
// 从 '../../api/login' 文件中解构导入登录、获取用户信息、登出相关的 API 函数,这些函数应该是与后端进行交互来实现对应功能的接口调用
import { login, getInfo, logout } from '../../api/login'
// 从 '../../store/mutation-types' 文件中导入 ACCESS_TOKEN 常量,这个常量可能是用于在 Vuex 的存储操作中标识与用户登录令牌token相关的键名等用途
import { ACCESS_TOKEN } from '../../store/mutation-types'
// 从 '../../utils/util' 文件中导入 welcome 函数,该函数可能用于生成欢迎语之类的相关功能
import { welcome } from '../../utils/util'
const user = {
state: {
// 用于存储用户登录的令牌token初始为空字符串登录成功后会更新为实际获取到的 token 值
token: '',
// 存储用户的姓名,初始为空字符串,通过获取用户信息后进行设置
name: '',
// 可能用于存储欢迎语相关内容,初始为空字符串,同样在获取用户信息等操作后更新
welcome: '',
// 存储用户头像的相关信息(比如头像的 URL 等),初始为空,后续获取用户信息时设置
avatar: '',
// 用于存储用户所拥有的角色列表,初始为空数组,会根据获取到的用户角色信息进行填充
roles: [],
// 用于存储更详细的用户信息对象,初始为空对象,在获取用户信息接口调用后进行赋值更新
info: {}
},
mutations: {
// 用于设置用户登录令牌token的 mutation 方法,接收当前状态对象 state 和要设置的 token 值
SET_TOKEN: (state, token) => {
// 将 state 中的 token 属性更新为传入的 token 值,实现对用户令牌的状态更新
state.token = token
},
// 用于设置用户姓名和欢迎语的 mutation 方法接收包含姓名name和欢迎语welcome的对象
SET_NAME: (state, { name, welcome }) => {
// 更新 state 中的 name 属性为传入的姓名值
state.name = name
// 更新 state 中的 welcome 属性为传入的欢迎语值
state.welcome = welcome
},
// 用于设置用户头像相关信息的 mutation 方法接收要设置的头像信息avatar更新 state 中的 avatar 属性
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
// 用于设置用户角色列表的 mutation 方法接收要设置的角色列表roles更新 state 中的 roles 属性
SET_ROLES: (state, roles) => {
state.roles = roles
},
// 用于设置详细用户信息对象的 mutation 方法接收要设置的用户信息对象info更新 state 中的 info 属性
SET_INFO: (state, info) => {
state.info = info
}
},
actions: {
// 登录
// 名为 Login 的 action 方法,用于处理用户登录逻辑,接收 commit 函数(用于提交 mutation和用户登录信息userInfo
Login ({ commit }, userInfo) {
return new Promise((resolve, reject) => {
// 调用从外部导入的 login 函数(应该是与后端交互的登录接口调用),传入用户登录信息 userInfo
login(userInfo).then(response => {
// 检查登录接口返回的响应中 code 是否为 0表示登录是否成功根据业务定义的响应规范
if (response.code === 0) {
const token = response.data
// 把接口返回的token字段的值设置到localStorage的token键值对中token的有效期是1天,Vue.ls中的ls是localStorage的意思
// 使用 Vue.ls.set 方法(推测是基于 Vue 的本地存储封装ls 表示 localStorage将获取到的 token 值存储到本地存储中
// 并设置有效期为 1 天(通过传入时间毫秒数 24 * 60 * 60 * 1000 来实现),键名为 ACCESS_TOKEN之前导入的常量
Vue.ls.set(ACCESS_TOKEN, token, 24 * 60 * 60 * 1000)
// 设置token事件,修改全局变量state中的token值讲mutations中的SET_TOKEN事件
// 调用 commit 函数提交 SET_TOKEN 这个 mutation将获取到的 token 值更新到 state 中的 token 属性,实现状态更新
commit('SET_TOKEN', token)
// 将 Promise 状态置为成功,意味着登录流程顺利完成
resolve()
} else {
// 自定义错误
// 如果登录失败code 不为 0创建一个自定义的错误对象,提示用户名或密码错误,并将 Promise 状态置为失败,向外抛出错误
reject(new Error('用户名或密码错误'))
}
}).catch(error => {
// 如果在登录接口调用过程中出现其他错误(比如网络问题等),在控制台打印错误信息,并将 Promise 状态置为失败,向外抛出错误
console.log(error)
reject(error)
})
})
},
// 获取用户信息
// 名为 GetInfo 的 action 方法,用于获取用户信息,接收 commit 函数用于提交 mutation
GetInfo ({ commit }) {
return new Promise((resolve, reject) => {
// 调用从外部导入的 getInfo 函数(与后端交互的获取用户信息接口调用)
getInfo().then(response => {
console.log('/user/info的响应如下:')
console.log('/user/info 的响应如下:')
console.log(response)
const result = response.data // 取出响应体
const result = response.data // 取出响应体中的数据部分(通常包含用户详细信息等内容)
if (result.role && result.role.permissions.length > 0) { // 如果权限
// 检查返回的用户信息中 role 是否存在且 permissions 数组长度大于 0即判断是否有有效的权限相关信息
if (result.role && result.role.permissions.length > 0) {
const role = result.role
role.permissions = result.role.permissions // permissions是给页面行为设置权限
role.permissions = result.role.permissions // 将权限相关信息重新赋值给 role.permissions可能是确保数据结构正确之类的操作
// 对每个权限对象per进行遍历操作主要是处理 actionEntitySet 相关内容(可能是解析出具体的操作行为等)
role.permissions.map(per => {
if (per.actionEntitySet != null && per.actionEntitySet.length > 0) {
if (per.actionEntitySet!= null && per.actionEntitySet.length > 0) {
const action = per.actionEntitySet.map(action => {
return action.action
})
per.actionList = action
}
})
role.permissionList = role.permissions.map(permission => { // permissionList是从permissions中遍历解析得来的
// 从 permissions 数组中遍历解析出每个权限对象的 permissionId组成新的 permissionList 数组,用于后续权限相关判断等操作
role.permissionList = role.permissions.map(permission => {
return permission.permissionId
})
// 这些设置都在Vuex的getters里面了
commit('SET_ROLES', result.role) // 在store中设置用户的权限
commit('SET_INFO', result) // 在store中设置用户信息
// 提交 SET_ROLES 这个 mutation将处理后的角色相关信息result.role更新到 state 中的 roles 属性,在 store 中设置用户的权限
commit('SET_ROLES', result.role)
// 提交 SET_INFO 这个 mutation将完整的用户信息result更新到 state 中的 info 属性,在 store 中设置用户信息
commit('SET_INFO', result)
} else {
reject(new Error('getInfo: roles must be a non-null array !'))
// 如果没有有效的权限相关信息,创建一个自定义的错误对象,提示角色信息必须是非空数组,并将 Promise 状态置为失败,向外抛出错误
reject(new Error('getInfo: roles must be a non-null array!'))
}
// 这些设置都在Vuex的getters里面了
commit('SET_NAME', { name: result.name, welcome: welcome() }) // 设置用户名称
commit('SET_AVATAR', result.avatar) // 设置用户头像
// 提交 SET_NAME 这个 mutation设置用户名称从 result 中获取 name和欢迎语调用 welcome 函数生成)
commit('SET_NAME', { name: result.name, welcome: welcome() })
// 提交 SET_AVATAR 这个 mutation设置用户头像从 result 中获取 avatar
commit('SET_AVATAR', result.avatar)
// 将 Promise 状态置为成功,意味着获取用户信息流程顺利完成,并返回完整的响应对象
resolve(response)
}).catch(error => {
// 如果在获取用户信息接口调用过程中出现错误,将 Promise 状态置为失败,向外抛出错误
reject(error)
})
})
},
// 登出
// 名为 Logout 的 action 方法,用于处理用户登出逻辑,接收 commit 函数用于提交 mutation 和当前的 state 对象(包含用户相关状态)
Logout ({ commit, state }) {
return new Promise((resolve) => {
// 提交 SET_TOKEN 这个 mutation将用户令牌token清空即将 state 中的 token 属性设置为空字符串
commit('SET_TOKEN', '')
// 提交 SET_ROLES 这个 mutation将用户角色列表清空即将 state 中的 roles 属性设置为空数组
commit('SET_ROLES', [])
// 使用 Vue.ls.remove 方法(基于 Vue 的本地存储封装)移除本地存储中以 ACCESS_TOKEN 为键名存储的用户令牌信息
Vue.ls.remove(ACCESS_TOKEN)
// 调用从外部导入的 logout 函数与后端交互的登出接口调用传入当前用户的令牌state.token
logout(state.token).then(() => {
// 无论登出接口调用是否成功(此处简单处理,实际可能需要更严谨的错误处理),都将 Promise 状态置为成功,意味着登出流程完成
resolve()
}).catch(() => {
// 如果登出接口调用出现错误,同样将 Promise 状态置为成功,保证登出流程能继续推进(可能需要根据实际情况优化错误处理逻辑)
resolve()
})
})
}
}
}
// 将定义好的 user 对象导出,方便在其他模块(比如 Vuex 相关模块)中引入使用,以实现用户登录、获取信息、登出等相关的状态管理和业务逻辑操作。
export default user

@ -1,16 +1,64 @@
// 以下是一系列使用 `export` 关键字导出的常量声明,在 JavaScript 模块系统中,
// 导出的常量可以被其他模块导入使用,这样便于在整个项目中统一管理和复用这些特定的标识字符串。
// 定义一个名为 ACCESS_TOKEN 的常量,其值为 'Access-Token',通常用于表示用户访问令牌相关的键名或标识,
// 例如在本地存储localStorage或者与后端交互传递用户登录认证信息时作为区分访问令牌的特定字符串使用。
export const ACCESS_TOKEN = 'Access-Token'
// 定义常量 SIDEBAR_TYPE值为 'SIDEBAR_TYPE',可能用于标识侧边栏类型相关的设置或状态,
// 在应用中,当需要区分不同样式、显示模式的侧边栏时,可以通过这个常量作为统一的类型标识,
// 比如在 Vuex 的状态管理中作为键名来存储和获取侧边栏类型相关的设置信息。
export const SIDEBAR_TYPE = 'SIDEBAR_TYPE'
// 定义常量 DEFAULT_THEME值为 'DEFAULT_THEME',该常量大概率是用于表示应用默认主题相关的标识,
// 当进行主题切换、设置默认主题或者存储主题相关配置时,以此常量作为统一的名称来指代默认主题,
// 便于在代码中准确地找到与之对应的主题相关的设置逻辑或数据存储位置。
export const DEFAULT_THEME = 'DEFAULT_THEME'
// 定义常量 DEFAULT_LAYOUT_MODE值为 'DEFAULT_LAYOUT_MODE',一般用于标识应用默认布局模式的相关设置,
// 例如区分是流式布局、固定宽度布局等不同布局模式,在应用启动或者重置布局时,可依据这个常量来确定默认采用的布局方式,
// 并且在状态管理、配置存储等环节作为特定的键名来使用。
export const DEFAULT_LAYOUT_MODE = 'DEFAULT_LAYOUT_MODE'
// 定义常量 DEFAULT_COLOR值为 'DEFAULT_COLOR',通常代表应用默认颜色相关的标识,
// 像是设置界面主体颜色、按钮颜色等默认颜色配置时,使用这个常量来作为对应的标识,方便在整个项目中统一处理颜色相关的设置和获取操作。
export const DEFAULT_COLOR = 'DEFAULT_COLOR'
// 定义常量 DEFAULT_COLOR_WEAK值为 'DEFAULT_COLOR_WEAK',很可能与颜色弱化相关的设置有关,
// 比如实现淡色模式或者降低颜色对比度等功能时,以此常量作为区分和操作颜色弱化相关状态的标识,
// 在状态管理以及样式切换等代码逻辑中起到关键的标识作用。
export const DEFAULT_COLOR_WEAK = 'DEFAULT_COLOR_WEAK'
// 定义常量 DEFAULT_FIXED_HEADER值为 'DEFAULT_FIXED_HEADER',用于标识头部是否固定的默认设置相关情况,
// 在布局调整、页面交互涉及头部显示状态变化时,通过这个常量来指代头部固定相关的默认设置或者当前状态,
// 方便进行相应的逻辑判断和状态更新操作,例如在 Vuex 中作为键名来存储和修改头部固定状态信息。
export const DEFAULT_FIXED_HEADER = 'DEFAULT_FIXED_HEADER'
// 定义常量 DEFAULT_FIXED_SIDEMENU值为 'DEFAULT_FIXED_SIDEMENU',类似地,它用于标识侧边栏是否固定的默认设置相关事项,
// 当处理侧边栏的固定、浮动等显示状态变化以及相关配置存储时,以这个常量作为统一的标识来进行操作和判断,
// 确保整个项目中对侧边栏固定状态的处理具有一致性。
export const DEFAULT_FIXED_SIDEMENU = 'DEFAULT_FIXED_SIDEMENU'
// 定义常量 DEFAULT_FIXED_HEADER_HIDDEN值为 'DEFAULT_FIXED_HEADER_HIDDEN',主要用于表示头部是否自动隐藏的默认设置情况,
// 在页面交互过程中,涉及头部隐藏、显示逻辑以及相关状态管理时,通过这个常量来区分和操作头部隐藏相关的默认设置及状态信息,
// 比如根据不同页面滚动位置或者用户操作来决定头部是否隐藏时,以此作为判断依据和状态标识。
export const DEFAULT_FIXED_HEADER_HIDDEN = 'DEFAULT_FIXED_HEADER_HIDDEN'
// 定义常量 DEFAULT_CONTENT_WIDTH_TYPE值为 'DEFAULT_CONTENT_WIDTH_TYPE',通常是用于标识应用内容宽度类型的默认设置相关标识,
// 例如区分内容区域是采用流体宽度(随页面大小自适应变化)还是固定宽度等不同宽度类型设置,在页面布局以及响应式设计中,
// 依据这个常量来确定默认的内容宽度类型,并在状态管理、配置存储等环节使用它作为相应的键名来操作相关信息。
export const DEFAULT_CONTENT_WIDTH_TYPE = 'DEFAULT_CONTENT_WIDTH_TYPE'
// 定义常量 DEFAULT_MULTI_TAB值为 'DEFAULT_MULTI_TAB',大概率是用于表示多标签功能相关的默认设置标识,
// 比如在应用中判断是否默认启用多标签页面展示、切换多标签相关状态以及存储多标签配置信息时,通过这个常量来进行统一的指代和操作,
// 保证多标签功能相关的代码逻辑能够清晰且一致地进行处理。
export const DEFAULT_MULTI_TAB = 'DEFAULT_MULTI_TAB'
// 定义一个名为 CONTENT_WIDTH_TYPE 的常量对象,用于明确内容宽度类型的具体取值情况,
// 这里定义了两个属性:'Fluid' 和 'Fixed',分别代表流体宽度(自适应)和固定宽度这两种不同的内容宽度类型,
// 在应用中,当需要根据具体类型来设置或者判断内容宽度时,可以通过这个对象来获取对应的类型字符串,
// 使得代码中对内容宽度类型的使用更加清晰和规范,避免直接使用字符串字面量可能带来的错误和不易理解的问题。
export const CONTENT_WIDTH_TYPE = {
Fluid: 'Fluid',
Fixed: 'Fixed'
}
}

@ -1,46 +1,79 @@
// 定义一个名为 PERMISSION_ENUM 的常量对象,用于列举各种权限相关的枚举信息。
// 该对象的每个属性键(如 'add'、'delete' 等)对应一种权限操作,属性值又是一个包含 'key' 和 'label' 的对象,
// 'key' 与属性键相同,用于在代码逻辑中作为唯一标识来区分不同的权限操作,
// 'label' 则是对应的权限操作的中文描述,方便在界面显示或者给开发人员查看时更直观地理解该权限的含义。
const PERMISSION_ENUM = {
// 'add' 权限,代表新增操作,其对应的标识 'key' 为 'add',中文描述 'label' 为 '新增',在权限管理系统中可用于判断用户是否具有新增数据等相关权限。
'add': { key: 'add', label: '新增' },
// 'delete' 权限,对应删除操作,'key' 为 'delete''label' 为 '删除',用于检查用户是否有权限删除相应资源等场景。
'delete': { key: 'delete', label: '删除' },
// 'edit' 权限,表示修改操作,'key' 为 'edit''label' 为 '修改',可用于判断用户是否能够对数据等进行修改操作的权限验证。
'edit': { key: 'edit', label: '修改' },
// 'query' 权限,意味着查询操作,'key' 为 'query''label' 为 '查询',例如在数据查询功能的权限控制方面会用到这个权限标识。
'query': { key: 'query', label: '查询' },
// 'get' 权限,通常可理解为获取详情操作,'key' 为 'get''label' 为 '详情',用于判断用户是否有权查看具体数据详情等情况。
'get': { key: 'get', label: '详情' },
// 'enable' 权限,代表启用操作,'key' 为 'enable''label' 为 '启用',在涉及功能启用、数据启用等权限控制场景中使用。
'enable': { key: 'enable', label: '启用' },
// 'disable' 权限,对应禁用操作,'key' 为 'disable''label' 为 '禁用',用于验证用户是否具备禁用相关功能或数据的权限。
'disable': { key: 'disable', label: '禁用' },
// 'import' 权限,表示导入操作,'key' 为 'import''label' 为 '导入',比如在数据导入功能的权限管理中依靠这个权限标识来判断权限。
'import': { key: 'import', label: '导入' },
// 'export' 权限,意味着导出操作,'key' 为 'export''label' 为 '导出',用于控制用户是否有权限导出数据等相关操作的权限判断。
'export': { key: 'export', label: '导出' }
}
function plugin (Vue) {
// 检查插件是否已经被安装过,如果已经安装(通过判断 plugin.installed 属性是否为 true则直接返回避免重复安装。
if (plugin.installed) {
return
}
// 如果 Vue.prototype 上不存在 $auth 属性,就通过 Object.defineProperties 方法来为其定义该属性。
// 这里使用 defineProperties 可以更精细地控制属性的描述符(如可枚举性、可配置性、读写特性等)。
!Vue.prototype.$auth && Object.defineProperties(Vue.prototype, {
$auth: {
// 定义 $auth 属性的读取器getter函数当在 Vue 实例上访问 $auth 属性时,会执行这个函数来获取相应的值。
get () {
// 将当前的 Vue 实例保存到 _this 变量中,方便在后续的内部函数中访问实例上的其他属性和方法,
// 例如访问 Vuex 的 store 实例等(通过 _this.$store
const _this = this
// 返回一个函数,这个函数接收一个参数 permissions它应该是一个表示权限的字符串格式可能类似 'permission.action'。
return (permissions) => {
// 将传入的 permissions 参数按照 '.' 进行分割,得到一个包含两个元素的数组,
// 第一个元素 permission 表示权限的类型(如 'add'、'delete' 等),第二个元素 action 表示具体的操作行为(对应权限类型下的具体操作)。
const [permission, action] = permissions.split('.')
// 通过 Vue 实例的 $store.getters 访问 Vuex 中定义的 getters然后获取 roles.permissions
// 这里的 roles.permissions 应该是存储了用户所拥有的所有权限信息的列表(通常是一个数组,每个元素包含权限相关的详细信息)。
const permissionList = _this.$store.getters.roles.permissions
// 在权限列表中查找与传入的 permission 类型匹配的权限对象,通过比较 permissionId 属性来确定。
return permissionList.find((val) => {
return val.permissionId === permission
}).actionList.findIndex((val) => {
return val === action
}) > -1
})
// 在上一步找到的权限对象中,查找其 actionList应该是包含该权限下所有具体操作行为的列表中是否存在传入的 action 操作行为,
// 通过 findIndex 方法查找,如果找到则返回该操作行为在列表中的索引,若索引大于 -1表示找到了该操作行为即用户具有对应的权限返回 true否则返回 false。
.actionList.findIndex((val) => {
return val === action
}) > -1
}
}
}
})
// 类似地,如果 Vue.prototype 上不存在 $enum 属性,就通过 Object.defineProperties 方法为其定义该属性。
!Vue.prototype.$enum && Object.defineProperties(Vue.prototype, {
$enum: {
// 定义 $enum 属性的读取器getter函数当在 Vue 实例上访问 $enum 属性时,执行此函数来获取相应的值。
get () {
// const _this = this;
// 返回一个函数,这个函数接收一个参数 val用于根据传入的参数来查找 PERMISSION_ENUM 中对应的枚举值。
return (val) => {
// 先将 PERMISSION_ENUM 赋值给 result后续会根据传入的 val 参数逐步在这个对象中查找对应的子属性。
let result = PERMISSION_ENUM
// 如果传入的 val 参数存在,就按照 '.' 分割成数组,然后遍历每个元素 v在 result 对象中查找对应的子属性。
val && val.split('.').forEach(v => {
// 如果当前的 result 对象存在并且包含对应的子属性 v则更新 result 为该子属性的值,否则将 result 设置为 null表示未找到对应的枚举值。
result = result && result[v] || null
})
// 最后返回查找后的结果,即对应的权限枚举值(如果找到的话)或者 null如果未找到
return result
}
}
@ -48,4 +81,6 @@ function plugin (Vue) {
})
}
// 将定义好的 plugin 函数导出,方便在其他模块中引入使用,通常是在 Vue 应用的入口文件或者插件注册相关的代码中,
// 通过 Vue.use(plugin) 的方式来注册这个插件,使其为 Vue 实例添加 $auth 和 $enum 等便捷的属性和功能。
export default plugin

@ -1,20 +1,27 @@
<template>
<a-layout>
<!-- 页面布局的头部区域设置了样式颜色为白色 -->
<a-layout-header class="header" style="color: #fff">
<!-- v-if="examDetail.exam" 是为了防止 异步请求时页面渲染的时候还没有拿到这个值而报错 下面多处这个判断都是这个道-->
<!-- v-if="examDetail.exam" 用于防止异步请求时页面渲染还没拿到 examDetail.exam 值而报错以下多处类似判断同-->
<span style="font-size:25px;margin-left: 0px;" v-if="examDetail.exam">
<!-- 使用 a-avatar 组件展示考试相关头像通过管道过滤器 imgSrcFilter 处理图片地址同时展示考试名称和描述 -->
<a-avatar slot="avatar" size="large" shape="circle" :src="examDetail.exam.examAvatar | imgSrcFilter"/>
{{ examDetail.exam.examName }}
<span style="font-size:15px;">{{ examDetail.exam.examDescription }} </span>
</span>
<span style="float: right;">
<!-- 同样基于 examDetail.exam 存在的情况下展示考试限时信息此处为倒计时相关显示 -->
<span style="margin-right: 60px; font-size: 20px" v-if="examDetail.exam">{{ examDetail.exam.examTimeLimit }} </span>
<!-- 点击交卷按钮触发 finishExam 方法 -->
<a-button type="danger" ghost style="margin-right: 60px;" @click="finishExam()"></a-button>
<!-- 展示用户头像 -->
<a-avatar class="avatar" size="small" :src="avatar()"/>
<!-- 展示用户昵称 -->
<span style="margin-left: 12px">{{ nickname() }}</span>
</span>
</a-layout-header>
<a-layout>
<!-- 页面布局的侧边栏区域设置了宽度背景滚动等样式属性 -->
<a-layout-sider width="190" :style="{background: '#444',overflow: 'auto', height: '100vh', position: 'fixed', left: 0 }">
<a-menu
mode="inline"
@ -22,13 +29,18 @@
:defaultOpenKeys="['question_radio', 'question_check', 'question_judge']"
:style="{ height: '100%', borderRight: 0 }"
>
<!-- 单选题菜单部分 -->
<a-sub-menu key="question_radio">
<!-- 根据 examDetail.exam 存在与否展示菜单标题包含图标和每题分值信息 -->
<span slot="title" v-if="examDetail.exam"><a-icon type="check-circle" theme="twoTone"/>单选题(每题{{ examDetail.exam.examScoreRadio }})</span>
<!-- 循环渲染单选题菜单项点击菜单项触发 getQuestionDetail 方法 -->
<a-menu-item v-for="(item, index) in examDetail.radioIds" :key="item" @click="getQuestionDetail(item)">
<!-- 如果用户已经做过这道题通过 answersMap 判断则显示查看图标 -->
<a-icon type="eye" theme="twoTone" twoToneColor="#52c41a" v-if="answersMap.get(item)"/>
题目{{ index + 1 }}
</a-menu-item>
</a-sub-menu>
<!-- 多选题菜单部分逻辑与单选题类似 -->
<a-sub-menu key="question_check">
<span slot="title" v-if="examDetail.exam"><a-icon type="check-square" theme="twoTone"/>多选题(每题{{ examDetail.exam.examScoreCheck }})</span>
<a-menu-item v-for="(item, index) in examDetail.checkIds" :key="item" @click="getQuestionDetail(item)">
@ -36,6 +48,7 @@
题目{{ index + 1 }}
</a-menu-item>
</a-sub-menu>
<!-- 判断题菜单部分逻辑与单选题类似 -->
<a-sub-menu key="question_judge">
<span slot="title" v-if="examDetail.exam"><a-icon type="like" theme="twoTone"/>判断题(每题{{ examDetail.exam.examScoreJudge }})</span>
<a-menu-item v-for="(item, index) in examDetail.judgeIds" :key="item" @click="getQuestionDetail(item)">
@ -48,16 +61,18 @@
<a-layout :style="{ marginLeft: '200px' }">
<a-layout-content :style="{ margin: '24px 16px 0',height: '84vh', overflow: 'initial' }">
<div :style="{ padding: '24px', background: '#fff',height: '84vh'}">
<!-- 如果当前没有题目currentQuestion为空显示欢迎参加考试的提示语 -->
<span v-show="currentQuestion === ''" style="font-size: 30px;font-family: Consolas"></span>
<!-- 展示当前题目类型和题目名称使用 v-html 渲染可能包含 HTML 标签的题目名称 -->
<strong>{{ currentQuestion.type }} </strong> <p v-html="currentQuestion.name"></p>
<!-- 单选题和判断题 --> <!-- key不重复只需要在一个for循环中保证即可 -->
<!-- 单选题和判断题的选项组通过 v-if 判断题目类型进行显示绑定 change 事件 onRadioChange使用 v-model 双向绑定单选或判断题的选择值 -->
<a-radio-group @change="onRadioChange" v-model="radioValue" v-if="currentQuestion.type === '单选题' || currentQuestion.type === '判断题'">
<a-radio v-for="option in currentQuestion.options" :key="option.questionOptionId" :style="optionStyle" :value="option.questionOptionId">
{{ option.questionOptionContent }}
</a-radio>
</a-radio-group>
<!-- 多选题 -->
<!-- 多选题的选项组类似单选/判断题选项组逻辑绑定 change 事件 onCheckChange使用 v-model 双向绑定多选题的选择值 -->
<a-checkbox-group @change="onCheckChange" v-model="checkValues" v-if="currentQuestion.type === '多选题'">
<a-checkbox v-for="option in currentQuestion.options" :key="option.questionOptionId" :style="optionStyle" :value="option.questionOptionId">
{{ option.questionOptionContent }}
@ -72,10 +87,12 @@
</a-layout>
</a-layout>
</template>
<script>
// API
import { getExamDetail, getQuestionDetail, finishExam } from '../../api/exam'
//
import UserMenu from '../../components/tools/UserMenu'
// Vuex mapGetters Vuex
import { mapGetters } from 'vuex'
export default {
@ -85,15 +102,15 @@ export default {
},
data () {
return {
//
//
examDetail: {},
// id, currentQuestion(answersidids),
// Map id currentQuestion answers idids
answersMap: {},
//
//
currentQuestion: '',
// answersMap
// answersMap
radioValue: '',
// answersMap
// answersMap
checkValues: [],
optionStyle: {
display: 'block',
@ -104,16 +121,18 @@ export default {
}
},
mounted () {
// answersMap Map
this.answersMap = new Map()
const that = this
//
// getExamDetail
getExamDetail(this.$route.params.id)
.then(res => {
if (res.code === 0) {
//
// examDetail
that.examDetail = res.data
return res.data
} else {
//
this.$notification.error({
message: '获取考试详情失败',
description: res.msg
@ -122,31 +141,32 @@ export default {
})
},
methods: {
// ,
// 使 mapGetters Vuex
...mapGetters(['nickname', 'avatar']),
getQuestionDetail (questionId) {
// content
const that = this
//
//
this.radioValue = ''
this.checkValues = []
// getQuestionDetail id
getQuestionDetail(questionId)
.then(res => {
if (res.code === 0) {
//
// currentQuestion
that.currentQuestion = res.data
// answersMapid
// answersMap
if (that.answersMap.get(that.currentQuestion.id)) {
//
// /
if (that.currentQuestion.type === '单选题' || that.currentQuestion.type === '判断题') {
that.radioValue = that.answersMap.get(that.currentQuestion.id)[0]
} else if (that.currentQuestion.type === '多选题') {
//
// 使 Object.assign
Object.assign(that.checkValues, that.answersMap.get(that.currentQuestion.id))
}
}
return res.data
} else {
//
this.$notification.error({
message: '获取问题详情失败',
description: res.msg
@ -155,21 +175,21 @@ export default {
})
},
/**
* 单选题勾选是触发的变化事件
* @param e
* 单选题勾选时触发的变化事件处理函数
* @param e 事件对象包含选中的选项值等信息
*/
onRadioChange (e) {
const userOptions = []
userOptions.push(e.target.value)
//
// answersMap idid
this.answersMap.set(this.currentQuestion.id, userOptions)
},
/**
* 多选题触发的变化事件
* @param checkedValues
* 多选题触发的变化事件处理函数
* @param checkedValues 选中的选项值数组
*/
onCheckChange (checkedValues) {
//
// answersMap id
this.answersMap.set(this.currentQuestion.id, checkedValues)
},
_strMapToObj (strMap) {
@ -180,26 +200,27 @@ export default {
return obj
},
/**
*map转换为json
* Map 对象转换为 JSON 字符串的函数
*/
_mapToJson (map) {
return JSON.stringify(this._strMapToObj(map))
},
/**
* 结束考试并交卷
* 结束考试并交卷的函数
*/
finishExam () {
// Todo:answersMap
// Todo: answersMap
finishExam(this.$route.params.id, this._mapToJson(this.answersMap))
.then(res => {
if (res.code === 0) {
//
//
this.$notification.success({
message: '考卷提交成功!'
})
this.$router.push('/list/exam-record-list')
return res.data
} else {
//
this.$notification.error({
message: '交卷失败!',
description: res.msg

@ -1,22 +1,28 @@
<template>
<a-layout>
<!-- 页面布局的头部区域设置文字颜色为白色 -->
<a-layout-header class="header" style="color: #fff">
<!-- v-if="examDetail.exam" 是为了防止 异步请求时页面渲染的时候还没有拿到这个值而报错 下面多处这个判断都是这个道-->
<!-- v-if="examDetail.exam" 用于防止异步请求时页面渲染还没拿到 examDetail.exam 值而报错下面多处类似判断同-->
<span style="font-size:25px;margin-left: 0px;" v-if="examDetail.exam">
<!-- 使用 a-avatar 组件展示考试相关头像通过管道过滤器 imgSrcFilter 处理图片地址同时展示考试名称和描述 -->
<a-avatar slot="avatar" size="large" shape="circle" :src="examDetail.exam.examAvatar | imgSrcFilter"/>
{{ examDetail.exam.examName }}
<span style="font-size:15px;">{{ examDetail.exam.examDescription }} </span>
</span>
<span style="float: right;">
<!-- 根据 recordDetail.examRecord 存在与否展示考试得分和参加考试时间信息 -->
<span style="margin-right: 40px; font-size: 20px" v-if="recordDetail.examRecord">
考试得分<span style="color: red">{{ recordDetail.examRecord.examJoinScore }}</span>&nbsp;&nbsp;
<span style="font-size:15px;">参加考试时间{{ recordDetail.examRecord.examJoinDate }}</span>
</span>
<!-- 展示用户头像 -->
<a-avatar class="avatar" size="small" :src="avatar()"/>
<!-- 展示用户昵称 -->
<span style="margin-left: 12px">{{ nickname() }}</span>
</span>
</a-layout-header>
<a-layout>
<!-- 页面布局的侧边栏区域设置了宽度背景滚动等样式属性 -->
<a-layout-sider width="190" :style="{background: '#444',overflow: 'auto', height: '100vh', position: 'fixed', left: 0 }">
<a-menu
mode="inline"
@ -24,14 +30,18 @@
:defaultOpenKeys="['question_radio', 'question_check', 'question_judge']"
:style="{ height: '100%', borderRight: 0 }"
>
<!-- 单选题菜单部分 -->
<a-sub-menu key="question_radio">
<!-- 根据 examDetail.exam 存在与否展示菜单标题包含图标和每题分值信息 -->
<span slot="title" v-if="examDetail.exam"><a-icon type="check-circle" theme="twoTone"/>单选题(每题{{ examDetail.exam.examScoreRadio }})</span>
<!-- 循环渲染单选题菜单项点击菜单项触发 getQuestionDetail 方法根据 resultsMap 中对应题目结果显示正确或错误图标 -->
<a-menu-item v-for="(item, index) in examDetail.radioIds" :key="item" @click="getQuestionDetail(item)">
<a-icon type="check" v-if="resultsMap.get(item)==='True'"/>
<a-icon type="close" v-if="resultsMap.get(item)==='False'"/>
题目{{ index + 1 }}
</a-menu-item>
</a-sub-menu>
<!-- 多选题菜单部分逻辑与单选题类似 -->
<a-sub-menu key="question_check">
<span slot="title" v-if="examDetail.exam"><a-icon type="check-square" theme="twoTone"/>多选题(每题{{ examDetail.exam.examScoreCheck }})</span>
<a-menu-item v-for="(item, index) in examDetail.checkIds" :key="item" @click="getQuestionDetail(item)">
@ -40,6 +50,7 @@
题目{{ index + 1 }}
</a-menu-item>
</a-sub-menu>
<!-- 判断题菜单部分逻辑与单选题类似 -->
<a-sub-menu key="question_judge">
<span slot="title" v-if="examDetail.exam"><a-icon type="like" theme="twoTone"/>判断题(每题{{ examDetail.exam.examScoreJudge }})</span>
<a-menu-item v-for="(item, index) in examDetail.judgeIds" :key="item" @click="getQuestionDetail(item)">
@ -53,21 +64,24 @@
<a-layout :style="{ marginLeft: '200px' }">
<a-layout-content :style="{ margin: '24px 16px 0',height: '84vh', overflow: 'initial' }">
<div :style="{ padding: '24px', background: '#fff',height: '84vh'}">
<!-- 如果当前没有题目currentQuestion 为空显示欢迎查看考试情况及操作提示语 -->
<span v-if="currentQuestion === ''" style="font-size: 30px;font-family: Consolas"></span>
<span v-if="currentQuestion !== ''">
<span v-if="currentQuestion!== ''">
<!-- 展示当前题目类型和题目名称使用 v-html 渲染可能包含 HTML 标签的题目名称 -->
<strong>{{ currentQuestion.type }} </strong> <p v-html="currentQuestion.name"></p>
<!-- 根据 questionRight 计算属性判断当前题目用户是否答对显示相应提示信息 -->
<strong style="color: green;" v-if="questionRight"></strong>
<strong style="color: red;" v-if="!questionRight"></strong>
</span>
<br><br>
<!-- 单选题和判断题 --> <!-- key不重复只需要在一个for循环中保证即可 -->
<!-- 单选题和判断题的选项组通过 v-model 双向绑定选择值根据题目类型进行显示 -->
<a-radio-group v-model="radioValue" v-if="currentQuestion.type === '单选题' || currentQuestion.type === '判断题'">
<a-radio v-for="option in currentQuestion.options" :key="option.questionOptionId" :style="optionStyle" :value="option.questionOptionId">
{{ option.questionOptionContent }}
</a-radio>
</a-radio-group>
<!-- 题目出错的时候才显示这块 -->
<!-- 当题目答错且当前题目不为空且为单选题或判断题时显示正确答案的选项组 -->
<div v-if="!questionRight && currentQuestion!=='' && (currentQuestion.type === '单选题' || currentQuestion.type === '判断题')">
<span style="color: red;"><br/>正确答案是<br/></span>
<a-radio-group v-model="radioRightValue">
@ -77,14 +91,14 @@
</a-radio-group>
</div>
<!-- 多选题 -->
<!-- 多选题的选项组通过 v-model 双向绑定选择值根据题目类型进行显示 -->
<a-checkbox-group v-model="checkValues" v-if="currentQuestion.type === '多选题'">
<a-checkbox v-for="option in currentQuestion.options" :key="option.questionOptionId" :style="optionStyle" :value="option.questionOptionId">
{{ option.questionOptionContent }}
</a-checkbox>
</a-checkbox-group>
<!-- 题目出错的时候才显示这块 -->
<!-- 当题目答错且当前题目不为空且为多选题时显示正确答案的选项组 -->
<div v-if="!questionRight && currentQuestion!=='' && currentQuestion.type === '多选题'">
<span style="color: red;"><br/>正确答案是<br/></span>
<a-checkbox-group v-model="checkRightValues">
@ -105,10 +119,12 @@
</a-layout>
</a-layout>
</template>
<script>
// API
import { getExamDetail, getQuestionDetail, getExamRecordDetail } from '../../api/exam'
//
import UserMenu from '../../components/tools/UserMenu'
// Vuex mapGetters Vuex
import { mapGetters } from 'vuex'
export default {
@ -118,25 +134,25 @@ export default {
},
data () {
return {
//
//
examDetail: {},
//
//
recordDetail: {},
// id, currentQuestion(answersidids),
// Map id currentQuestion answers idids
answersMap: {},
//
// Map id
answersRightMap: {},
// ()
// Map 'True' 'False' id
resultsMap: {},
//
//
currentQuestion: '',
// answersMap
// answersMap
radioValue: '',
// answersRightMap
// answersRightMap
radioRightValue: '',
// answersMap
// answersMap
checkValues: [],
// answersRightMap
// answersRightMap
checkRightValues: [],
optionStyle: {
display: 'block',
@ -148,42 +164,44 @@ export default {
},
computed: {
/**
* 当前题目用户是否作答正确
* */
* 计算属性用于判断当前题目用户是否作答正确通过对比 resultsMap 中对应题目结果是否为 'True' 来确定
*/
questionRight () {
return this.resultsMap !== '' && this.resultsMap.get(this.currentQuestion.id) === 'True'
return this.resultsMap!== '' && this.resultsMap.get(this.currentQuestion.id) === 'True'
}
},
mounted () {
// answersMapanswersRightMapresultsMap Map
this.answersMap = new Map()
this.answersRightMap = new Map()
this.resultsMap = new Map()
const that = this
// ,
const that = this;
// getExamDetail
getExamDetail(this.$route.params.exam_id)
.then(res => {
if (res.code === 0) {
//
// examDetail
that.examDetail = res.data
return res.data
} else {
//
this.$notification.error({
message: '获取考试详情失败',
description: res.msg
})
}
})
//
// getExamRecordDetail
getExamRecordDetail(this.$route.params.record_id)
.then(res => {
if (res.code === 0) {
console.log(res.data)
//
that.recordDetail = res.data
//
that.objToMap()
return res.data
console.log(res.data);
// recordDetail objToMap
that.recordDetail = res.data;
that.objToMap();
return res.data;
} else {
//
this.$notification.error({
message: '获取考试记录详情失败',
description: res.msg
@ -192,51 +210,52 @@ export default {
})
},
methods: {
// ,
// 使 mapGetters Vuex
...mapGetters(['nickname', 'avatar']),
/**
* 把后端传过来的对象Object转换成Map
**/
* 方法用于将后端传过来的对象数据转换为 Map 结构分别处理 answersMapanswersRightMapresultsMap 的赋值
*/
objToMap () {
for (const item in this.recordDetail.answersMap) {
this.answersMap.set(item, this.recordDetail.answersMap[item])
this.answersMap.set(item, this.recordDetail.answersMap[item]);
}
for (const item in this.recordDetail.answersRightMap) {
this.answersRightMap.set(item, this.recordDetail.answersRightMap[item])
this.answersRightMap.set(item, this.recordDetail.answersRightMap[item]);
}
for (const item in this.recordDetail.resultsMap) {
this.resultsMap.set(item, this.recordDetail.resultsMap[item])
this.resultsMap.set(item, this.recordDetail.resultsMap[item]);
}
},
getQuestionDetail (questionId) {
// content
const that = this
//
this.radioValue = ''
this.radioRightValue = ''
this.checkValues = []
this.checkRightValues = []
const that = this;
//
this.radioValue = '';
this.radioRightValue = '';
this.checkValues = [];
this.checkRightValues = [];
// getQuestionDetail id
getQuestionDetail(questionId)
.then(res => {
if (res.code === 0) {
//
that.currentQuestion = res.data
// answersMapid
// currentQuestion
that.currentQuestion = res.data;
// answersMap
if (that.answersMap.get(that.currentQuestion.id)) {
//
// /
if (that.currentQuestion.type === '单选题' || that.currentQuestion.type === '判断题') {
that.radioValue = that.answersMap.get(that.currentQuestion.id)[0]
that.radioRightValue = that.answersRightMap.get(that.currentQuestion.id)[0]
that.radioValue = that.answersMap.get(that.currentQuestion.id)[0];
that.radioRightValue = that.answersRightMap.get(that.currentQuestion.id)[0];
} else if (that.currentQuestion.type === '多选题') {
//
Object.assign(that.checkValues, that.answersMap.get(that.currentQuestion.id))
Object.assign(that.checkRightValues, that.answersRightMap.get(that.currentQuestion.id))
// 使 Object.assign
Object.assign(that.checkValues, that.answersMap.get(that.currentQuestion.id));
Object.assign(that.checkRightValues, that.answersRightMap.get(that.currentQuestion.id));
}
}
return res.data
return res.data;
} else {
//
this.$notification.error({
message: '获取问题详情失败',
description: res.msg

@ -1,18 +1,27 @@
<template>
<div>
<!-- 使用 a-card 组件创建一个卡片式布局设置了上边距并且无边框标题为参加过的考试 -->
<a-card style="margin-top: 24px" :bordered="false" title="参加过的考试">
<!-- 在卡片的额外插槽通常用于放置一些操作按钮等元素中添加一个输入搜索框 -->
<div slot="extra">
<a-input-search style="margin-left: 16px; width: 272px;"/>
</div>
<!-- 使用 a-list 组件创建一个列表设置了较大的尺寸 -->
<a-list size="large">
<!-- 循环遍历 data 数组中的每个元素代表每条考试记录信息来生成列表项每个列表项对应一次考试记录 -->
<a-list-item :key="index" v-for="(item, index) in data">
<!-- 使用 a-list-item-meta 组件来展示列表项的元信息如头像标题描述等 -->
<a-list-item-meta :description="item.exam.examDescription">
<!-- 在头像插槽中使用 a-avatar 组件展示考试相关的头像通过管道过滤器 imgSrcFilter 处理图片地址设置了较大尺寸和方形形状 -->
<a-avatar slot="avatar" size="large" shape="square" :src="item.exam.examAvatar | imgSrcFilter"/>
<!-- 在标题插槽中展示考试名称 -->
<a slot="title">{{ item.exam.examName }}</a>
</a-list-item-meta>
<!-- 在操作插槽中添加一个查看考试详情的链接点击时会触发 viewExamRecordDetail 方法并传入对应的考试记录信息 -->
<div slot="actions">
<a @click="viewExamRecordDetail(item.examRecord)"></a>
</div>
<!-- 自定义的列表内容区域用于展示更多考试记录相关详细信息 -->
<div class="list-content">
<div class="list-content-item">
<span>Owner</span>
@ -29,53 +38,57 @@
</div>
</a-list-item>
</a-list>
</a-card>
</div>
</template>
<script>
// HeadInfo
import HeadInfo from '../../components/tools/HeadInfo'
// API
import { getExamRecordList } from '../../api/exam'
export default {
//
// 'ExamRecordList'
name: 'ExamRecordList',
components: {
HeadInfo
},
data () {
return {
// data
data: {}
}
},
methods: {
/**
* 根据考试记录的id拿到本次考试的详情并查看
* @param record 考试详情的记录
* 方法用于根据传入的考试记录信息跳转到对应的考试详情页面查看所有题目的详细情况
* @param record 考试详情的记录对象包含了如考试id考试记录id等关键信息用于构建跳转路径
*/
viewExamRecordDetail (record) {
//
// 使 $router.resolve idid
const routeUrl = this.$router.resolve({
path: `/exam/record/${record.examId}/${record.examRecordId}`
})
//
// window.open
window.open(routeUrl.href, '_blank')
}
},
mounted () {
//
// getExamRecordList
getExamRecordList().then(res => {
if (res.code === 0) {
// 0 data
this.data = res.data
} else {
// msg
this.$notification.error({
message: '获取考试记录失败',
description: res.msg
})
}
}).catch(err => {
//
//
this.$notification.error({
message: '获取考试记录失败',
description: err.message
@ -86,27 +99,41 @@ export default {
</script>
<style lang="less" scoped>
.ant-avatar-lg {
width: 48px;
height: 48px;
line-height: 48px;
}
/* 针对类名为 ant-avatar-lg 的元素设置样式,通常这个类可能是来自某个 UI 组件库(比如 Ant Design中定义的大尺寸头像相关的类 */
.ant-avatar-lg {
/* 设置元素的宽度为 48 像素,用于控制头像在页面上显示的横向尺寸大小 */
width: 48px;
/* 设置元素的高度为 48 像素,配合宽度一起确定头像的整体尺寸外观 */
height: 48px;
/* 设置元素内部文本的行高为 48 像素,行高与元素高度相等时,文本在垂直方向上能实现居中对齐效果(前提是文本的其他布局相关属性设置合适),常用于让头像中的文字(如果有)垂直居中显示 */
line-height: 48px;
}
.list-content-item {
color: rgba(0, 0, 0, .45);
display: inline-block;
vertical-align: middle;
font-size: 14px;
margin-left: 40px;
/* 针对类名为 list-content-item 的元素设置样式,从命名来看可能是用于自定义的列表内容项相关的样式类 */
.list-content-item {
/* 设置元素的文本颜色为 rgba 格式表示的颜色值,这里是带有 45% 透明度的黑色,使得文本颜色看起来相对淡一些,呈现出一种弱化显示的视觉效果,常用于次要信息的展示等场景 */
color: rgba(0, 0, 0,.45);
/* 将元素设置为行内块级元素显示方式,行内块级元素兼具了行内元素(可以和其他行内元素在同一行显示)和块级元素(可以设置宽、高、内外边距等盒模型属性)的特点,方便对列表内容项进行布局排版以及尺寸控制等操作 */
display: inline-block;
/* 设置元素在垂直方向上与同属一行的其他元素进行居中对齐,确保列表内容项在垂直方向上排列整齐,视觉上更加美观协调 */
vertical-align: middle;
/* 设置元素内文本的字体大小为 14 像素,确定文本的字号大小 */
font-size: 14px;
/* 设置元素的左边距为 40 像素,让列表内容项与左侧的其他元素或者其他列表内容项之间保持一定的间隔距离,增强视觉层次感和区分度 */
margin-left: 40px;
span {
line-height: 20px;
}
span {
/* 针对类名为 list-content-item 的元素内部的 <span> 标签元素设置行高为 20 像素,用于控制 <span> 标签内文本在垂直方向上的间距等显示效果,使其与父元素(.list-content-item中其他文本的显示风格有所区分或者更符合特定的布局需求 */
line-height: 20px;
}
p {
margin-top: 4px;
margin-bottom: 0;
line-height: 22px;
}
p {
/* 设置 <p> 段落元素的顶部外边距为 4 像素,使得段落与上方的元素之间产生一定的空白间隔,避免文本过于紧凑,提升可读性和视觉效果 */
margin-top: 4px;
/* 清除 <p> 段落元素底部的外边距,防止出现多余的空白间距,保证后续元素(如果有)能按照预期紧密排列或者与其他元素的间距符合设计要求 */
margin-bottom: 0;
/* 设置 <p> 段落元素内部文本的行高为 22 像素,用于控制段落文本在垂直方向上的排版效果,合适的行高有助于提高文本的阅读舒适度和整体美观度 */
line-height: 22px;
}
</style>
}
</style>

@ -1,29 +1,39 @@
<template>
<!-- 使用 a-card 组件创建一个卡片式布局设置无边框样式 -->
<a-card :bordered="false">
<!-- 创建一个具有 id "toolbar" div 容器用于放置操作按钮作为工具条区域 -->
<div id="toolbar">
<!-- 创建一个 "新建" 按钮按钮类型为 "primary"通常表示主要操作按钮的样式带有 "plus" 图标点击按钮时调用 $refs.createExamModal.create() 方法可能用于触发创建新考试相关的操作 -->
<a-button type="primary" icon="plus" @click="$refs.createExamModal.create()"></a-button>&nbsp;
<!-- 创建一个 "刷新" 按钮按钮类型为 "primary"带有 "reload" 图标点击按钮时调用 loadAll() 方法用于重新加载数据 -->
<a-button type="primary" icon="reload" @click="loadAll()"></a-button>
</div>
<!-- 使用 BootstrapTable 组件可能是基于 Bootstrap 样式的表格组件具体功能依赖其自身实现通过 ref 属性给组件实例添加引用标识方便后续在 JavaScript 代码中通过 this.$refs 进行访问同时传入表头数据和相关配置选项等属性 -->
<BootstrapTable
ref="table"
:columns="columns"
:data="tableData"
:options="options"
/>
<!-- ref是为了方便用this.$refs.modal直接引用下同 -->
<!-- 使用 step-by-step-exam-modal 组件自定义模态框组件用于逐步创建考试相关操作具体功能由该组件内部定义同样通过 ref 属性添加引用标识并且监听该组件的 @ok 事件当事件触发时调用 handleOk 方法 -->
<step-by-step-exam-modal ref="createExamModal" @ok="handleOk" />
<!-- 这里的详情需要传进去 -->
<!-- 使用 exam-edit-modal 组件自定义模态框组件用于编辑考试相关信息具体功能由组件内部定义通过 ref 属性添加引用标识监听 @ok 事件并在触发时调用 handleOk 方法这里编辑考试详情时可能需要传入相关数据进行处理 -->
<exam-edit-modal ref="editExamModal" @ok="handleOk" />
<!-- 更新考试封面图片 -->
<!-- 使用 update-avatar-modal 组件自定义模态框组件用于更新考试封面图片相关操作具体功能由组件内部定义通过 ref 属性添加引用标识监听 @ok 事件并在触发时调用 handleOk 方法 -->
<update-avatar-modal ref="updateAvatarModal" @ok="handleOk" />
</a-card>
</template>
<script>
// bootstrap-table
import '../../plugins/bootstrap-table'
// API
import { getExamAll } from '../../api/exam'
// StepByStepExamModal
import StepByStepExamModal from './modules/StepByStepExamModal'
// ExamEditModal
import ExamEditModal from './modules/ExamEditModal'
// UpdateAvatarModal
import UpdateAvatarModal from '@views/list/modules/UpdateAvatarModal'
export default {
@ -34,27 +44,32 @@ export default {
StepByStepExamModal
},
data () {
const that = this // 便bootstrap-tablemethods
const that = this; // 便 bootstrap-table
return {
//
//
columns: [
{
// ""
title: '序号',
//
field: 'serial',
// 1 1
formatter: function (value, row, index) {
return index + 1 // 1
return index + 1;
}
},
{
title: '封面',
field: 'avatar',
width: 50,
// value "exam-avatar" div
formatter: (value, row) => {
return '<div class="exam-avatar">' + value + '</div>'
return '<div class="exam-avatar">' + value + '</div>';
},
events: {
'click .exam-avatar': function (e, value, row, index) {
that.handleAvatarEdit(row)
// "exam-avatar" that.handleAvatarEdit(row) row
'click.exam-avatar': function (e, value, row, index) {
that.handleAvatarEdit(row);
}
}
},
@ -83,33 +98,43 @@ export default {
title: '操作',
field: 'action',
width: '150px',
// "" "" HTML
formatter: (value, row) => {
return '<button type="button" class="btn btn-success view-exam">详情</button>' +
'&nbsp;&nbsp;' +
'<button type="button" class="btn btn-success edit-exam">编辑</button>'
'<button type="button" class="btn btn-success edit-exam">编辑</button>';
},
events: {
'click .view-exam': function (e, value, row, index) {
that.handleSub(row)
// "view-exam" that.handleSub(row) row
'click.view-exam': function (e, value, row, index) {
that.handleSub(row);
},
'click .edit-exam': function (e, value, row, index) {
that.handleEdit(row)
// "edit-exam" that.handleEdit(row) row
'click.edit-exam': function (e, value, row, index) {
that.handleEdit(row);
}
}
}
],
tableData: [], // bootstrap-table
// custom bootstrap-table
// bootstrap-table
tableData: [],
// bootstrap-table
options: {
//
search: true,
// /
showColumns: true,
// Excel
showExport: true,
//
pagination: true,
// HTML id id "toolbar" div
toolbar: '#toolbar',
//
//
advancedSearch: true,
idTable: 'advancedTable'
// http://www.itxst.com/bootstrap-table-events/tutorial.html
// 使
idTable: 'advancedTable',
// 使
// onClickRow: that.clickRow,
// onClickCell: that.clickCell, //
// onDblClickCell: that.dblClickCell //
@ -117,50 +142,55 @@ export default {
}
},
mounted () {
this.loadAll() //
// loadAll()
this.loadAll();
},
methods: {
handleEdit (record) {
// Todo:
console.log('开始编辑啦')
console.log(record)
this.$refs.editExamModal.edit(record)
// Todo:
console.log('开始编辑啦');
console.log(record);
this.$refs.editExamModal.edit(record);
},
handleAvatarEdit (record) {
// Todo:
console.log('开始更新封面啦')
console.log(record)
this.$refs.updateAvatarModal.edit(record)
// Todo:
console.log('开始更新封面啦');
console.log(record);
this.$refs.updateAvatarModal.edit(record);
},
handleSub (record) {
//
// console.log(record)
// this.$refs.modalView.edit(record)
//
// console.log(record);
// this.$refs.modalView.edit(record);
//
// $router.resolve 使 record.id
const routeUrl = this.$router.resolve({
path: `/exam/${record.id}`
})
//
window.open(routeUrl.href, '_blank')
});
// 使 window.open
window.open(routeUrl.href, '_blank');
},
handleOk () {
this.loadAll()
// @ok
this.loadAll();
},
loadAll () {
const that = this
const that = this;
// getExamAll()
getExamAll()
.then(res => {
if (res.code === 0) {
that.tableData = res.data
that.$refs.table._initTable()
// 0 tableData _initTable()
that.tableData = res.data;
that.$refs.table._initTable();
} else {
// "" msg
that.$notification.error({
message: '获取全部考试的列表失败',
description: res.msg
})
});
}
})
});
}
}
}

Loading…
Cancel
Save