Merge pull request"更新注释文档"(#11) from feature/wy into develop

feature/wyh
王勇 3 days ago committed by 王奕辉
parent db8aaf463e
commit e8b4b16459

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

@ -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,7 +11,9 @@ 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> {
List<Question> findByQuestionTypeId(Integer id);
@Query("select q from Question q order by q.updateTime desc")

@ -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> {
/**
*

@ -47,7 +47,12 @@ public class UserServiceImpl implements UserService {
@Autowired
ActionRepository actionRepository;
/**
*
*
* @param registerDTO
* @return null
*/
@Override
public User register(RegisterDTO registerDTO) {
try {
@ -106,7 +111,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 +125,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;
}

@ -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,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,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,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