Compare commits

...

14 Commits

@ -12,12 +12,21 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
*
*/
@Configuration
public class IntercepterConfig implements WebMvcConfigurer {
/**
* LoginInterceptor
*/
@Autowired
private LoginInterceptor loginInterceptor;
/**
*
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截user下的api

@ -7,12 +7,22 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
/**
* Servlet
*/
@Configuration
public class ServletConfig {
/**
* WebServerFactoryCustomizer beanWeb
* @return WebServerFactoryCustomizer<ConfigurableWebServerFactory>
*/
@Bean
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer() {
// 返回一个lambda表达式用于自定义Web服务器的工厂
return factory -> {
// 创建一个ErrorPage对象用于处理404错误
ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/");
// 将ErrorPage对象添加到Web服务器的工厂中
factory.addErrorPages(error404Page);
};
}

@ -23,12 +23,18 @@ import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
/**
* Swagger2
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
/**
* Docket bean
* @return Docket
*/
@Bean
public Docket api() {
ParameterBuilder ticketPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<>();
ticketPar.name("Access-Token").description("Rest接口权限认证token,无需鉴权可为空")
@ -48,6 +54,10 @@ public class Swagger2Config {
.globalOperationParameters(pars);
}
/**
* ApiInfo
* @return ApiInfo
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("online exam by springboot")

@ -40,51 +40,79 @@ public class UploadDownloadController {
// return FileTransUtil.uploadFile(uploadfile, "/root/" + File.separator + uploadfile.getOriginalFilename());
// }
/**
*
*
* @param dir
* @param uploadfile
* @return String
*/
@ApiOperation("单文件上传,支持同时传入参数")
@PostMapping("/api/upload/singleAndparas")
// 单文件上传。
public String uploadFileSingle(@RequestParam("dir") String dir, @RequestParam("file") MultipartFile uploadfile) {
// 调用FileTransUtil工具类中的uploadFile方法将上传的文件和目录作为参数传入。
return FileTransUtil.uploadFile(uploadfile, dir);
}
// 单文件上传支持同时传入参数Model。
/**
* Model
*
* @param model UploadModel2
* @return String
*/
@ApiOperation("单文件上传,支持同时传入参数,Model")
@PostMapping("/upload/single/model")
public String singleUploadFileModel(@ModelAttribute("model") UploadModel2 model) {
// 调用FileTransUtil工具类中的uploadFile方法将上传的文件和目录作为参数传入。
return FileTransUtil.uploadFile(model.getFile(), model.getDir());
}
// 多文件上传,支持同时传入参数。
/**
*
*
* @param dir
* @param uploadfiles
* @return String
*/
@ApiOperation("多文件上传,支持同时传入参数")
@PostMapping("upload/multiAndparas")
public String uploadFileMulti(@RequestParam("dir") String dir, @RequestParam("files") MultipartFile[] uploadfiles) {
// 调用FileTransUtil工具类中的uploadFiles方法将上传的文件数组和目录作为参数传入。
return FileTransUtil.uploadFiles(uploadfiles, dir);
}
// 多文件上传,支持同时传入参数。
/**
*
*
* @param model UploadModel
* @return String
*/
@ApiOperation("多文件上传,支持同时传入参数")
@PostMapping(value = "/upload/multi/model")
public String multiUploadFileModel(@ModelAttribute(("model")) UploadModel model) {
// 调用FileTransUtil工具类中的uploadFiles方法将上传的文件数组和目录作为参数传入。
return FileTransUtil.uploadFiles(model.getFiles(), model.getDir());
}
// Get下载文件
/**
* Get
*
* @param filePath
* @return ResponseEntity<InputStreamResource>
* @throws IOException
*/
@ApiOperation("Get下载文件")
@GetMapping(value = "/download/get")
public ResponseEntity<InputStreamResource> downloadFileGet(@RequestParam String filePath) throws IOException {
// 调用FileTransUtil工具类中的downloadFile方法将文件路径作为参数传入。
return FileTransUtil.downloadFile(filePath);
}
// Post下载文件
/**
* Post
*
* @param downloadQo DownloadQo
* @return ResponseEntity<InputStreamResource>
* @throws IOException
*/
@ApiOperation("Post下载文件")
@PostMapping(value = "/download/post")
public ResponseEntity<InputStreamResource> downloadFilePost(@RequestBody DownloadQo downloadQo) throws IOException {
// 调用FileTransUtil工具类中的downloadFile方法将文件路径作为参数传入。
return FileTransUtil.downloadFile(downloadQo.getPath());
}
}

@ -29,6 +29,12 @@ public class UserController {
@Autowired
private UserService userService;
/**
*
*
* @param registerDTO RegisterDTO
* @return ResultVO<User>
*/
@PostMapping("/register")
@ApiOperation("注册")
ResultVO<User> register(@RequestBody RegisterDTO registerDTO) {
@ -44,6 +50,12 @@ public class UserController {
return resultVO;
}
/**
* ,token
*
* @param loginQo LoginQo
* @return ResultVO<String>
*/
@PostMapping("/login")
@ApiOperation("根据用户名或邮箱登录,登录成功返回token")
ResultVO<String> login(@RequestBody LoginQo loginQo) { // 这里不用手机号是因为手机号和用户名难以进行格式区分,而用户名和
@ -60,6 +72,12 @@ public class UserController {
return resultVO;
}
/**
*
*
* @param request HttpServletRequest
* @return ResultVO<UserVo>
*/
@GetMapping("/user-info")
@ApiOperation("获取用户信息")
// 根据请求获取用户信息
@ -72,6 +90,12 @@ public class UserController {
return new ResultVO<>(ResultEnum.GET_INFO_SUCCESS.getCode(), ResultEnum.GET_INFO_SUCCESS.getMessage(), userVo);
}
/**
*
*
* @param request HttpServletRequest
* @return ResultVO<UserInfoVo>
*/
@GetMapping("/info")
@ApiOperation("获取用户的详细信息,包括个人信息页面和操作权限")
// 获取用户信息的接口
@ -86,6 +110,12 @@ public class UserController {
return new ResultVO<>(ResultEnum.GET_INFO_SUCCESS.getCode(), ResultEnum.GET_INFO_SUCCESS.getMessage(), userInfoVo);
}
/**
*
*
* @param request HttpServletRequest
* @return String
*/
@GetMapping("/test")
@ApiOperation("测试接口")
String test(HttpServletRequest request) {

@ -8,11 +8,26 @@ package lsgwr.exam.dto;
import lombok.Data;
/**
*
*/
@Data
public class RegisterDTO {
/**
*
*/
private String email;
/**
*
*/
private String password;
/**
*
*/
private String password2;
/**
*
*/
private String mobile;
/**
*

@ -23,37 +23,85 @@ import java.util.Date;
@Entity
@Data
@DynamicUpdate
/**
*
*/
public class Exam {
// 使用JPA的@Id注解声明该字段为主键
/**
* ID
*/
@Id
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注解自定义日期格式
private String examId;
/**
*
*/
private String examName;
/**
*
*/
private String examAvatar;
/**
*
*/
private String examDescription;
/**
* IDID
*/
private String examQuestionIds;
/**
* ID
*/
private String examQuestionIdsRadio;
/**
* ID
*/
private String examQuestionIdsCheck;
/**
* ID
*/
private String examQuestionIdsJudge;
/**
*
*/
private Integer examScore;
/**
*
*/
private Integer examScoreRadio;
/**
*
*/
private Integer examScoreCheck;
/**
*
*/
private Integer examScoreJudge;
/**
* ID
*/
private String examCreatorId;
/**
*
*/
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
* 使Jackson@JsonFormat便
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
/**
* Java
* 使Hibernate@DynamicUpdate

@ -16,13 +16,22 @@ import javax.persistence.Id;// 引入JPA中的@Id注解标识实体类的主
@Data
// 使用@Entity注解标识这是一个JPA实体类
@Entity
public class ExamRecordLevel {
// 考试记录等级的ID是主键通过@Id注解标识并通过@GeneratedValue注解指定主键生成策略
/**
*
*/
public class ExamRecordLevel {
/**
* ID@Id@GeneratedValue
*/
@Id
@GeneratedValue
private Integer examRecordLevelId;
// 考试记录等级的名称,用于标识等级,如“优秀”、“良好”等
/**
*
*/
private String examRecordLevelName;
// 考试记录等级的描述,用于对等级进行更详细的说明
/**
*
*/
private String examRecordLevelDescription;
}

@ -12,16 +12,28 @@ import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
*
*/
@Data
@Entity
public class Page {
/**
* ID@Id@GeneratedValue
*/
@Id
@GeneratedValue
private Integer pageId;
/**
*
*/
private String pageName;
/**
*
*/
private String pageDescription;
/**
* ID
*/
private String actionIds;
}

@ -9,18 +9,32 @@ package lsgwr.exam.exception;
import lsgwr.exam.enums.ResultEnum;
import lombok.Getter;
/**
*
*/
@Getter
public class ExamException extends RuntimeException {
// 定义异常码
/**
*
*/
private Integer code;
// 构造函数,传入枚举类型
/**
*
*
* @param resultEnum ResultEnum
*/
public ExamException(ResultEnum resultEnum) {
super(resultEnum.getMessage());
this.code = resultEnum.getCode();
}
// 构造函数,传入异常码和异常信息
/**
*
*
* @param code
* @param message
*/
public ExamException( Integer code, String message) {
super(message);
this.code = code;

@ -23,6 +23,10 @@ import java.io.PrintWriter;
*
* @author liangshanguang
*/
/**
* Token
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {

@ -13,5 +13,8 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
public class DownloadQo {
/**
*
*/
String path;
}

@ -10,6 +10,9 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
*/
@Data
@AllArgsConstructor
@NoArgsConstructor

@ -12,6 +12,9 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;
/**
*
*/
@Data
@AllArgsConstructor
@NoArgsConstructor

@ -12,6 +12,9 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;
/**
*
*/
@Data
@AllArgsConstructor
@NoArgsConstructor

@ -9,6 +9,8 @@ package lsgwr.exam.repository;
import lsgwr.exam.entity.ExamRecordLevel;
import org.springframework.data.jpa.repository.JpaRepository;
// ExamRecordLevelRepository接口继承JpaRepository接口用于操作ExamRecordLevel实体类
/**
* ExamRecordLevelRepositoryJpaRepositoryExamRecordLevel
*/
public interface ExamRecordLevelRepository extends JpaRepository<ExamRecordLevel, Integer> {
}

@ -5,6 +5,9 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
/**
*
*/
public interface ExamRecordRepository extends JpaRepository<ExamRecord, String> {
/**
*

@ -12,7 +12,9 @@ import org.springframework.data.jpa.repository.Query;
import java.util.List;
// ExamRepository接口继承JpaRepository接口用于操作Exam实体类
/**
* ExamRepositoryJpaRepositoryExam
*/
public interface ExamRepository extends JpaRepository<Exam, String> {
// 使用JPQL查询语句查询Exam实体类按照updateTime降序排列
@Query("select e from Exam e order by e.updateTime desc")

@ -8,6 +8,8 @@ package lsgwr.exam.repository;
import lsgwr.exam.entity.Page;
import org.springframework.data.jpa.repository.JpaRepository;
/**
*
*/
public interface PageRepository extends JpaRepository<Page, Integer> {
}

@ -8,6 +8,8 @@ package lsgwr.exam.repository;
import lsgwr.exam.entity.QuestionCategory;
import org.springframework.data.jpa.repository.JpaRepository;
/**
*
*/
public interface QuestionCategoryRepository extends JpaRepository<QuestionCategory, Integer> {
}

@ -8,6 +8,8 @@ package lsgwr.exam.repository;
import lsgwr.exam.entity.QuestionLevel;
import org.springframework.data.jpa.repository.JpaRepository;
/**
*
*/
public interface QuestionLevelRepository extends JpaRepository<QuestionLevel, Integer> {
}

@ -8,6 +8,8 @@ package lsgwr.exam.repository;
import lsgwr.exam.entity.QuestionOption;
import org.springframework.data.jpa.repository.JpaRepository;
/**
*
*/
public interface QuestionOptionRepository extends JpaRepository<QuestionOption, String> {
}

@ -11,9 +11,22 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
/**
*
*/
public interface QuestionRepository extends JpaRepository<Question, String> {
/**
* ID
*
* @param id ID
* @return
*/
List<Question> findByQuestionTypeId(Integer id);
/**
*
*
* @return
*/
@Query("select q from Question q order by q.updateTime desc")
List<Question> findAll();
}

@ -8,6 +8,8 @@ package lsgwr.exam.repository;
import lsgwr.exam.entity.QuestionType;
import org.springframework.data.jpa.repository.JpaRepository;
/**
*
*/
public interface QuestionTypeRepository extends JpaRepository<QuestionType, Integer> {
}

@ -9,5 +9,8 @@ package lsgwr.exam.repository;
import lsgwr.exam.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository;
/**
*
*/
public interface RoleRepository extends JpaRepository<Role, Integer> {
}

@ -8,7 +8,9 @@ package lsgwr.exam.repository;
import lsgwr.exam.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
/**
*
*/
public interface UserRepository extends JpaRepository<User, String> {
/**
*

@ -19,6 +19,9 @@ import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.*;
/**
*
*/
@Service
@Transactional
public class ExamServiceImpl implements ExamService {
@ -39,6 +42,17 @@ public class ExamServiceImpl implements ExamService {
private final QuestionOptionRepository questionOptionRepository;
/**
*
* @param questionRepository
* @param userRepository
* @param questionLevelRepository
* @param questionTypeRepository
* @param questionCategoryRepository
* @param questionOptionRepository
* @param examRepository
* @param examRecordRepository
*/
public ExamServiceImpl(QuestionRepository questionRepository, UserRepository userRepository, QuestionLevelRepository questionLevelRepository, QuestionTypeRepository questionTypeRepository, QuestionCategoryRepository questionCategoryRepository, QuestionOptionRepository questionOptionRepository, ExamRepository examRepository, ExamRecordRepository examRecordRepository) {
this.questionRepository = questionRepository;//
this.userRepository = userRepository;//
@ -50,12 +64,22 @@ public class ExamServiceImpl implements ExamService {
this.examRecordRepository = examRecordRepository;
}
/**
* @decription :
* return List<QuestionVo>
*/
@Override
public List<QuestionVo> getQuestionAll() {
List<Question> questionList = questionRepository.findAll();
return getQuestionVos(questionList);
}
/**
* Vo
*
* @param questionList
* @return Vo
*/
private List<QuestionVo> getQuestionVos(List<Question> questionList) {
// 需要自定义的question列表
List<QuestionVo> questionVoList = new ArrayList<>();

@ -47,7 +47,12 @@ public class UserServiceImpl implements UserService {
@Autowired
ActionRepository actionRepository;
/**
*
*
* @param registerDTO
* @return null
*/
@Override
public User register(RegisterDTO registerDTO) {
try {
@ -81,6 +86,12 @@ public class UserServiceImpl implements UserService {
}
}
/**
*
*
* @param loginQo
* @return tokennull
*/
@Override
public String login(LoginQo loginQo) {
User user;
@ -106,7 +117,12 @@ public class UserServiceImpl implements UserService {
}
return null;
}
/**
*
*
* @param userId id
* @return
*/
@Override
public UserVo getUserInfo(String userId) {
User user = userRepository.findById(userId).orElse(null);
@ -115,7 +131,12 @@ public class UserServiceImpl implements UserService {
BeanUtils.copyProperties(user, userVo);
return userVo;
}
/**
*
*
* @param userId id
* @return
*/
@Override
public UserInfoVo getInfo(String userId) {
User user = userRepository.findById(userId).orElse(null);

@ -7,22 +7,32 @@
package lsgwr.exam.utils;
import lsgwr.exam.vo.ResultVO;
/**
*
*/
public class ResultVOUtil {
/**
*
*/
public static ResultVO success(Integer code, String msg, Object object) {
return new ResultVO(code, msg, object);
}
/**
*
*/
public static ResultVO success(Object object) {
return new ResultVO(0, "成功", object);
}
/**
*
*/
public static ResultVO success() {
return new ResultVO(0, "成功", null);
}
/**
*
*/
public static ResultVO error(Integer code, String msg) {
return new ResultVO(code, msg, null);
}

@ -8,15 +8,24 @@ package lsgwr.exam.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* Action
*/
@Data
public class ActionVo {
/**
*
*/
@JsonProperty("action")
private String actionName;
/**
*
*/
@JsonProperty("describe")
private String actionDescription;
/**
*
*/
@JsonProperty("defaultCheck")
private Boolean defaultCheck;
}

@ -16,7 +16,9 @@ import java.io.Serializable;
@AllArgsConstructor
@NoArgsConstructor
public class JsonData implements Serializable {
/**
* id
*/
private static final long serialVersionUID = 1L;
/**

@ -10,15 +10,24 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
*
*/
@Data
public class PageVo {
/**
*
*/
@JsonProperty("actionEntitySet")
private List<ActionVo> actionVoList;
/**
*
*/
@JsonProperty("permissionId")
private String pageName;
/**
*
*/
@JsonProperty("permissionName")
private String pageDescription;
}

@ -10,7 +10,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* ,
*/
@Data
public class QuestionCreateSimplifyVo {
/**

@ -10,7 +10,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
*
*/
@Data
public class QuestionCreateVo {
/**

@ -11,7 +11,9 @@ import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
@Data
public class QuestionDetailVo {
/**

@ -8,7 +8,9 @@ package lsgwr.exam.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
*
*/
@Data
public class QuestionOptionCreateVo {

@ -8,18 +8,29 @@ package lsgwr.exam.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
*
*/
@Data
public class QuestionOptionVo {
/**
* id
*/
@JsonProperty("id")
private String questionOptionId;
/**
*
*/
@JsonProperty("content")
private String questionOptionContent;
/**
*
*/
@JsonProperty("answer")
private Boolean answer = false;
/**
*
*/
@JsonProperty("description")
private String questionOptionDescription;
}

@ -10,7 +10,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
*
*/
@Data
public class QuestionPageVo {

@ -13,15 +13,24 @@ import lsgwr.exam.entity.QuestionType;
import lombok.Data;
import java.util.List;
/**
*
*/
@Data
public class QuestionSelectionVo {
/**
*
*/
@JsonProperty("types")
private List<QuestionType> questionTypeList;
/**
*
*/
@JsonProperty("categories")
private List<QuestionCategory> questionCategoryList;
/**
*
*/
@JsonProperty("levels")
private List<QuestionLevel> questionLevelList;
}

@ -12,15 +12,24 @@ import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* VO
*/
@Data
public class QuestionVo {
/**
* idquestion
*/
@JsonProperty("id")
private String questionId;
/**
* question
*/
@JsonProperty("name")
private String questionName;
/**
* question
*/
@JsonProperty("score")
private Integer questionScore;

@ -11,7 +11,9 @@ import lombok.Data;
import java.util.HashMap;
import java.util.List;
/**
* VO
*/
@Data
public class RecordDetailVo {
/**

@ -13,7 +13,12 @@ import lombok.Data;
@JsonInclude(JsonInclude.Include.NON_NULL) // 避免返回NULL的字段
public class ResultVO<T> {
/**
*
* @param code
* @param msg
* @param data
*/
public ResultVO(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;

@ -10,18 +10,29 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* VO
*/
@Data
public class RoleVo {
/**
*
*/
@JsonProperty("id")
private String roleName;
/**
*
*/
@JsonProperty("name")
private String roleDescription;
/**
*
*/
@JsonProperty("describe")
private String roleDetail;
/**
*
*/
@JsonProperty("permissions")
private List<PageVo> pageVoList;
}

@ -8,19 +8,29 @@ package lsgwr.exam.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
*
*/
@Data
public class UserInfoVo {
/**
* ID
*/
@JsonProperty("id")
private String userId;
/**
* URL
*/
@JsonProperty("avatar")
private String userAvatar;
/**
*
*/
@JsonProperty("name")
private String userNickname;
/**
*
*/
@JsonProperty("username")
private String userUsername;

@ -8,30 +8,49 @@ package lsgwr.exam.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
*
*/
@Data
public class UserVo {
/**
* ID
*/
@JsonProperty("id")
private String userId;
/**
*
*/
@JsonProperty("username")
private String userUsername;
/**
*
*/
@JsonProperty("nickname")
private String userNickname;
/**
* ID
*/
@JsonProperty("role")
private Integer userRoleId;
/**
*
*/
@JsonProperty("avatar")
private String userAvatar;
/**
*
*/
@JsonProperty("description")
private String userDescription;
/**
*
*/
@JsonProperty("email")
private String userEmail;
/**
*
*/
@JsonProperty("phone")
private String userPhone;
}

@ -5,7 +5,7 @@ spring:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: aA111111
url: jdbc:mysql://localhost:3306/exam?characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
url: jdbc:mysql://localhost:3306/exam?characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&&allowPublicKeyRetrieval=true
jpa:
# 调试的时候用用于打印完成SQL语句(但是不打印参数),联合下面的logging.level一同打印最完整的SQL信息(语句+参数)
show-sql: false

File diff suppressed because one or more lines are too long

@ -6,13 +6,91 @@
<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>
<!-- 定义加载中的样式 -->
<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>
<!-- 当JavaScript被禁用时显示提示信息 -->
<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>

@ -1 +1,52 @@
#preloadingAnimation{position:fixed;left:0;top:0;height:100%;width:100%;background:#ffffff;user-select:none;z-index: 9999;overflow: hidden}.lds-roller{display:inline-block;position:relative;left:50%;top:50%;transform:translate(-50%,-50%);width:64px;height:64px;}.lds-roller div{animation:lds-roller 1.2s cubic-bezier(0.5,0,0.5,1) infinite;transform-origin:32px 32px;}.lds-roller div:after{content:" ";display:block;position:absolute;width:6px;height:6px;border-radius:50%;background:#13c2c2;margin:-3px 0 0 -3px;}.lds-roller div:nth-child(1){animation-delay:-0.036s;}.lds-roller div:nth-child(1):after{top:50px;left:50px;}.lds-roller div:nth-child(2){animation-delay:-0.072s;}.lds-roller div:nth-child(2):after{top:54px;left:45px;}.lds-roller div:nth-child(3){animation-delay:-0.108s;}.lds-roller div:nth-child(3):after{top:57px;left:39px;}.lds-roller div:nth-child(4){animation-delay:-0.144s;}.lds-roller div:nth-child(4):after{top:58px;left:32px;}.lds-roller div:nth-child(5){animation-delay:-0.18s;}.lds-roller div:nth-child(5):after{top:57px;left:25px;}.lds-roller div:nth-child(6){animation-delay:-0.216s;}.lds-roller div:nth-child(6):after{top:54px;left:19px;}.lds-roller div:nth-child(7){animation-delay:-0.252s;}.lds-roller div:nth-child(7):after{top:50px;left:14px;}.lds-roller div:nth-child(8){animation-delay:-0.288s;}.lds-roller div:nth-child(8):after{top:45px;left:10px;}#preloadingAnimation .load-tips{color: #13c2c2;font-size:2rem;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin-top:80px;text-align:center;width:400px;height:64px;} @keyframes lds-roller{0%{transform:rotate(0deg);} 100%{transform:rotate(360deg);}}
#preloadingAnimation{
position:fixed;left:0;top:0;height:100%;width:100%;background:#ffffff;user-select:none;
z-index: 9999;overflow: hidden}
.lds-roller{
display:inline-block;
position:relative;
left:50%;
top:50%;
transform:translate(-50%,-50%);
width:64px;
height:64px;
}
.lds-roller div{
animation:lds-roller 1.2s cubic-bezier(0.5,0,0.5,1) infinite;
transform-origin:32px 32px;
}
.lds-roller div:after{
content:" ";
display:block;
position:absolute;
width:6px;
height:6px;
border-radius:50%;
background:#13c2c2;
margin:-3px 0 0 -3px;
}
.lds-roller div:nth-child(1){animation-delay:-0.036s;}
.lds-roller div:nth-child(1):after{top:50px;left:50px;}
.lds-roller div:nth-child(2){animation-delay:-0.072s;}
.lds-roller div:nth-child(2):after{top:54px;left:45px;}
.lds-roller div:nth-child(3){animation-delay:-0.108s;}
.lds-roller div:nth-child(3):after{top:57px;left:39px;}
.lds-roller div:nth-child(4){animation-delay:-0.144s;}
.lds-roller div:nth-child(4):after{top:58px;left:32px;}
.lds-roller div:nth-child(5){animation-delay:-0.18s;}
.lds-roller div:nth-child(5):after{top:57px;left:25px;}
.lds-roller div:nth-child(6){animation-delay:-0.216s;}
.lds-roller div:nth-child(6):after{top:54px;left:19px;}
.lds-roller div:nth-child(7){animation-delay:-0.252s;}
.lds-roller div:nth-child(7):after{top:50px;left:14px;}
.lds-roller div:nth-child(8){animation-delay:-0.288s;}
.lds-roller div:nth-child(8):after{top:45px;left:10px;}
#preloadingAnimation .load-tips{
color: #13c2c2;
font-size:2rem;
position:absolute;
left:50%;
top:50%;
transform:translate(-50%,-50%);margin-top:80px;
text-align:center;width:400px;height:64px;}
@keyframes lds-roller{0%{transform:rotate(0deg);}
100%{transform:rotate(360deg);}}

@ -1 +1,27 @@
<div id="preloadingAnimation"><div class=lds-roller><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div><div class=load-tips>Loading</div></div>
<div id="preloadingAnimation"><div class=lds-roller><div>
</div>
<div>
</div>
<div>
</div>
<div>
</div>
<div>
</div>
<div>
</div>
<div>
</div>
<div>
</div>
</div>
<div class=load-tips>Loading</div>
</div>

@ -1,5 +1,31 @@
/**
* 预加载动画
*/
<div class="preloading-animate">
<!--预加载的包装容器 -->
<div class="preloading-wrapper">
<svg class="preloading-balls" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"><circle cx="67.802" cy="59.907" r="6" fill="#51CACC"><animate attributeName="cx" values="75;57.72542485937369" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="50;73.77641290737884" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#51CACC;#9DF871" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="46.079" cy="69.992" r="6" fill="#9DF871"><animate attributeName="cx" values="57.72542485937369;29.774575140626318" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="73.77641290737884;64.69463130731182" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#9DF871;#E0FF77" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="29.775" cy="52.449" r="6" fill="#E0FF77"><animate attributeName="cx" values="29.774575140626318;29.774575140626315" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="64.69463130731182;35.30536869268818" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#E0FF77;#DE9DD6" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="41.421" cy="31.521" r="6" fill="#DE9DD6"><animate attributeName="cx" values="29.774575140626315;57.72542485937368" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="35.30536869268818;26.22358709262116" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#DE9DD6;#FF708E" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="64.923" cy="36.13" r="6" fill="#FF708E"><animate attributeName="cx" values="57.72542485937368;75" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="26.22358709262116;49.99999999999999" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#FF708E;#51CACC" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle></svg>
<svg class="preloading-balls"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="67.802" cy="59.907" r="6" fill="#51CACC">
<animate attributeName="cx" values="75;57.72542485937369" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
<animate attributeName="cy" values="50;73.77641290737884" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
<animate attributeName="fill" values="#51CACC;#9DF871" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
</circle>
<circle cx="46.079" cy="69.992" r="6" fill="#9DF871"><animate attributeName="cx" values="57.72542485937369;29.774575140626318" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
<animate attributeName="cy" values="73.77641290737884;64.69463130731182" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
<animate attributeName="fill" values="#9DF871;#E0FF77" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
</circle><circle cx="29.775" cy="52.449" r="6" fill="#E0FF77">
<animate attributeName="cx" values="29.774575140626318;29.774575140626315" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
<animate attributeName="cy" values="64.69463130731182;35.30536869268818" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
<animate attributeName="fill" values="#E0FF77;#DE9DD6" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
</circle><circle cx="41.421" cy="31.521" r="6" fill="#DE9DD6"><animate attributeName="cx" values="29.774575140626315;57.72542485937368" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
<animate attributeName="cy" values="35.30536869268818;26.22358709262116" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
<animate attributeName="fill" values="#DE9DD6;#FF708E" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
</circle>
<circle cx="64.923" cy="36.13" r="6" fill="#FF708E"><animate attributeName="cx" values="57.72542485937368;75" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
<animate attributeName="cy" values="26.22358709262116;49.99999999999999" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
<animate attributeName="fill" values="#FF708E;#51CACC" keyTimes="0;1" dur="1s" repeatCount="indefinite"/>
</circle>
</svg>
</div>
</div>

@ -1 +1,20 @@
.preloading-animate{background:#ffffff;width:100%;height:100%;position:fixed;left:0;top:0;z-index:299;}.preloading-animate .preloading-wrapper{position:absolute;width:5rem;height:5rem;left:50%;top:50%;transform:translate(-50%,-50%);}.preloading-animate .preloading-wrapper .preloading-balls{font-size:5rem;}
.preloading-animate{
background:#ffffff;
width:100%;
height:100%;
position:fixed;
left:0;
top:0;
z-index:299;
}
.preloading-animate .preloading-wrapper{
position:absolute;
width:5rem;
height:5rem;
left:50%;
top:50%;
transform:translate(-50%,-50%);
}
.preloading-animate .preloading-wrapper .preloading-balls{
font-size:5rem;
}

@ -1,24 +1,33 @@
<template>
<!--异常页面-->
<div class="exception">
<!--背景图-->
<div class="imgBlock">
<!-- 图片元素根据传入的type动态设置背景图片 -->
<div class="imgEle" :style="{backgroundImage: `url(${config[type].img})`}">
</div>
</div>
<!-- 内容区块 -->
<div class="content">
<!-- 根据传入的type动态显示标题 -->
<h1>{{ config[type].title }}</h1>
<!-- 根据传入的type动态显示描述 -->
<div class="desc">{{ config[type].desc }}</div>
<!-- 操作按钮区块 -->
<div class="actions">
<!-- 点击按钮返回首页 -->
<a-button type="primary" @click="handleToHome"></a-button>
</div>
</div>
</div>
</template>
<script>
<script>//
import types from './type'
export default {
name: 'Exception',
//
props: {
type: {
type: String,
@ -27,11 +36,14 @@ export default {
},
data () {
return {
//
config: types
}
},
methods: {
//
handleToHome () {
// 使Vue Routerdashboard
this.$router.push({ name: 'dashboard' })
}
}

@ -1,19 +1,27 @@
/**
* 定义一个常量对象types用于存储不同HTTP状态码的信息
* 每个状态码都包含一个图片URL状态码标题和描述信息
* 这些信息可以用于在界面上展示更友好的错误提示
*/
const types = {
// 403状态码信息禁止访问
403: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
title: '403',
desc: '抱歉,你无权访问该页面'
},
// 404状态码信息页面未找到
404: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
title: '404',
desc: '抱歉,你访问的页面不存在或仍在开发中'
},
// 500状态码信息服务器内部错误
500: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
title: '500',
desc: '抱歉,服务器出错了'
}
}
// 将types对象导出以便在其他模块中使用
export default types

@ -1,18 +1,25 @@
<template>
<!-- 底部信息栏 -->
<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>
</div>
<!-- 版权信息区域 -->
<div class="copyright">
Copyright
<!-- 版权图标 -->
<a-icon type="copyright" /> 2020 <span>Liang Shan Guang</span>
</div>
</div>
</template>
<script>
<script>//
export default {
name: 'GlobalFooter',
data () {
@ -21,7 +28,7 @@ export default {
}
</script>
<style lang="less" scoped>
<style lang="less" scoped>//
.footer {
padding: 0 16px;
margin: 24px 0 24px;

@ -1,22 +1,31 @@
<template>
<!-- 使用 transition 组件为头部添加动画效果 -->
<transition name="showHeader">
<!-- 根据 visible 属性控制头部的显示与隐藏 -->
<div v-if="visible" class="header-animat">
<!-- 根据 visible 属性fixedHeadersidebarOpened 等属性动态设置头部样式 -->
<a-layout-header
v-if="visible"
:class="[fixedHeader && 'ant-header-fixedHeader', sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed', ]"
:style="{ padding: '0' }">
<!-- 根据 mode 属性判断菜单类型并根据不同设备类型显示不同的折叠/展开图标 -->
<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">
<!-- 渲染 logo 组件根据设备类型决定是否显示标题 -->
<logo class="top-nav-header" :show-title="device !== 'mobile'"/>
<!-- 根据设备类型和折叠状态渲染菜单 -->
<s-menu v-if="device !== 'mobile'" mode="horizontal" :menu="menus" :theme="theme" />
<a-icon v-else class="trigger" :type="collapsed ? 'menu-fold' : 'menu-unfold'" @click="toggle" />
</div>
<!-- 渲染用户菜单组件 -->
<user-menu class="header-index-right"></user-menu>
</div>
</div>
@ -25,10 +34,11 @@
</transition>
</template>
<script>
<script>// logo
import UserMenu from '../tools/UserMenu'
import SMenu from '../Menu/'
import Logo from '../tools/Logo'
// 使
import { mixin } from '../../utils/mixin'
export default {
@ -39,6 +49,7 @@ export default {
Logo
},
mixins: [mixin],
//
props: {
mode: {
type: String,
@ -67,14 +78,18 @@ export default {
},
data () {
return {
//
visible: true,
//
oldScrollTop: 0
}
},
// /
mounted () {
document.body.addEventListener('scroll', this.handleScroll, { passive: true })
},
methods: {
//
handleScroll () {
if (!this.autoHideHeader) {
return
@ -96,17 +111,19 @@ export default {
})
}
},
// toggle
toggle () {
this.$emit('toggle')
}
},
//
beforeDestroy () {
document.body.removeEventListener('scroll', this.handleScroll, true)
}
}
</script>
<style lang="less">
<style lang="less">//
.header-animat{
position: relative;
z-index: 2;

@ -1,11 +1,14 @@
<template>
<!-- 侧边栏组件包含logo和菜单 -->
<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 />
<!-- 菜单组件 -->
<s-menu
:collapsed="collapsed"
:menu="menus"
@ -17,7 +20,7 @@
</template>
<script>
<script>// LogoSMenu
import Logo from '../../components/tools/Logo'
import SMenu from './index'
import { mixin, mixinDevice } from '../../utils/mixin'

@ -26,22 +26,35 @@
-->
<script>
/**
* 多标签页组件
*/
export default {
name: 'MultiTab',
data () {
return {
// fullPath
fullPathList: [],
//
pages: [],
// fullPath
activeKey: '',
//
newTabIndex: 0
}
},
created () {
//
this.pages.push(this.$route)
this.fullPathList.push(this.$route.fullPath)
this.selectedLastPath()
},
methods: {
/**
* 处理标签页编辑事件
* @param {String} targetKey - 目标标签页的 fullPath
* @param {String} action - 操作类型例如'remove'
*/
onEdit (targetKey, action) {
this[action](targetKey)
},
@ -53,11 +66,14 @@ export default {
this.selectedLastPath()
}
},
/**
* 选择最后一个标签页
*/
selectedLastPath () {
this.activeKey = this.fullPathList[this.fullPathList.length - 1]
},
// content menu
//
closeThat (e) {
this.remove(e)
},
@ -93,6 +109,10 @@ export default {
}
})
},
/**
* 处理右键菜单点击事件
* @param {Object} { key, item, domEvent } - 菜单项的相关信息
*/
closeMenuClick ({ key, item, domEvent }) {
const vkey = domEvent.target.getAttribute('data-vkey')
switch (key) {
@ -111,6 +131,10 @@ export default {
break
}
},
/**
* 渲染标签页右键菜单
* @param {String} e - 标签页的 fullPath
*/
renderTabPaneMenu (e) {
return (
<a-menu {...{ on: { click: this.closeMenuClick } }}>
@ -122,6 +146,11 @@ export default {
)
},
// render
/**
* 渲染标签页
* @param {String} title - 标签页的标题
* @param {String} keyPath - 标签页的 fullPath
*/
renderTabPane (title, keyPath) {
const menu = this.renderTabPaneMenu(keyPath)
@ -133,6 +162,10 @@ export default {
}
},
watch: {
/**
* 监听路由变化更新标签页列表和激活的标签页
* @param {Object} newVal - 新的路由信息
*/
'$route': function (newVal) {
this.activeKey = newVal.fullPath
if (this.fullPathList.indexOf(newVal.fullPath) < 0) {
@ -140,6 +173,10 @@ export default {
this.pages.push(newVal)
}
},
/**
* 监听激活的标签页变化更新路由
* @param {String} newPathKey - 新的激活标签页的 fullPath
*/
activeKey: function (newPathKey) {
this.$router.push({ path: newPathKey })
}

@ -1,4 +1,6 @@
<template>
<!-- 进入层展示一个通知消息组件透过点击设置触发展示-->
<!-- 通过 <a-popover> 实现通知消息框配置消息流和点击事件-->
<a-popover
v-model="visible"
trigger="click"
@ -9,23 +11,40 @@
:overlayStyle="{ width: '300px', top: '50px' }"
>
<template slot="content">
<!-- 透过加载线段和标签分类显示通知 -->
<a-spin :spinning="loadding">
<a-tabs>
<a-tab-pane tab="通知" key="1">
<!-- 显示通知列表具体消息以组件形式显示 -->
<a-list>
<a-list-item>
<a-list-item-meta title="你收到了 14 份新周报" description="一年前">
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png"/>
<a-list-item-meta
title="你收到了 14 份新周报"
description="一年前">
<a-avatar
style="background-color: white"
slot="avatar"
src="https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png"/>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta title="你推荐的 曲妮妮 已通过第三轮面试" description="一年前">
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png"/>
<a-list-item-meta
title="你推荐的 曲妮妮 已通过第三轮面试"
description="一年前">
<a-avatar
style="background-color: white"
slot="avatar"
src="https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png"/>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta title="这种模板可以区分多种通知类型" description="一年前">
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png"/>
<a-list-item-meta
title="这种模板可以区分多种通知类型"
description="一年前">
<a-avatar
style="background-color: white"
slot="avatar"
src="https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png"/>
</a-list-item-meta>
</a-list-item>
</a-list>
@ -39,6 +58,7 @@
</a-tabs>
</a-spin>
</template>
<!-- 点击回调消息分类加载功能 -->
<span @click="fetchNotice" class="header-notice">
<a-badge count="12">
<a-icon style="font-size: 16px; padding: 4px" type="bell" />
@ -49,41 +69,45 @@
<script>
export default {
name: 'HeaderNotice',
name: 'HeaderNotice', //
data () {
return {
loadding: false,
visible: false
loadding: false, //
visible: false //
}
},
methods: {
//
fetchNotice () {
if (!this.visible) {
this.loadding = true
//
setTimeout(() => {
this.loadding = false
}, 2000)
} else {
this.loadding = false
}
this.visible = !this.visible
this.visible = !this.visible //
}
}
}
</script>
<style lang="css">
.header-notice-wrapper {
/* 通知框样式配置 */
.header-notice-wrapper {
top: 50px !important;
}
}
</style>
<style lang="less" scoped>
.header-notice{
/* 层级框样式 */
.header-notice{
display: inline-block;
transition: all 0.3s;
span {
vertical-align: initial;
}
}
}
</style>

@ -1,28 +1,38 @@
<template>
<!-- 页面头部组件 -->
<div class="page-header">
<div class="page-header-index-wide">
<!-- 面包屑导航 -->
<s-breadcrumb />
<div class="detail">
<!-- 主要内容区域 -->
<div class="main" v-if="!$route.meta.hiddenHeaderContent">
<div class="row">
<!-- Logo图片 -->
<img v-if="logo" :src="logo" class="logo"/>
<!-- 页面标题 -->
<h1 v-if="title" class="title">{{ title }}</h1>
<!-- 自定义操作区域 -->
<div class="action">
<slot name="action"></slot>
</div>
</div>
<div class="row">
<!-- 头像 -->
<div v-if="avatar" class="avatar">
<a-avatar :src="avatar" />
</div>
<!-- 自定义内容区域 -->
<div v-if="this.$slots.content" class="headerContent">
<slot name="content"></slot>
</div>
<!-- 额外的自定义区域 -->
<div v-if="this.$slots.extra" class="extra">
<slot name="extra"></slot>
</div>
</div>
<div>
<!-- 页面菜单 -->
<slot name="pageMenu"></slot>
</div>
</div>
@ -31,25 +41,28 @@
</div>
</template>
<script>
<script>//
import Breadcrumb from '../../components/tools/Breadcrumb'
//
export default {
name: 'PageHeader',
components: {
's-breadcrumb': Breadcrumb
},
props: {
// true
title: {
type: [String, Boolean],
default: true,
required: false
},
// Logo
logo: {
type: String,
default: '',
required: false
},
//
avatar: {
type: String,
default: '',
@ -62,7 +75,7 @@ export default {
}
</script>
<style lang="less" scoped>
<style lang="less" scoped>//
.page-header {
background: #fff;
padding: 16px 32px 0;
@ -152,7 +165,7 @@ export default {
}
}
}
//
.mobile .page-header {
.main {
.row {

@ -1,28 +1,34 @@
<template>
<!-- 结果展示组件 -->
<div class="result">
<!-- 根据成功或错误状态动态显示对应图标 -->
<div>
<a-icon :class="{ 'icon': true, [`${type}`]: true }" :type="localIsSuccess ? 'check-circle' : 'close-circle'"/>
</div>
<!-- 标题区域支持自定义插槽内容 -->
<div class="title">
<slot name="title">
{{ title }}
</slot>
</div>
<!-- 描述区域支持自定义插槽内容 -->
<div class="description">
<slot name="description">
{{ description }}
</slot>
</div>
<!-- 额外信息区域根据插槽内容动态显示 -->
<div class="extra" v-if="$slots.default">
<slot></slot>
</div>
<!-- 操作区域根据插槽内容动态显示 -->
<div class="action" v-if="$slots.action">
<slot name="action"></slot>
</div>
</div>
</template>
<script>
<script>//
const resultEnum = ['success', 'error']
export default {
@ -33,6 +39,7 @@ export default {
type: Boolean,
default: false
},
// success error
type: {
type: String,
default: resultEnum[0],
@ -40,16 +47,19 @@ export default {
return (val) => resultEnum.includes(val)
}
},
//
title: {
type: String,
default: ''
},
//
description: {
type: String,
default: ''
}
},
computed: {
//
localIsSuccess: function () {
return this.type === resultEnum[0]
}
@ -69,9 +79,11 @@ export default {
line-height: 72px;
margin-bottom: 24px;
}
//
.success {
color: #52c41a;
}
//
.error {
color: red;
}

@ -1,12 +1,22 @@
<template>
<!-- 设置项容器 -->
<div class="setting-drawer-index-item">
<!-- 设置项标题 -->
<h3 class="setting-drawer-index-title">{{ title }}</h3>
<!-- 插槽内容用于显示设置项的具体内容 -->
<slot></slot>
<!-- 根据props中的divider决定是否显示分割线 -->
<a-divider v-if="divider"/>
</div>
</template>
<script>
/**
* 设置项组件
*
* @param {String} title - 设置项的标题默认为空字符串
* @param {Boolean} divider - 是否显示设置项后的分割线默认不显示
*/
export default {
name: 'SettingItem',
props: {
@ -22,11 +32,11 @@ export default {
}
</script>
<style lang="less" scoped>
<style lang="less" scoped>/* 设置项样式 */
.setting-drawer-index-item {
margin-bottom: 24px;
/* 设置项标题样式 */
.setting-drawer-index-title {
font-size: 14px;
color: rgba(0, 0, 0, .85);

@ -1,15 +1,18 @@
<template>
<!-- 标准表单行组件根据不同的属性组合有不同的样式表现 -->
<div :class="[prefixCls, lastCls, blockCls, gridCls]">
<!-- 标题部分只有当title属性存在时才显示 -->
<div v-if="title" class="antd-pro-components-standard-form-row-index-label">
<span>{{ title }}</span>
</div>
<!-- 内容部分使用slot以便于自定义内容 -->
<div class="antd-pro-components-standard-form-row-index-content">
<slot></slot>
</div>
</div>
</template>
<script>
<script>//
const classes = [
'antd-pro-components-standard-form-row-index-standardFormRowBlock',
'antd-pro-components-standard-form-row-index-standardFormRowGrid',
@ -17,6 +20,7 @@ const classes = [
]
export default {
name: 'StandardFormRow',
//
props: {
prefixCls: {
type: String,
@ -36,6 +40,7 @@ export default {
type: Boolean
}
},
// props
computed: {
lastCls () {
return this.last ? classes[2] : null
@ -50,9 +55,9 @@ export default {
}
</script>
<style lang="less" scoped>
<style lang="less" scoped>//
@import '../index.less';
//
.antd-pro-components-standard-form-row-index-standardFormRow {
display: flex;
margin-bottom: 16px;
@ -71,7 +76,7 @@ export default {
padding: 0;
line-height: 32px;
}
//
.antd-pro-components-standard-form-row-index-label {
flex: 0 0 auto;
margin-right: 24px;
@ -87,27 +92,27 @@ export default {
}
}
}
//
.antd-pro-components-standard-form-row-index-content {
flex: 1 1 0;
/deep/ .ant-form-item:last-child {
margin-right: 0;
}
}
//
&.antd-pro-components-standard-form-row-index-standardFormRowLast {
margin-bottom: 0;
padding-bottom: 0;
border: none;
}
// 使block
&.antd-pro-components-standard-form-row-index-standardFormRowBlock {
/deep/ .ant-form-item,
div.ant-form-item-control-wrapper {
display: block;
}
}
// 使grid
&.antd-pro-components-standard-form-row-index-standardFormRowGrid {
/deep/ .ant-form-item,
div.ant-form-item-control-wrapper {

@ -1,6 +1,9 @@
<template>
<!-- 面包屑组件 -->
<a-breadcrumb class="breadcrumb">
<!-- 遍历面包屑列表生成每个面包屑项 -->
<a-breadcrumb-item v-for="(item, index) in breadList" :key="item.name">
<!-- 根据条件渲染面包屑项的链接或文本 -->
<router-link
v-if="item.name != name && index != 1"
:to="{ path: item.path === '' ? '/' : item.path }"
@ -14,26 +17,35 @@
export default {
data () {
return {
//
name: '',
//
breadList: []
}
},
created () {
//
this.getBreadcrumb()
},
methods: {
/**
* 获取面包屑信息
* 该方法根据当前路由生成面包屑列表
*/
getBreadcrumb () {
this.breadList = []
// this.breadList.push({name: 'index', path: '/dashboard/', meta: {title: ''}})
//
this.name = this.$route.name
this.$route.matched.forEach(item => {
// item.name !== 'index' && this.breadList.push(item)
//
this.breadList.push(item)
})
}
},
watch: {
//
$route () {
this.getBreadcrumb()
}

@ -1,14 +1,19 @@
<template>
<!-- 头部信息组件根据props调整样式和布局 -->
<div class="head-info" :class="center && 'center'">
<!-- 显示标题 -->
<span>{{ title }}</span>
<!-- 显示内容 -->
<p>{{ content }}</p>
<!-- 根据bordered prop决定是否显示边框 -->
<em v-if="bordered"/>
</div>
</template>
<script>
<script>//
export default {
name: 'HeadInfo',
//
props: {
title: {
type: String,

@ -1,26 +1,34 @@
<template>
<!-- Logo组件主体 -->
<div class="logo">
<!-- 使用router-link组件创建到dashboard的链接 -->
<router-link :to="{name:'dashboard'}">
<!-- 渲染LogoSvg组件带alt属性说明 -->
<LogoSvg alt="logo" />
<!-- 根据showTitle属性决定是否显示标题 -->
<h1 v-if="showTitle">{{ title }}</h1>
</router-link>
</div>
</template>
<script>
<script>// logo.svg
import LogoSvg from '../../assets/logo.svg?inline'
// Logo
export default {
name: 'Logo',
//
components: {
LogoSvg
},
props: {
// titleString'Online Exam'
title: {
type: String,
default: 'Online Exam',
required: false
},
// showTitleBooleantrue
showTitle: {
type: Boolean,
default: true,

@ -1,19 +1,28 @@
<template>
<!-- 用户信息和操作菜单的容器 -->
<div class="user-wrapper">
<div class="content-box">
<!-- 用户下拉菜单 -->
<a-dropdown>
<!-- 用户信息展示区域 -->
<span class="action ant-dropdown-link user-dropdown-menu">
<!-- 用户头像 -->
<a-avatar class="avatar" size="small" :src="avatar()"/>
<!-- 用户昵称 -->
<span>{{ nickname() }}</span>
</span>
<!-- 下拉菜单内容 -->
<a-menu slot="overlay" class="user-dropdown-menu-wrapper">
<!-- 账户设置菜单项 -->
<a-menu-item key="1">
<router-link :to="{ name: 'settings' }">
<a-icon type="setting"/>
<span>账户设置</span>
</router-link>
</a-menu-item>
<!-- 菜单分割符 -->
<a-menu-divider/>
<!-- 退出登录菜单项 -->
<a-menu-item key="3">
<a href="javascript:;" @click="handleLogout">
<a-icon type="logout"/>
@ -44,6 +53,7 @@ export default {
return that.Logout({}).then(() => {
window.location.reload()
}).catch(err => {
//
that.$message.error({
title: '错误',
description: err.message
@ -51,6 +61,7 @@ export default {
})
},
onCancel () {
//
}
})
}

@ -14,16 +14,26 @@ import store from '../../store'
*
* @see https://github.com/sendya/ant-design-pro-vue/pull/53
*/
/**
* 创建一个 Vue 自定义指令 'action'用于控制元素基于用户权限的显示
*/
const action = Vue.directive('action', {
inserted: function (el, binding, vnode) {
// 获取指令的参数,即动作名称
const actionName = binding.arg
// 从 Vuex store 中获取当前用户的角色信息
const roles = store.getters.roles
// 获取当前路由的权限配置
const elVal = vnode.context.$route.meta.permission
// 确保权限配置为数组形式
const permissionId = elVal instanceof String && [elVal] || elVal
// 遍历用户角色的权限列表
roles.permissions.forEach(p => {
// 如果当前权限ID不在元素的权限配置中直接返回
if (!permissionId.includes(p.permissionId)) {
return
}
// 如果当前权限有动作列表限制,且动作列表中不包含指令的参数,移除或隐藏元素
if (p.actionList && !p.actionList.includes(actionName)) {
el.parentNode && el.parentNode.removeChild(el) || (el.style.display = 'none')
}

@ -1,16 +1,20 @@
<template>
<!-- 主要用于渲染子路由组件 -->
<div>
<router-view />
</div>
</template>
<script>
/**
* 定义一个名为 BlankLayout Vue 组件
* 该组件用作空白布局通常用于需要一个干净的页面来进行特定的功能或展示
*/
export default {
name: 'BlankLayout'
}
</script>
<style scoped>
/* 此处添加特定于组件的样式,使用 scoped 属性确保样式仅在本组件内生效 */
</style>

@ -1,10 +1,16 @@
<template>
<!-- 根据路由元数据决定是否显示页面头部内容 -->
<div :style="!$route.meta.hiddenHeaderContent ? 'margin: -24px -24px 0px;' : null">
<!-- pageHeader , route meta :true on hide -->
<!-- 条件渲染页面头部组件 -->
<page-header v-if="!$route.meta.hiddenHeaderContent" :title="pageTitle" :logo="logo" :avatar="avatar">
<!-- 插槽用于自定义操作按钮 -->
<slot slot="action" name="action"></slot>
<!-- 插槽用于自定义头部内容 -->
<slot slot="content" name="headerContent"></slot>
<!-- 默认头部内容当没有自定义头部内容且存在描述时显示 -->
<div slot="content" v-if="!this.$slots.headerContent && description">
<!-- 显示链接列表 -->
<p style="font-size: 14px;color: rgba(0,0,0,.65)">{{ description }}</p>
<div class="link">
<template v-for="(link, index) in linkList">
@ -15,12 +21,15 @@
</template>
</div>
</div>
<!-- 插槽用于自定义额外内容 -->
<slot slot="extra" name="extra">
<div class="extra-img">
<img v-if="typeof extraImage !== 'undefined'" :src="extraImage"/>
</div>
</slot>
<!-- 页面菜单区域 -->
<div slot="pageMenu">
<!-- 条件渲染搜索框 -->
<div class="page-menu-search" v-if="search">
<a-input-search
style="width: 80%; max-width: 522px;"
@ -29,6 +38,7 @@
enterButton="搜索"
/>
</div>
<!-- 条件渲染标签页 -->
<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">
@ -37,10 +47,13 @@
</div>
</div>
</page-header>
<!-- 主要内容区域 -->
<div class="content">
<!-- 插槽用于自定义页面内容 -->
<div class="page-header-index-wide">
<slot>
<!-- keep-alive -->
<!-- 条件渲染路由视图支持多页签 -->
<keep-alive v-if="multiTab">
<router-view ref="content" />
</keep-alive>
@ -96,15 +109,19 @@ export default {
this.getPageMeta()
},
methods: {
//
getPageMeta () {
// eslint-disable-next-line
// title
this.pageTitle = (typeof(this.title) === 'string' || !this.title) ? this.title : this.$route.meta.title
const content = this.$refs.content
if (content) {
if (content.pageMeta) {
// pageMeta
Object.assign(this, content.pageMeta)
} else {
//
this.description = content.description
this.linkList = content.linkList
this.extraImage = content.extraImage

@ -1,22 +1,32 @@
<script>
/**
* 定义一个名为 RouteView Vue 组件
* 该组件根据路由的 meta 信息和 store 中的 multiTab 状态决定是否缓存路由视图
*/
export default {
//
name: 'RouteView',
props: {
// true
keepAlive: {
type: Boolean,
default: true
}
},
//
data () {
return {}
},
//
render () {
// meta store getters
const { $route: { meta }, $store: { getters } } = this
const inKeep = (
<keep-alive>
<router-view />
</keep-alive>
)
//
const notKeep = (
<router-view />
)
@ -26,6 +36,7 @@ export default {
if (!getters.multiTab && meta.keepAlive === false) {
return notKeep
}
// keepAlive multiTab keepAlive
return this.keepAlive || getters.multiTab || meta.keepAlive ? inKeep : notKeep
}
}

@ -1,34 +1,39 @@
<template>
<!-- 用户布局模板根据设备类型调整样式 -->
<div id="userLayout" :class="['user-layout-wrapper', device]">
<div class="container">
<div class="top">
<div class="header">
<!-- 顶部标题和logo -->
<a href="/">
<img src="../assets/logo.svg" class="logo" alt="logo">
<span class="title">Online Exam</span>
</a>
</div>
<div class="desc">
<!-- 系统描述 -->
基于SpringBoot+Vue实现的在线考试系统
</div>
</div>
<!-- 路由视图用于显示子路由的内容 -->
<route-view></route-view>
<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>
</div>
<div class="copyright">
<!-- 版权信息 -->
Copyright &copy; 2020 Liang Shan Guang
</div>
</div>
</div>
</div>
</template>
// RouteView
<script>
import RouteView from './RouteView'
import { mixinDevice } from '../utils/mixin'
@ -41,9 +46,11 @@ export default {
return {}
},
mounted () {
// bodyuserLayout
document.body.classList.add('userLayout')
},
beforeDestroy () {
// bodyuserLayout
document.body.classList.remove('userLayout')
}
}
@ -52,7 +59,7 @@ export default {
<style lang="less" scoped>
#userLayout.user-layout-wrapper {
height: 100%;
//
&.mobile {
.container {
.main {

@ -1,12 +1,15 @@
import enquireJs from 'enquire.js'
// 定义设备类型的常量
export const DEVICE_TYPE = {
DESKTOP: 'desktop',
TABLET: 'tablet',
MOBILE: 'mobile'
}
// 检测设备类型的函数
export const deviceEnquire = function (callback) {
// 定义每个设备类型的匹配函数
const matchDesktop = {
match: () => {
callback && callback(DEVICE_TYPE.DESKTOP)
@ -25,6 +28,7 @@ export const deviceEnquire = function (callback) {
}
}
// 为每个设备类型注册媒体查询
// screen and (max-width: 1087.99px)
enquireJs
.register('screen and (max-width: 576px)', matchMobile)

@ -1,8 +1,8 @@
import Vue from 'vue'
import moment from 'moment'
import 'moment/locale/zh-cn'
import $ from 'jquery'
moment.locale('zh-cn')
import Vue from 'vue' // 引入Vue
import moment from 'moment' // 引入moment库
import 'moment/locale/zh-cn' // 引入moment的中文语言包
import $ from 'jquery' // 引入jQuery
moment.locale('zh-cn') // 设置moment的语言为中文
Vue.filter('NumberFormat', function (value) {
if (!value) {

@ -1,3 +1,4 @@
// 定义权限枚举
const PERMISSION_ENUM = {
'add': { key: 'add', label: '新增' },
'delete': { key: 'delete', label: '删除' },
@ -10,21 +11,30 @@ const PERMISSION_ENUM = {
'export': { key: 'export', label: '导出' }
}
// 定义插件
function plugin (Vue) {
// 如果插件已经安装,则返回
if (plugin.installed) {
return
}
// 如果Vue实例中没有$auth属性则定义$auth属性
!Vue.prototype.$auth && Object.defineProperties(Vue.prototype, {
$auth: {
get () {
// 获取当前实例
const _this = this
// 返回一个函数,用于判断权限
return (permissions) => {
// 将权限字符串分割成permission和action
const [permission, action] = permissions.split('.')
// 获取当前用户的权限列表
const permissionList = _this.$store.getters.roles.permissions
// 在权限列表中查找对应的permission
return permissionList.find((val) => {
return val.permissionId === permission
}).actionList.findIndex((val) => {
// 在actionList中查找对应的action
return val === action
}) > -1
}
@ -32,15 +42,21 @@ function plugin (Vue) {
}
})
// 如果Vue实例中没有$enum属性则定义$enum属性
!Vue.prototype.$enum && Object.defineProperties(Vue.prototype, {
$enum: {
get () {
// const _this = this;
// 返回一个函数,用于获取枚举值
return (val) => {
// 初始化result为PERMISSION_ENUM
let result = PERMISSION_ENUM
// 如果val存在则将val按'.'分割成数组
val && val.split('.').forEach(v => {
// 在result中查找对应的值
result = result && result[v] || null
})
// 返回result
return result
}
}
@ -48,4 +64,5 @@ function plugin (Vue) {
})
}
// 导出插件
export default plugin

@ -1,9 +1,13 @@
// import Vue from 'vue'
// 引入Vue
import { deviceEnquire, DEVICE_TYPE } from '../utils/device'
// 引入设备检测工具和设备类型
import { mapState } from 'vuex'
// 引入Vuex的mapState方法
// const mixinsComputed = Vue.config.optionMergeStrategies.computed
// const mixinsMethods = Vue.config.optionMergeStrategies.methods
// 定义Vue的computed和methods的合并策略
const mixin = {
computed: {
@ -72,5 +76,5 @@ const AppDeviceEnquire = {
})
}
}
// 导出mixin、AppDeviceEnquire、mixinDevice
export { mixin, AppDeviceEnquire, mixinDevice }

@ -1,8 +1,12 @@
// 导出一个函数将json字符串转换为对象
export function actionToObject (json) {
try {
// 尝试将json字符串转换为对象
return JSON.parse(json)
} catch (e) {
// 如果转换失败,打印错误信息
console.log('err', e.message)
}
// 返回一个空数组
return []
}

@ -15,6 +15,7 @@ const service = axios.create({
timeout: 6000 // 请求超时时间
})
// 错误处理函数
const err = (error) => {
if (error.response) {
const data = error.response.data

@ -6,17 +6,22 @@
* @param maxAge
*/
export const setStore = (name, content, maxAge = null) => {
// 如果没有全局window对象或者没有传入name则返回
if (!global.window || !name) {
return
}
// 如果content不是字符串则将其转换为字符串
if (typeof content !== 'string') {
content = JSON.stringify(content)
}
// 获取localStorage对象
const storage = global.window.localStorage
// 将content存储到localStorage中
storage.setItem(name, content)
// 如果maxAge存在且为数字则将过期时间存储到localStorage中
if (maxAge && !isNaN(parseInt(maxAge))) {
const timeout = parseInt(new Date().getTime() / 1000)
storage.setItem(`${name}_expire`, timeout + maxAge)
@ -30,13 +35,17 @@ export const setStore = (name, content, maxAge = null) => {
* @returns {*}
*/
export const getStore = name => {
// 如果没有全局window对象或者没有传入name则返回
if (!global.window || !name) {
return
}
// 获取localStorage中存储的content
const content = window.localStorage.getItem(name)
// 获取localStorage中存储的过期时间
const _expire = window.localStorage.getItem(`${name}_expire`)
// 如果过期时间存在,则判断当前时间是否超过过期时间
if (_expire) {
const now = parseInt(new Date().getTime() / 1000)
if (now > _expire) {
@ -44,9 +53,11 @@ export const getStore = name => {
}
}
// 尝试将content解析为JSON对象
try {
return JSON.parse(content)
} catch (e) {
// 如果解析失败则返回content
return content
}
}
@ -57,11 +68,14 @@ export const getStore = name => {
* @param name
*/
export const clearStore = name => {
// 如果没有全局window对象或者没有传入name则返回
if (!global.window || !name) {
return
}
// 从localStorage中移除content
window.localStorage.removeItem(name)
// 从localStorage中移除过期时间
window.localStorage.removeItem(`${name}_expire`)
}
@ -69,9 +83,11 @@ export const clearStore = name => {
* Clear all storage
*/
export const clearAll = () => {
// 如果没有全局window对象或者没有传入name则返回
if (!global.window || !name) {
return
}
// 清空localStorage
window.localStorage.clear()
}

@ -1,9 +1,11 @@
// 导出一个函数,用于获取当前时间并返回相应的问候语
export function timeFix () {
const time = new Date()
const hour = time.getHours()
return hour < 9 ? '早上好' : hour <= 11 ? '上午好' : hour <= 13 ? '中午好' : hour < 20 ? '下午好' : '晚上好'
}
// 导出一个函数,用于随机返回一个问候语
export function welcome () {
const arr = ['休息一会儿吧', '准备吃什么呢?', '要不要打一把 DOTA', '我猜你可能累了']
const index = Math.floor(Math.random() * arr.length)
@ -13,6 +15,7 @@ export function welcome () {
/**
* 触发 window.resize
*/
// 导出一个函数,用于触发 window.resize 事件
export function triggerWindowResizeEvent () {
const event = document.createEvent('HTMLEvents')
event.initEvent('resize', true, true)
@ -20,6 +23,7 @@ export function triggerWindowResizeEvent () {
window.dispatchEvent(event)
}
// 导出一个函数,用于监听页面滚动事件
export function handleScrollHeader (callback) {
let timer = 0
@ -50,6 +54,7 @@ export function handleScrollHeader (callback) {
* @param id parent element id or class
* @param timeout
*/
// 导出一个函数,用于移除加载动画
export function removeLoadingAnimate (id = '', timeout = 1500) {
if (id === '') {
return

@ -1,11 +1,14 @@
<template>
<!-- 这是 Vue 组件的模板部分 -->
<exception-page type="403" />
</template>
<script>
// components ExceptionPage
import { ExceptionPage } from '../../components'
export default {
// ExceptionPage
components: {
ExceptionPage
}

@ -1,11 +1,14 @@
<template>
<!-- 使用 ExceptionPage 组件来显示 404 错误页面 -->
<exception-page type="404" />
</template>
<script>
// ../../componentsExceptionPage
import { ExceptionPage } from '../../components'
// components
export default {
//
components: {
ExceptionPage
}
@ -13,5 +16,5 @@ export default {
</script>
<style scoped>
/* 此处写入组件的样式scoped属性确保样式仅在本组件生效 */
</style>

@ -1,10 +1,12 @@
<template>
<!-- 使用 ExceptionPage 组件来显示 500 错误页面 -->
<exception-page type="500" />
</template>
<script>
// ExceptionPage
import { ExceptionPage } from '../../components'
// components ExceptionPage
export default {
components: {
ExceptionPage
@ -13,5 +15,5 @@ export default {
</script>
<style scoped>
>/* 此处添加组件的样式,由于没有样式内容,所以留空 */
</style>

@ -1,24 +1,35 @@
<template>
<!-- 头部区域 -->
<div class="banner-wrapper">
<!-- 标题区域 -->
<div class="banner-title-wrapper">
<!-- 标题线动画效果 -->
<div class="title-line-wrapper" style="opacity: 1; transform: translate(0px, 0px);">
<div class="title-line" style="transform: translateX(-64px);"></div>
</div>
<!-- 主标题 -->
<h1 class="" style="opacity: 1; transform: translate(0px, 0px);">Online Exam</h1>
<!-- 副标题 -->
<p style="opacity: 1; transform: translate(0px, 0px);">
<span>基于SpringBoot+Vue技术栈开发的在线考试系统</span>
</p>
<!-- 按钮区域 -->
<div class="button-wrapper">
<!-- 预览按钮链接到GitHub页面 -->
<a href="https://github.com/19920625lsg">
<a-button type="primary">预览</a-button>
</a>
<!-- 开始使用按钮点击后导航到文档页面 -->
<a @click="$router.push({ name: 'docs' })">
<a-button style="margin: 0 16px;">开始使用</a-button>
</a>
</div>
</div>
<!-- 图片轮播区域 -->
<div class="banner-image-wrapper" style="opacity: 1;">
<!-- 图片轮播组件设置自动播放和显示箭头 -->
<a-carousel arrows autoplay>
<!-- 动态生成轮播图片 -->
<div v-for="i in 5" :key="i">
<img :src="`/home/cover${i}.jpg`" style="height: 324px"/>
</div>
@ -28,11 +39,12 @@
</template>
<script>
// 'Banner'
export default {
name: 'Banner'
}
</script>
//
<style lang="less">
@import "home";
</style>

@ -1,6 +1,8 @@
<template>
<!-- 使用 v-for 遍历 dataSource 数组为每个列表生成一个 ul 元素 -->
<div>
<ul class="page1-box-wrapper" v-for="(list, index) in dataSource" :key="index">
<!-- 使用 v-for 遍历当前列表为每个项生成一个 listItem 组件 -->
<template v-for="item in list">
<list-item :item="item" :key="item.title"/>
</template>
@ -9,14 +11,18 @@
</template>
<script>
// ListItem
import ListItem from './ListItem'
// List Vue
export default {
name: 'List',
components: {
// ListItem
ListItem
},
props: {
// dataSource
dataSource: {
type: Array,
requited: true,
@ -26,6 +32,7 @@ export default {
}
},
watch: {
// dataSource
dataSource (val) {
console.log('dataSource::update', val)
}
@ -34,5 +41,5 @@ export default {
</script>
<style scoped>
/* 此处添加组件的样式,使用 scoped 属性限制样式仅在当前组件生效 */
</style>

@ -1,19 +1,28 @@
<template>
<!-- 使用 :key 动态绑定列表项的唯一键值以优化渲染性能 -->
<li :key="item.title">
<div class="page1-box">
<!-- 用于装饰或布局的 div可能包含一些样式或动画效果 -->
<div class="page1-point-wrapper"></div>
<!-- 使用 :style 动态绑定图片框的阴影颜色增强视觉效果 -->
<div class="page1-image" :style="{ boxShadow: `${item.shadowColor} 0px 6px 12px` }">
<!-- 动态加载图片资源 -->
<img :src="item.src" />
</div>
<!-- 显示列表项的标题 -->
<h3>{{ item.title }}</h3>
<!-- 显示列表项的内容 -->
<p>{{ item.content }}</p>
</div>
</li>
</template>
<script>
// Vue ListItem
export default {
//
name: 'ListItem',
// item
props: {
item: {
type: Object,

@ -1,17 +1,26 @@
<template>
<!-- HomePage的主布局 -->
<div class="home-page page1">
<!-- 内容包装器用于承载页面元素 -->
<div class="home-page-wrapper" id="page1-wrapper">
<!-- 背景文本通过样式进行位置调整 -->
<div class="page1-bg" style="transform: translate(0px, 200.953px);">Feature</div>
<!-- 主标题描述页面核心内容 -->
<h2>What can <span>Online System</span> do for you </h2>
<!-- 标题下装饰线 -->
<div class="title-line-wrapper page1-line"></div>
<!-- 使用List组件展示特性列表 -->
<list :data-source="features" />
</div>
</div>
</template>
<script>
// List
import List from './List'
//
const featuresCN = [
//
{
title: '优雅美观',
content: '基于 Ant Design 体系精心设计',
@ -19,6 +28,7 @@ const featuresCN = [
color: '#13C2C2',
shadowColor: 'rgba(19,194,194,.12)'
},
//
{
title: '常见设计模式',
content: '提炼自中后台应用的典型页面和场景',
@ -26,6 +36,7 @@ const featuresCN = [
color: '#2F54EB',
shadowColor: 'rgba(47,84,235,.12)'
},
//
{
title: '最新技术栈',
content: '使用 Vue/vuex/antd 等前端前沿技术开发',
@ -33,6 +44,7 @@ const featuresCN = [
color: '#F5222D',
shadowColor: 'rgba(245,34,45,.12)'
},
//
{
title: '响应式',
content: '针对不同屏幕大小设计',
@ -40,6 +52,7 @@ const featuresCN = [
color: '#1AC44D',
shadowColor: 'rgba(26,196,77,.12)'
},
//
{
title: '最佳实践',
content: '良好的工程实践助你持续产出高质量代码',
@ -47,6 +60,7 @@ const featuresCN = [
color: '#FA8C16',
shadowColor: 'rgba(250,140,22,.12)'
},
// UI
{
title: 'UI 测试',
content: '自动化测试保障前端产品质量',
@ -57,19 +71,25 @@ const featuresCN = [
]
export default {
//
name: 'Page1',
//
components: {
List
},
//
data () {
return {
features: featuresCN
}
},
//
created () {
this.updateFeatures()
},
//
methods: {
// 便
updateFeatures () {
const arr = featuresCN
const newArr = [[], [], []]

@ -1,18 +1,28 @@
<template>
<div class="card-list" ref="content">
<!-- 使用a-list组件设置grid属性gutter为24lg为3md为2sm为1xs为1 -->
<a-list
:grid="{gutter: 24, lg: 3, md: 2, sm: 1, xs: 1}"
:dataSource="dataSource"
>
<!-- 使用a-list-item组件设置slot为renderItemslot-scope为item -->
<a-list-item slot="renderItem" slot-scope="item">
<!-- 使用a-card组件设置hoverable为true点击事件为joinExam -->
<a-card :hoverable="true" @click="joinExam(item.id)">
<!-- 使用a-card-meta组件设置slot为titleavatardescription -->
<a-card-meta>
<!-- 设置title为item.title -->
<div style="margin-bottom: 3px" slot="title">{{ item.title }}</div>
<!-- 设置avatar为item.avatar使用imgSrcFilter过滤器 -->
<a-avatar class="card-avatar" slot="avatar" :src="item.avatar | imgSrcFilter" size="large" />
<!-- 设置description为item.content -->
<div class="meta-content" slot="description">{{ item.content }}</div>
</a-card-meta>
<!-- 使用template组件设置slot为actions -->
<template class="ant-card-actions" slot="actions">
<!-- 设置a为满分item.score分 -->
<a>满分{{ item.score }}</a>
<!-- 设置a为限时item.elapse分钟 -->
<a>限时{{ item.elapse }}分钟</a>
</template>
</a-card>
@ -34,6 +44,7 @@ export default {
}
},
methods: {
//
joinExam (id) {
const routeUrl = this.$router.resolve({
path: `/exam/${id}`

@ -85,15 +85,15 @@ export default {
},
data () {
return {
//
//
examDetail: {},
// id, currentQuestion(answersidids),
// id, currentQuestion(answersidids),
answersMap: {},
//
currentQuestion: '',
// answersMap
// answersMap
radioValue: '',
// answersMap
// answersMap
checkValues: [],
optionStyle: {
display: 'block',
@ -106,7 +106,7 @@ export default {
mounted () {
this.answersMap = new Map()
const that = this
//
//
getExamDetail(this.$route.params.id)
.then(res => {
if (res.code === 0) {
@ -122,26 +122,26 @@ export default {
})
},
methods: {
// ,
//
...mapGetters(['nickname', 'avatar']),
getQuestionDetail (questionId) {
// content
// content
const that = this
//
//
this.radioValue = ''
this.checkValues = []
getQuestionDetail(questionId)
.then(res => {
if (res.code === 0) {
//
//
that.currentQuestion = res.data
// answersMapid
// answersMapid
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(that.checkValues, that.answersMap.get(that.currentQuestion.id))
}
}

@ -1,7 +1,11 @@
<template>
<!-- 使用Ant Design的a-card组件作为外层容器设置无边框 -->
<a-card :bordered="false">
<!-- 定义操作按钮的工具栏区域 -->
<div id="toolbar">
<!-- 新建按钮类型为主要按钮primary带有加号图标点击时调用$refs.createQuestionModal.create()方法来触发新建操作 -->
<a-button type="primary" icon="plus" @click="$refs.createQuestionModal.create()"></a-button>&nbsp;
<!-- 全量刷新按钮类型为主要按钮primary带有刷新图标点击时调用loadAll()方法来重新加载所有题目数据 -->
<a-button type="primary" icon="reload" @click="loadAll()"></a-button>
</div>
<BootstrapTable
@ -12,8 +16,11 @@
/>
<!-- ref是为了方便用this.$refs.modal直接引用下同 -->
<step-by-step-question-modal ref="createQuestionModal" @ok="handleOk" />
<!-- 更新题目模态框 -->
<summernote-update-modal ref="questionUpdateModal" @ok="handleOk" />
<!-- 查看题目模态框 -->
<question-view-modal ref="modalView" @ok="handleOk" />
<!-- 编辑题目模态框 -->
<question-edit-modal ref="modalEdit" @ok="handleOk" />
</a-card>
</template>
@ -28,6 +35,15 @@ import SummernoteUpdateModal from '@views/list/modules/SummernoteUpdateModal'
import $ from 'jquery'
export default {
/**
* 名称QuestionTableList 组件
* 描述此组件用于显示问题表格列表并提供对问题进行详细操作的功能
* 组件包含
* - SummernoteUpdateModal: 用于更新问题描述的模态框
* - StepByStepQuestionModal: 用于逐步解答问题的模态框
* - QuestionViewModal: 用于查看问题详情的模态框
* - QuestionEditModal: 用于编辑问题的模态框
*/
name: 'QuestionTableList',
components: {
SummernoteUpdateModal,
@ -44,69 +60,113 @@ export default {
title: '序号',
field: 'serial',
formatter: function (value, row, index) {
//
// :
// value:
// row:
// index:
return index + 1 // 1
//
}
},
//
{
//
title: '题干',
//
field: 'name',
//
width: 200,
formatter: (value, row) => {
// HTMLdiv
return '<div class="question-name" style="height: 100%;width: 100%">' + value + '</div>'
},
//
events: {
'click .question-name': function (e, value, row, index) {
//
that.$refs.questionUpdateModal.edit('summernote-question-name-update', row, 'name', '更新题干', questionUpdate)
}
}
},
{
//
title: '解析',
//
field: 'description',
//
width: 200,
formatter: (value, row) => {
// question-descdiv
return '<div class="question-desc">' + value + '</div>'
},
//
events: {
// question-desc
'click .question-desc': function (e, value, row, index) {
that.$refs.questionUpdateModal.edit('summernote-question-desc-update', row, 'description', '更新题目解析', questionUpdate)
}
}
},
{
//
title: '分数',
//
field: 'score',
//
formatter: (value, row) => {
// div便
return '<div class="question-score">' + value + '</div>'
},
//
events: {
//
'click .question-score': function (e, value, row, index) {
// jQuery便使jQuery
const $element = $(e.target) // html
$element.html('<input type="text" value="' + value + '">')
}
}
},
{
/**
* 配置表格列标题和数据字段
*
* @param {String} title - 表格列的标题用于显示在表头
* @param {String} field - 表格列的数据字段用于绑定数据源中的属性
*/
title: '创建人',
field: 'creator'
},
{
//
title: '难度',
//
field: 'level',
formatter: (value, row) => {
//
// value:
// row:
return '<div class="question-level">' + value + '</div>'
},
events: {
'click .question-level': function (e, value, row, index) {
//
// e:
// value:
// row:
// index:
const $element = $(e.target) // html
if ($element.children().length > 0) return //
getQuestionSelection().then(res => {
//
console.log(res)
if (res.code === 0) {
console.log(res.data)
const levels = res.data.levels
let inner = '<select>'
for (let i = 0; i < levels.length; i++) {
//
if (levels[i].description === value) {
//
inner += '<option value ="' + levels[i].id + '" name="' + levels[i].name + '" selected="selected">' + levels[i].description + '</option>'
@ -117,6 +177,7 @@ export default {
inner += '</select>'
$element.html(inner)
} else {
//
that.$notification.error({
message: '获取问题下拉选项失败',
description: res.msg
@ -127,16 +188,27 @@ export default {
}
},
{
//
title: '题型',
//
field: 'type',
formatter: (value, row) => {
//
// value:
// row:
return '<div class="question-type">' + value + '</div>'
},
events: {
'click .question-type': function (e, value, row, index) {
//
// e:
// value:
// row:
// index:
const $element = $(e.target) // html
if ($element.children().length > 0) return //
getQuestionSelection().then(res => {
//
console.log(res)
if (res.code === 0) {
console.log(res.data)
@ -163,21 +235,27 @@ export default {
}
},
{
//
title: '学科',
//
field: 'category',
formatter: (value, row) => {
//
// HTML
return '<div class="question-category">' + value + '</div>'
},
events: {
'click .question-category': function (e, value, row, index) {
const $element = $(e.target) // html
if ($element.children().length > 0) return //
//
getQuestionSelection().then(res => {
console.log(res)
if (res.code === 0) {
console.log(res.data)
const categories = res.data.categories
let inner = '<select>'
// <option>
for (let i = 0; i < categories.length; i++) {
if (categories[i].name === value) { //
//
@ -189,6 +267,7 @@ export default {
inner += '</select>'
$element.html(inner)
} else {
//
that.$notification.error({
message: '获取问题下拉选项失败',
description: res.msg
@ -199,22 +278,32 @@ export default {
}
},
{
//
title: '更新时间',
//
field: 'updateTime'
},
{
//
title: '操作',
//
field: 'action',
//
align: 'center',
//
formatter: (value, row) => {
// HTML
return '<button type="button" class="btn btn-success view-question">详情</button>' +
'&nbsp;&nbsp;' +
'<button type="button" class="btn btn-success edit-question">编辑</button>'
},
//
events: {
//
'click .view-question': function (e, value, row, index) {
that.handleSub(row)
},
//
'click .edit-question': function (e, value, row, index) {
that.handleEdit(row)
}
@ -223,14 +312,20 @@ export default {
],
tableData: [], // bootstrap-table
// custom bootstrap-table
// bootstrap-table
options: {
//
search: true,
//
showColumns: true,
//
showExport: true,
//
pagination: true,
toolbar: '#toolbar',
//
advancedSearch: true,
//
idTable: 'advancedTable',
// http://www.itxst.com/bootstrap-table-events/tutorial.html
// onClickRow: that.clickRow,
@ -243,17 +338,35 @@ export default {
this.loadAll() //
},
methods: {
/**
* 编辑问题
* @param {Object} record - 问题记录
*/
handleEdit (record) {
this.$refs.modalEdit.edit(record)
},
handleSub (record) {
//
/**
* 查看问题
* @param {Object} record - 问题记录
*/
console.log(record)
this.$refs.modalView.edit(record)
},
/**
* 确认操作后重新加载数据
*/
handleOk () {
this.loadAll() //
},
/**
* 双击表格单元格进行编辑
* @param {String} field - 字段名
* @param {String} value - 字段值
* @param {Object} row - 行记录
* @param {Object} $element - DOM元素
*/
dblClickCell (field, value, row, $element) {
if (field === 'score') { //
const childrenInput = $element.children('.question-score').children('input') //
@ -274,18 +387,24 @@ export default {
}
if (field === 'level') { //
//
const childrenSelect = $element.children('.question-level').children('select') //
if (childrenSelect.length === 0) return
//
const optionSelected = $(childrenSelect[0]).find('option:selected')
// ID
row.levelId = optionSelected.val()
console.log(row.levelId)
// API
row.level = optionSelected.text()
//
console.log(row.level)
const that = this
questionUpdate(row).then(res => {
//
console.log(res)
if (res.code === 0) {
//
$element.children('.question-level').text(row.level)
that.$notification.success({
message: '更新成功',
@ -296,17 +415,24 @@ export default {
}
if (field === 'type') { //
//
const childrenSelect = $element.children('.question-type').children('select') //
if (childrenSelect.length === 0) return
//
const optionSelected = $(childrenSelect[0]).find('option:selected')
// rowtypeIdtype
row.typeId = optionSelected.val()
row.type = optionSelected.text()
// this便promise使
const that = this
// questionUpdate
questionUpdate(row).then(res => {
//
console.log(res)
if (res.code === 0) {
//
$element.children('.question-type').text(row.type)
//
that.$notification.success({
message: '更新成功',
description: '更新成功'
@ -316,18 +442,24 @@ export default {
}
if (field === 'category') { //
//
const childrenSelect = $element.children('.question-category').children('select') //
console.log(childrenSelect)
if (childrenSelect.length === 0) return
//
const optionSelected = $(childrenSelect[0]).find('option:selected')
// ID
row.categoryId = optionSelected.val()
row.category = optionSelected.text()
const that = this
// API
questionUpdate(row).then(res => {
//
console.log(res)
if (res.code === 0) {
//
$element.children('.question-category').text(row.category)
//
that.$notification.success({
message: '更新成功',
description: '更新成功'
@ -336,14 +468,24 @@ export default {
})
}
},
/**
* 加载所有问题数据
* 此方法通过调用后端API来获取所有问题的列表并将其用于更新表格数据
*/
loadAll () {
// 使this
const that = this
// API
getQuestionAll()
.then(res => {
//
if (res.code === 0) {
//
that.tableData = res.data
//
that.$refs.table._initTable()
} else {
//
that.$notification.error({
message: '获取全部问题的列表失败',
description: res.msg

@ -1,6 +1,9 @@
<!-- 问题编辑模态框组件 -->
<template>
<!-- 模态框主体 -->
<a-modal title="编辑题目" :width="640" :visible="visible" :confirmLoading="confirmLoading" @cancel="handleCancel">
<a-spin :spinning="confirmLoading">
<!-- 问题编辑表单 -->
<a-form :form="form">
<h3><b>题干</b></h3>
<div id="summernote-question-name-edit" />
@ -48,6 +51,7 @@
<div id="summernote-question-desc-edit" />
</a-form>
</a-spin>
<!-- 模态框底部按钮区域 -->
<template slot="footer">
<a-button key="cancel" @click="handleCancel"></a-button>
<a-button key="update" type="primary" @click="handleUpdate"></a-button>
@ -56,6 +60,7 @@
</template>
<script>
// API
import '../../../plugins/summernote'
import $ from 'jquery'
import { questionUpdate } from '../../../api/exam'
@ -65,10 +70,13 @@ export default {
name: 'QuestionEditModal',
data () {
return {
//
visible: false,
//
size: 'default',
//
confirmLoading: false,
//
form: this.$form.createForm(this),
//
question: {},
@ -85,7 +93,7 @@ export default {
desc: ''
}
},
//
updated () {
this.initSummernote('summernote-question-name-edit')
this.initSummernote('summernote-question-desc-edit')
@ -93,6 +101,7 @@ export default {
this.setSummernoteContent('summernote-question-desc-edit', this.desc)
},
methods: {
//
initSummernote (divId) {
console.log('初始化富文本插件:' + divId)
$('#' + divId).summernote({
@ -120,9 +129,11 @@ export default {
}
})
},
//
getSummernoteContent (divId) {
return $('#' + divId).summernote('code')
},
//
setSummernoteContent (divId, content) {
return $('#' + divId).summernote('code', content)
},
@ -166,7 +177,7 @@ export default {
}
console.log(`Selected: ${value}`)
},
//
handleMultiChange (values) {
console.log(values)
// id
@ -191,11 +202,11 @@ export default {
}
}
},
//
popupScroll () {
console.log('popupScroll')
},
//
handleUpdate () {
const that = this
that.question.name = that.getSummernoteContent('summernote-question-name-edit')

@ -1,24 +1,32 @@
<template>
<!-- 题目信息模态框 -->
<a-modal title="题目信息" :width="640" :visible="visible" :confirmLoading="confirmLoading" @cancel="handleCancel">
<!-- 加载状态 -->
<a-spin :spinning="confirmLoading">
<!-- 表单 -->
<a-form :form="form">
<!-- 题干 -->
<h3><b>题干</b></h3>
<div v-html="question.name"></div>
<br>
<!-- 选项 -->
<h3><b>选项</b></h3>
<ul>
<li v-for="option in question.options" :key="option.id" v-html="option.content"/>
</ul>
<br>
<!-- 答案 -->
<h3><b>答案</b></h3>
<ul>
<li v-for="option in question.options" :key="option.id" v-show="option.answer===true" v-html="option.content"/>
</ul>
<br>
<!-- 解析 -->
<h3><b>解析</b></h3>
<div v-html="question.description"></div>
</a-form>
</a-spin>
<!-- 模态框底部按钮 -->
<template slot="footer">
<a-button key="cancel" @click="handleCancel"></a-button>
</template>
@ -32,23 +40,28 @@ export default {
name: 'QuestionViewModal',
data () {
return {
//
visible: false,
//
confirmLoading: false,
//
form: this.$form.createForm(this),
//
question: {},
//
options: [],
//
answerOption: ''
}
},
methods: {
//
edit (record) {
this.visible = true
// data
this.question = record
},
//
handleCancel () {
// clear form & currentStep
this.visible = false

Loading…
Cancel
Save