Compare commits

..

7 Commits

@ -0,0 +1,94 @@
<template>
<el-select
v-model="currentValue"
:multiple="multi"
:remote-method="fetchData"
filterable
remote
clearable
placeholder="选择或搜索考试"
class="filter-item"
@change="handlerChange"
>
<el-option
v-for="item in dataList"
:key="item.id"
:label="item.title"
:value="item.id"
/>
</el-select>
</template>
<script>
import { fetchList } from '@/api/exam/exam'
export default {
name: 'ExamSelect',
props: {
// /**
// *
// */
multi: Boolean,
// /**
// *
// */
value: Array,
// /**
// * valueArray
// */
default: String
},
data() {
return {
// /**
// *
// */
dataList: [],
// /**
// *
// */
currentValue: []
}
},
watch: {
// /**
// * valuevaluecurrentValue
// */
value: {
handler() {
this.currentValue = this.value
}
}
},
created() {
// /**
// * currentValuefetchData
// */
this.currentValue = this.value
this.fetchData()
},
methods: {
// /**
// *
// */
fetchData() {
fetchList().then(response => {
this.dataList = response.data.records
})
},
// /**
// *
// * @param e
// */
handlerChange(e) {
console.log(e)
this.$emit('change', e)
this.$emit('input', e)
}
}
}
</script>

@ -0,0 +1,305 @@
package com.yf.exam.modules.qu.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.google.common.collect.Lists;
import com.yf.exam.core.api.ApiRest;
import com.yf.exam.core.api.controller.BaseController;
import com.yf.exam.core.api.dto.BaseIdReqDTO;
import com.yf.exam.core.api.dto.BaseIdRespDTO;
import com.yf.exam.core.api.dto.BaseIdsReqDTO;
import com.yf.exam.core.api.dto.PagingReqDTO;
import com.yf.exam.core.exception.ServiceException;
import com.yf.exam.core.utils.BeanMapper;
import com.yf.exam.core.utils.excel.ExportExcel;
import com.yf.exam.core.utils.excel.ImportExcel;
import com.yf.exam.modules.qu.dto.QuDTO;
import com.yf.exam.modules.qu.dto.export.QuExportDTO;
import com.yf.exam.modules.qu.dto.ext.QuDetailDTO;
import com.yf.exam.modules.qu.dto.request.QuQueryReqDTO;
import com.yf.exam.modules.qu.entity.Qu;
import com.yf.exam.modules.qu.service.QuService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
///**
// * <p>
// * 问题题目控制器
// * </p>
// *
// * @author 聪明笨狗
// * @since 2020-05-25 13:25
// */
@Api(tags={"问题题目"})
@RestController
@RequestMapping("/exam/api/qu/qu")
public class QuController extends BaseController {
@Autowired
private QuService baseService;
// /**
// * 添加或修改问题题目
// *
// * @param reqDTO 包含问题题目的详细信息
// * @return ApiRest 对象,包含操作结果信息
// */
@RequiresRoles("sa")
@ApiOperation(value = "添加或修改问题题目")
@RequestMapping(value = "/save", method = {RequestMethod.POST})
public ApiRest<BaseIdRespDTO> save(@RequestBody QuDetailDTO reqDTO) {
baseService.save(reqDTO);
return super.success();
}
// /**
// * 批量删除问题题目
// *
// * @param reqDTO 包含要删除的问题题目的ID列表
// * @return ApiRest 对象,包含操作结果信息
// */
@RequiresRoles("sa")
@ApiOperation(value = "批量删除问题题目")
@RequestMapping(value = "/delete", method = {RequestMethod.POST})
public ApiRest edit(@RequestBody BaseIdsReqDTO reqDTO) {
// 根据ID删除问题题目
baseService.delete(reqDTO.getIds());
return super.success();
}
// /**
// * 查找问题题目的详情
// *
// * @param reqDTO 包含要查询的问题题目的ID
// * @return ApiRest 对象,包含问题题目的详细信息
// */
@ApiOperation(value = "查找问题题目的详情")
@RequestMapping(value = "/detail", method = {RequestMethod.POST})
public ApiRest<QuDetailDTO> detail(@RequestBody BaseIdReqDTO reqDTO) {
QuDetailDTO dto = baseService.detail(reqDTO.getId());
return super.success(dto);
}
// /**
// * 分页查找问题题目
// *
// * @param reqDTO 包含分页查询的条件
// * @return ApiRest 对象,包含分页后的问题题目列表
// */
@RequiresRoles("sa")
@ApiOperation(value = "分页查找问题题目")
@RequestMapping(value = "/paging", method = {RequestMethod.POST})
public ApiRest<IPage<QuDTO>> paging(@RequestBody PagingReqDTO<QuQueryReqDTO> reqDTO) {
// 分页查询并转换为DTO对象
IPage<QuDTO> page = baseService.paging(reqDTO);
return super.success(page);
}
// /**
// * 导出Excel文件
// *
// * @param response HttpServletResponse 对象用于处理HTTP响应
// * @param reqDTO 包含查询条件的请求DTO对象
// * @return ApiRest 对象,包含导出结果信息
// */
@RequiresRoles("sa")
@ResponseBody
@RequestMapping(value = "/export")
public ApiRest exportFile(HttpServletResponse response, @RequestBody QuQueryReqDTO reqDTO) {
// 导出文件名
String fileName = "导出的试题-" + System.currentTimeMillis() + ".xlsx";
try {
int no = 0;
String quId = "";
// 获取要导出的问题列表
List<QuExportDTO> list = baseService.listForExport(reqDTO);
for (QuExportDTO item : list) {
// 如果QId不同则增加题目序号
if (!quId.equals(item.getQId())) {
quId = item.getQId();
no += 1;
} else {
// 如果QId相同则清空部分重复信息
item.setQuType("0");
item.setQContent("");
item.setQAnalysis("");
item.setRepoList(null);
item.setQImage("");
item.setQVideo("");
}
item.setNo(String.valueOf(no));
}
// 导出Excel文件
new ExportExcel("试题", QuExportDTO.class).setDataList(list).write(response, fileName).dispose();
return super.success();
} catch (Exception e) {
return failure(e.getMessage());
}
}
// /**
// * 导入Excel文件
// *
// * @param file 上传的Excel文件
// * @return ApiRest 对象,包含导入结果信息
// */
@RequiresRoles("sa")
@ResponseBody
@RequestMapping(value = "/import")
public ApiRest importFile(@RequestParam("file") MultipartFile file) {
try {
// 创建ImportExcel对象读取文件从第二行开始不使用表头
ImportExcel ei = new ImportExcel(file, 1, 0);
// 获取Excel文件中的数据列表并转换为QuExportDTO对象
List<QuExportDTO> list = ei.getDataList(QuExportDTO.class);
// 校验Excel数据的有效性
this.checkExcel(list);
// 导入数据条数
baseService.importExcel(list);
// 导入成功
return super.success();
} catch (IOException e) {
// 处理文件读取异常
} catch (InvalidFormatException e) {
// 处理Excel文件格式无效异常
} catch (IllegalAccessException e) {
// 处理数据访问异常
} catch (InstantiationException e) {
// 处理实例化异常
}
return super.failure();
}
// /**
// * 校验Excel数据的有效性
// *
// * @param list 包含从Excel导入的问题数据列表
// * @throws ServiceException 如果数据校验失败抛出ServiceException异常
// */
private void checkExcel(List<QuExportDTO> list) throws ServiceException {
// 约定第三行开始导入
int line = 3;
StringBuffer sb = new StringBuffer();
if (CollectionUtils.isEmpty(list)) {
throw new ServiceException(1, "您导入的数据似乎是一个空表格!");
}
Integer quNo = null;
for (QuExportDTO item : list) {
System.out.println(item.getNo());
if (StringUtils.isBlank(item.getNo())) {
line++;
continue;
}
System.out.println(item.getQContent());
Integer no;
try {
no = Integer.parseInt(item.getNo());
} catch (Exception e) {
line++;
continue;
}
if (no == null) {
sb.append("第" + line + "行,题目序号不能为空!<br>");
}
if (quNo == null || !quNo.equals(no)) {
if (item.getQuType() == null) {
sb.append("第" + line + "行,题目类型不能为空<br>");
}
if (StringUtils.isBlank(item.getQContent())) {
sb.append("第" + line + "行,题目内容不能为空<br>");
}
if (CollectionUtils.isEmpty(item.getRepoList())) {
sb.append("第" + line + "行,题目必须包含一个题库<br>");
}
}
if (StringUtils.isBlank(item.getAIsRight())) {
sb.append("第" + line + "行,选项是否正确不能为空<br>");
}
if (StringUtils.isBlank(item.getAContent()) && StringUtils.isBlank(item.getAImage())) {
sb.append("第" + line + "行,选项内容和选项图片必须有一个不为空<br>");
}
quNo = no;
line++;
}
// 如果存在错误信息抛出ServiceException异常
if (!"".equals(sb.toString())) {
throw new ServiceException(1, sb.toString());
}
}
// /**
// * 下载导入试题数据模板
// *
// * @param response HttpServletResponse 对象用于处理HTTP响应
// * @return ApiRest 对象,包含下载结果信息
// */
@ResponseBody
@RequestMapping(value = "import/template")
public ApiRest importFileTemplate(HttpServletResponse response) {
try {
String fileName = "试题导入模板.xlsx";
List<QuExportDTO> list = Lists.newArrayList();
// 创建模板数据
QuExportDTO l1 = new QuExportDTO();
l1.setNo("正式导入,请删除此说明行:数字,相同的数字表示同一题的序列");
l1.setQContent("问题内容");
l1.setQAnalysis("整个问题的解析");
l1.setQuType("只能填写1、2、3、41表示单选题2表示多选题3表示判断题4表示主观题");
l1.setQImage("题目图片完整URL多个用逗号隔开限制10个");
l1.setQVideo("题目视频完整URL只限一个");
l1.setAImage("答案图片完整URL只限一个");
l1.setRepoList(Arrays.asList(new String[]{"已存在题库的ID多个用逗号隔开题库ID错误无法导入"}));
l1.setAContent("候选答案1");
l1.setAIsRight("只能填写0或10表示否1表示是");
l1.setAAnalysis("这个项是正确的");
QuExportDTO l2 = new QuExportDTO();
l2.setQContent("找出以下可以被2整除的数多选");
l2.setQAnalysis("最基本的数学题,不做过多解析");
l2.setQuType("2");
l2.setNo("1");
l2.setAIsRight("1");
l2.setAContent("数字2");
l2.setAAnalysis("2除以2=1对的");
QuExportDTO l3 = new QuExportDTO();
l3.setNo("1");
l3.setAIsRight("0");
l3.setAContent("数字3");
l3.setAAnalysis("3除以2=1.5,不能被整除");
QuExportDTO l4 = new QuExportDTO();
l4.setNo("1");
l4.setAIsRight("1");
l4.setAContent("数字6");
l4.setAAnalysis("6除以2=3对的");
list.add(l1);
list.add(l2);
list.add(l3);
list.add(l4);
// 导出Excel模板文件
new ExportExcel("试题数据", QuExportDTO.class, 1).setDataList(list).write(response, fileName).dispose();
return super.success();
} catch (Exception e) {
return super.failure("导入模板下载失败!失败信息:" + e.getMessage());
}
}
}

@ -0,0 +1,56 @@
package com.yf.exam.modules.qu.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
///**
// * <p>
// * 候选答案请求类
// * </p>
// *
// * @author 聪明笨狗
// * @since 2020-05-25 13:23
// */
@Data
@ApiModel(value="候选答案", description="候选答案")
public class QuAnswerDTO implements Serializable {
private static final long serialVersionUID = 1L;
// /**
// * 答案的唯一标识ID
// */
@ApiModelProperty(value = "答案ID", required=true)
private String id;
// /**
// * 所属问题的唯一标识ID
// */
@ApiModelProperty(value = "问题ID", required=true)
private String quId;
// /**
// * 表示该答案是否正确
// */
@ApiModelProperty(value = "是否正确", required=true)
private Boolean isRight;
// /**
// * 选项的图片URL
// */
@ApiModelProperty(value = "选项图片", required=true)
private String image;
// /**
// * 答案的具体内容
// */
@ApiModelProperty(value = "答案内容", required=true)
private String content;
// /**
// * 对该答案的分析说明
// */
@ApiModelProperty(value = "答案分析", required=true)
private String analysis;
}

@ -0,0 +1,75 @@
package com.yf.exam.modules.qu.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
// * <p>
// * 问题题目请求类
// * </p>
// *
// * @author 聪明笨狗
// * @since 2020-05-25 13:23
// */
@Data
@ApiModel(value="问题题目", description="问题题目")
public class QuDTO implements Serializable {
private static final long serialVersionUID = 1L;
// /**
// * 题目的唯一标识ID
// */
@ApiModelProperty(value = "题目ID", required=true)
private String id;
// /**
// * 题目的类型
// */
@ApiModelProperty(value = "题目类型", required=true)
private Integer quType;
// /**
// * 题目的难度级别1表示普通2表示较难
// */
@ApiModelProperty(value = "1普通,2较难", required=true)
private Integer level;
// /**
// * 题目的图片URL
// */
@ApiModelProperty(value = "题目图片", required=true)
private String image;
// /**
// * 题目的具体内容
// */
@ApiModelProperty(value = "题目内容", required=true)
private String content;
// /**
// * 题目的创建时间
// */
@ApiModelProperty(value = "创建时间", required=true)
private Date createTime;
// /**
// * 题目的更新时间
// */
@ApiModelProperty(value = "更新时间", required=true)
private Date updateTime;
// /**
// * 题目的备注信息
// */
@ApiModelProperty(value = "题目备注", required=true)
private String remark;
// /**
// * 题目的整题解析
// */
@ApiModelProperty(value = "整题解析", required=true)
private String analysis;
}

@ -0,0 +1,49 @@
package com.yf.exam.modules.qu.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
///**
// * <p>
// * 试题题库请求类
// * </p>
// *
// * @author 聪明笨狗
// * @since 2020-05-25 13:23
// */
@Data
@ApiModel(value="试题题库", description="试题题库")
public class QuRepoDTO implements Serializable {
private static final long serialVersionUID = 1L;
// /**
// * 题库记录的唯一标识ID
// */
private String id;
// /**
// * 关联试题的唯一标识ID
// */
@ApiModelProperty(value = "试题", required=true)
private String quId;
// /**
// * 归属题库的唯一标识ID
// */
@ApiModelProperty(value = "归属题库", required=true)
private String repoId;
// /**
// * 试题的类型
// */
@ApiModelProperty(value = "题目类型", required=true)
private Integer quType;
// /**
// * 试题在题库中的排序顺序
// */
@ApiModelProperty(value = "排序", required=true)
private Integer sort;
}

@ -0,0 +1,86 @@
package com.yf.exam.modules.qu.dto.export;
import com.yf.exam.core.utils.excel.annotation.ExcelField;
import com.yf.exam.core.utils.excel.fieldtype.ListType;
import lombok.Data;
import java.util.List;
///**
// * 用于导出的数据结构
// * @author bool
// */
@Data
public class QuExportDTO {
private static final long serialVersionUID = 1L;
//
// /**
// * 问题题目的唯一标识ID
// */
private String qId;
// /**
// * 题目序号
// */
@ExcelField(title="题目序号", align=2, sort=1)
private String no;
// /**
// * 题目类型
// */
@ExcelField(title="题目类型", align=2, sort=2)
private String quType;
// /**
// * 题目内容
// */
@ExcelField(title="题目内容", align=2, sort=3)
private String qContent;
// /**
// * 整体解析
// */
@ExcelField(title="整体解析", align=2, sort=4)
private String qAnalysis;
// /**
// * 题目图片完整URL多个用逗号隔开限制10个
// */
@ExcelField(title="题目图片", align=2, sort=5)
private String qImage;
//
// /**
// * 题目视频完整URL只限一个
// */
@ExcelField(title="题目视频", align=2, sort=6)
private String qVideo;
// /**
// * 所属题库的ID列表
// */
@ExcelField(title="所属题库", align=2, sort=7, fieldType = ListType.class)
private List<String> repoList;
// /**
// * 是否为正确项0表示否1表示是
// */
@ExcelField(title="是否正确项", align=2, sort=8)
private String aIsRight;
//
// /**
// * 选项内容
// */
@ExcelField(title="选项内容", align=2, sort=9)
private String aContent;
// /**
// * 选项解析
// */
@ExcelField(title="选项解析", align=2, sort=10)
private String aAnalysis;
// /**
// * 选项图片完整URL只限一个
// */
@ExcelField(title="选项图片", align=2, sort=11)
private String aImage;
}

@ -0,0 +1,44 @@
package com.yf.exam.modules.qu.dto.export;
import com.yf.exam.modules.qu.dto.QuAnswerDTO;
import lombok.Data;
import java.util.List;
///**
// * 用于导入的数据结构
// * @author bool
// */
@Data
public class QuImportDTO {
private static final long serialVersionUID = 1L;
// /**
// * 题目类型
// */
private String quType;
// /**
// * 题目内容
// */
private String qContent;
// /**
// * 整体解析
// */
private String qAnalysis;
// /**
// * 题目图片完整URL
// */
private String qImage;
// /**
// * 所属题库名称
// */
private String repoName;
// /**
// * 答案列表
// */
private List<QuAnswerDTO> answerList;
}

@ -0,0 +1,34 @@
package com.yf.exam.modules.qu.dto.ext;
import com.yf.exam.modules.qu.dto.QuAnswerDTO;
import com.yf.exam.modules.qu.dto.QuDTO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
///**
// * <p>
// * 问题题目请求类
// * </p>
// *
// * @author 聪明笨狗
// * @since 2020-05-25 13:23
// */
@Data
@ApiModel(value="问题题目详情", description="问题题目详情")
public class QuDetailDTO extends QuDTO {
private static final long serialVersionUID = 1L;
// /**
// * 备选项列表
// */
@ApiModelProperty(value = "备选项列表", required=true)
private List<QuAnswerDTO> answerList;
// /**
// * 题库ID列表
// */
@ApiModelProperty(value = "题库列表", required=true)
private List<String> repoIds;
}

@ -0,0 +1,45 @@
package com.yf.exam.modules.qu.dto.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
///**
// * <p>
// * 题目查询请求类
// * </p>
// *
// * @author 聪明笨狗
// * @since 2020-05-25 13:23
// */
@Data
@ApiModel(value="题目查询请求类", description="题目查询请求类")
public class QuQueryReqDTO implements Serializable {
private static final long serialVersionUID = 1L;
// /**
// * 题目类型
// */
@ApiModelProperty(value = "题目类型")
private Integer quType;
// /**
// * 归属题库ID列表
// */
@ApiModelProperty(value = "归属题库")
private List<String> repoIds;
// /**
// * 题目内容的关键字
// */
@ApiModelProperty(value = "题目内容")
private String content;
// /**
// * 需要排除的题目ID列表
// */
@ApiModelProperty(value = "排除ID列表")
private List<String> excludes;
}

@ -0,0 +1,39 @@
package com.yf.exam.modules.qu.dto.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
///**
// * <p>
// * 试题题库批量操作类
// * </p>
// *
// * @author 聪明笨狗
// * @since 2020-05-25 13:23
// */
@Data
@ApiModel(value="试题题库批量操作类", description="试题题库批量操作类")
public class QuRepoBatchReqDTO implements Serializable {
private static final long serialVersionUID = 1L;
// /**
// * 题目ID列表
// */
@ApiModelProperty(value = "题目ID", required=true)
private List<String> quIds;
//
// /**
// * 题库ID列表
// */
@ApiModelProperty(value = "题目类型", required=true)
private List<String> repoIds;
// /**
// * 是否移除题目false表示新增true表示移除
// */
@ApiModelProperty(value = "是否移除,否就新增;是就移除", required=true)
private Boolean remove;
}

@ -0,0 +1,75 @@
package com.yf.exam.modules.qu.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import java.util.Date;
///**
//* <p>
//* 问题题目实体类
//* </p>
//*
//* @author 聪明笨狗
//* @since 2020-05-25 13:23
//*/
@Data
@TableName("el_qu")
public class Qu extends Model<Qu> {
private static final long serialVersionUID = 1L;
// /**
// * 题目ID
// */
@TableId(value = "id", type = IdType.ASSIGN_ID)
private String id;
// /**
// * 题目类型
// */
@TableField("qu_type")
private Integer quType;
// /**
// * 1普通,2较难
// */
private Integer level;
// /**
// * 题目图片
// */
private String image;
// /**
// * 题目内容
// */
private String content;
// /**
// * 创建时间
// */
@TableField("create_time")
private Date createTime;
// /**
// * 更新时间
// */
@TableField("update_time")
private Date updateTime;
// /**
// * 题目备注
// */
private String remark;
// /**
// * 整题解析
// */
private String analysis;
}

@ -0,0 +1,58 @@
package com.yf.exam.modules.qu.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
///**
//* <p>
//* 候选答案实体类
//* </p>
//*
//* @author 聪明笨狗
//* @since 2020-05-25 13:23
//*/
@Data
@TableName("el_qu_answer")
public class QuAnswer extends Model<QuAnswer> {
private static final long serialVersionUID = 1L;
// /**
// * 答案ID
// */
@TableId(value = "id", type = IdType.ASSIGN_ID)
private String id;
// /**
// * 问题ID
// */
@TableField("qu_id")
private String quId;
// /**
// * 是否正确
// */
@TableField("is_right")
private Boolean isRight;
// /**
// * 选项图片
// */
private String image;
// /**
// * 答案内容
// */
private String content;
/**
*
*/
private String analysis;
}

@ -0,0 +1,50 @@
package com.yf.exam.modules.qu.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
///**
//* <p>
//* 试题题库实体类
//* </p>
//*
//* @author 聪明笨狗
//* @since 2020-05-25 13:23
//*/
@Data
@TableName("el_qu_repo")
public class QuRepo extends Model<QuRepo> {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
private String id;
// /**
// * 试题
// */
@TableField("qu_id")
private String quId;
// /**
// * 归属题库
// */
@TableField("repo_id")
private String repoId;
// /**
// * 题目类型
// */
@TableField("qu_type")
private Integer quType;
// /**
// * 排序
// */
private Integer sort;
}

@ -0,0 +1,26 @@
package com.yf.exam.modules.qu.enums;
///**
// * 题目类型
// * @author bool
// * @date 2019-10-30 13:11
// */
public interface QuType {
// /**
// * 单选题
// */
Integer RADIO = 1;
// /**
// * 多选题
// */
Integer MULTI = 2;
// /**
// * 判断题
// */
Integer JUDGE = 3;
}

@ -0,0 +1,16 @@
package com.yf.exam.modules.qu.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yf.exam.modules.qu.entity.QuAnswer;
///**
//* <p>
//* 候选答案Mapper
//* </p>
//*
//* @author 聪明笨狗
//* @since 2020-05-25 13:23
//*/
public interface QuAnswerMapper extends BaseMapper<QuAnswer> {
}

@ -0,0 +1,56 @@
package com.yf.exam.modules.qu.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yf.exam.modules.qu.dto.QuDTO;
import com.yf.exam.modules.qu.dto.export.QuExportDTO;
import com.yf.exam.modules.qu.dto.request.QuQueryReqDTO;
import com.yf.exam.modules.qu.entity.Qu;
import org.apache.ibatis.annotations.Param;
import java.util.List;
///**
//* <p>
//* 问题题目Mapper
//* </p>
//*
//* @author 聪明笨狗
//* @since 2020-05-25 13:23
//*/
public interface QuMapper extends BaseMapper<Qu> {
// /**
// * 随机抽取题库的数据
// * @param repoId
// * @param quType
// * @param level
// * @param excludes 要排除的ID列表
// * @param size
// * @return
// */
List<Qu> listByRandom(@Param("repoId") String repoId,
@Param("quType") Integer quType,
@Param("excludes") List<String> excludes,
@Param("size") Integer size);
// /**
// * 查找导出列表
// * @param query
// * @return
// */
List<QuExportDTO> listForExport(@Param("query") QuQueryReqDTO query);
// /**
// * 分页查找
// * @param page
// * @param query
// * @return
// */
IPage<QuDTO> paging(Page page, @Param("query") QuQueryReqDTO query);
}

@ -0,0 +1,16 @@
package com.yf.exam.modules.qu.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yf.exam.modules.qu.entity.QuRepo;
///**
//* <p>
//* 试题题库Mapper
//* </p>
//*
//* @author 聪明笨狗
//* @since 2020-05-25 13:23
//*/
public interface QuRepoMapper extends BaseMapper<QuRepo> {
}

@ -0,0 +1,48 @@
package com.yf.exam.modules.qu.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yf.exam.core.api.dto.PagingReqDTO;
import com.yf.exam.modules.qu.dto.QuAnswerDTO;
import com.yf.exam.modules.qu.entity.QuAnswer;
import java.util.List;
///**
//* <p>
//* 候选答案业务类
//* </p>
//*
//* @author 聪明笨狗
//* @since 2020-05-25 13:23
//*/
public interface QuAnswerService extends IService<QuAnswer> {
// /**
// * 分页查询数据
// * @param reqDTO
// * @return
// */
IPage<QuAnswerDTO> paging(PagingReqDTO<QuAnswerDTO> reqDTO);
//
// /**
// * 根据题目ID查询答案并随机
// * @param quId
// * @return
// */
List<QuAnswer> listAnswerByRandom(String quId);
// /**
// * 根据问题查找答案
// * @param quId
// * @return
// */
List<QuAnswerDTO> listByQu(String quId);
// /**
// * 保存试题
// * @param quId
// * @param list
// */
void saveAll(String quId, List<QuAnswerDTO> list);
}

@ -0,0 +1,59 @@
package com.yf.exam.modules.qu.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yf.exam.core.api.dto.PagingReqDTO;
import com.yf.exam.modules.qu.dto.QuRepoDTO;
import com.yf.exam.modules.qu.dto.request.QuRepoBatchReqDTO;
import com.yf.exam.modules.qu.entity.QuRepo;
import java.util.List;
///**
//* <p>
//* 试题题库业务类
//* </p>
//*
//* @author 聪明笨狗
//* @since 2020-05-25 13:23
//*/
public interface QuRepoService extends IService<QuRepo> {
// /**
// * 分页查询数据
// * @param reqDTO
// * @return
// */
IPage<QuRepoDTO> paging(PagingReqDTO<QuRepoDTO> reqDTO);
// /**
// * 保存全部列表
// * @param quId
// * @param quType
// * @param ids
// */
void saveAll(String quId, Integer quType, List<String> ids);
// /**
// * 根据问题查找题库
// * @param quId
// * @return
// */
List<String> listByQu(String quId);
// /**
// * 根据题库查找题目ID列表
// * @param repoId
// * @param quType
// * @param rand
// * @return
// */
List<String> listByRepo(String repoId, Integer quType, boolean rand);
// /**
// * 批量操作
// * @param reqDTO
// */
void batchAction(QuRepoBatchReqDTO reqDTO);
}

@ -0,0 +1,76 @@
package com.yf.exam.modules.qu.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yf.exam.core.api.dto.PagingReqDTO;
import com.yf.exam.modules.qu.dto.QuDTO;
import com.yf.exam.modules.qu.dto.export.QuExportDTO;
import com.yf.exam.modules.qu.dto.ext.QuDetailDTO;
import com.yf.exam.modules.qu.dto.request.QuQueryReqDTO;
import com.yf.exam.modules.qu.entity.Qu;
import java.util.List;
///**
//* <p>
//* 问题题目业务类
//* </p>
//*
//* @author 聪明笨狗
//* @since 2020-05-25 13:23
//*/
public interface QuService extends IService<Qu> {
// /**
// * 分页查询数据
// * @param reqDTO
// * @return
// */
IPage<QuDTO> paging(PagingReqDTO<QuQueryReqDTO> reqDTO);
// /**
// * 删除试题
// * @param ids
// */
void delete(List<String> ids);
// /**
// * 随机抽取题库的数据
// * @param repoId
// * @param quType
// * @param excludes 要排除的ID列表
// * @param size
// * @return
// */
List<Qu> listByRandom(String repoId,
Integer quType,
List<String> excludes,
Integer size);
// /**
// * 问题详情
// * @param id
// * @return
// */
QuDetailDTO detail(String id);
// /**
// * 保存试题
// * @param reqDTO
// */
void save(QuDetailDTO reqDTO);
// /**
// * 查找导出列表
// * @param query
// * @return
// */
List<QuExportDTO> listForExport(QuQueryReqDTO query);
// /**
// * 导入Excel
// * @param dtoList
// * @return
// */
int importExcel(List<QuExportDTO> dtoList);
}

@ -0,0 +1,144 @@
package com.yf.exam.modules.qu.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yf.exam.core.api.dto.PagingReqDTO;
import com.yf.exam.core.utils.BeanMapper;
import com.yf.exam.modules.qu.dto.QuAnswerDTO;
import com.yf.exam.modules.qu.entity.QuAnswer;
import com.yf.exam.modules.qu.mapper.QuAnswerMapper;
import com.yf.exam.modules.qu.service.QuAnswerService;
import com.yf.exam.modules.qu.utils.ImageCheckUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
///**
//* <p>
//* 语言设置 服务实现类
//* </p>
//*
//* @author 聪明笨狗
//* @since 2020-05-25 13:23
//*/
@Service
public class QuAnswerServiceImpl extends ServiceImpl<QuAnswerMapper, QuAnswer> implements QuAnswerService {
@Autowired
private ImageCheckUtils imageCheckUtils;
@Override
public IPage<QuAnswerDTO> paging(PagingReqDTO<QuAnswerDTO> reqDTO) {
//创建分页对象
IPage<QuAnswer> query = new Page<>(reqDTO.getCurrent(), reqDTO.getSize());
//查询条件
QueryWrapper<QuAnswer> wrapper = new QueryWrapper<>();
//获得数据
IPage<QuAnswer> page = this.page(query, wrapper);
//转换结果
IPage<QuAnswerDTO> pageData = JSON.parseObject(JSON.toJSONString(page), new TypeReference<Page<QuAnswerDTO>>(){});
return pageData;
}
@Override
public List<QuAnswer> listAnswerByRandom(String quId) {
QueryWrapper<QuAnswer> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(QuAnswer::getQuId, quId);
wrapper.last(" ORDER BY RAND() ");
return this.list(wrapper);
}
@Override
public List<QuAnswerDTO> listByQu(String quId) {
QueryWrapper<QuAnswer> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(QuAnswer::getQuId, quId);
List<QuAnswer> list = this.list(wrapper);
if(!CollectionUtils.isEmpty(list)){
return BeanMapper.mapList(list, QuAnswerDTO.class);
}
return null;
}
// /**
// * 查找已存在的列表
// * @param quId
// * @return
// */
public List<String> findExistsList(String quId) {
//返回结果
List<String> ids = new ArrayList<>();
QueryWrapper<QuAnswer> wrapper = new QueryWrapper();
wrapper.lambda().eq(QuAnswer::getQuId, quId);
List<QuAnswer> list = this.list(wrapper);
if (!CollectionUtils.isEmpty(list)) {
for (QuAnswer item : list) {
ids.add(item.getId());
}
}
return ids;
}
@Override
public void saveAll(String quId, List<QuAnswerDTO> list) {
//最终要保存的列表
List<QuAnswer> saveList = new ArrayList<>();
//已存在的标签列表
List<String> ids = this.findExistsList(quId);
if(!CollectionUtils.isEmpty(list)){
for(QuAnswerDTO item: list){
// 校验图片地址
imageCheckUtils.checkImage(item.getImage(), "选项图片地址错误!");
//标签ID
String id = item.getId();
QuAnswer answer = new QuAnswer();
BeanMapper.copy(item, answer);
answer.setQuId(quId);
//补全ID避免新增
if(ids.contains(id)){
ids.remove(id);
}
saveList.add(answer);
}
//保存标签列表
if(!CollectionUtils.isEmpty(saveList)) {
this.saveOrUpdateBatch(saveList);
}
//删除已移除
if(!ids.isEmpty()){
this.removeByIds(ids);
}
}else{
QueryWrapper<QuAnswer> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(QuAnswer::getQuId, quId);
this.remove(wrapper);
}
}
}

@ -0,0 +1,175 @@
package com.yf.exam.modules.qu.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yf.exam.core.api.dto.PagingReqDTO;
import com.yf.exam.modules.qu.dto.QuRepoDTO;
import com.yf.exam.modules.qu.dto.request.QuRepoBatchReqDTO;
import com.yf.exam.modules.qu.entity.Qu;
import com.yf.exam.modules.qu.entity.QuRepo;
import com.yf.exam.modules.qu.mapper.QuMapper;
import com.yf.exam.modules.qu.mapper.QuRepoMapper;
import com.yf.exam.modules.qu.service.QuRepoService;
import com.yf.exam.modules.repo.service.RepoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
///**
//* <p>
//* 语言设置 服务实现类
//* </p>
//*
//* @author 聪明笨狗
//* @since 2020-05-25 13:23
//*/
@Service
public class QuRepoServiceImpl extends ServiceImpl<QuRepoMapper, QuRepo> implements QuRepoService {
@Autowired
private QuMapper quMapper;
@Autowired
private RepoService repoService;
@Override
public IPage<QuRepoDTO> paging(PagingReqDTO<QuRepoDTO> reqDTO) {
//创建分页对象
IPage<QuRepo> query = new Page<>(reqDTO.getCurrent(), reqDTO.getSize());
//查询条件
QueryWrapper<QuRepo> wrapper = new QueryWrapper<>();
//获得数据
IPage<QuRepo> page = this.page(query, wrapper);
//转换结果
IPage<QuRepoDTO> pageData = JSON.parseObject(JSON.toJSONString(page), new TypeReference<Page<QuRepoDTO>>(){});
return pageData;
}
@Override
public void saveAll(String quId, Integer quType, List<String> ids) {
// 先删除
QueryWrapper<QuRepo> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(QuRepo::getQuId, quId);
this.remove(wrapper);
// 保存全部
if(!CollectionUtils.isEmpty(ids)){
List<QuRepo> list = new ArrayList<>();
for(String id: ids){
QuRepo ref = new QuRepo();
ref.setQuId(quId);
ref.setRepoId(id);
ref.setQuType(quType);
list.add(ref);
}
this.saveBatch(list);
for(String id: ids){
this.sortRepo(id);
}
}
}
@Override
public List<String> listByQu(String quId) {
// 先删除
QueryWrapper<QuRepo> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(QuRepo::getQuId, quId);
List<QuRepo> list = this.list(wrapper);
List<String> ids = new ArrayList<>();
if(!CollectionUtils.isEmpty(list)){
for(QuRepo item: list){
ids.add(item.getRepoId());
}
}
return ids;
}
@Override
public List<String> listByRepo(String repoId, Integer quType, boolean rand) {
QueryWrapper<QuRepo> wrapper = new QueryWrapper<>();
wrapper.lambda()
.eq(QuRepo::getRepoId, repoId);
if(quType!=null){
wrapper.lambda().eq(QuRepo::getQuType, quType);
}
if(rand){
wrapper.orderByAsc(" RAND() ");
}else{
wrapper.lambda().orderByAsc(QuRepo::getSort);
}
List<QuRepo> list = this.list(wrapper);
List<String> ids = new ArrayList<>();
if(!CollectionUtils.isEmpty(list)){
for(QuRepo item: list){
ids.add(item.getQuId());
}
}
return ids;
}
@Override
public void batchAction(QuRepoBatchReqDTO reqDTO) {
// 移除的
if(reqDTO.getRemove()!=null && reqDTO.getRemove()){
QueryWrapper<QuRepo> wrapper = new QueryWrapper<>();
wrapper.lambda()
.in(QuRepo::getRepoId, reqDTO.getRepoIds())
.in(QuRepo::getQuId, reqDTO.getQuIds());
this.remove(wrapper);
}else{
// 新增的
for(String quId : reqDTO.getQuIds()){
Qu q = quMapper.selectById(quId);
this.saveAll(quId, q.getQuType(), reqDTO.getRepoIds());
}
}
for(String id: reqDTO.getRepoIds()){
this.sortRepo(id);
}
}
// /**
// * 单个题库进行排序
// * @param repoId
// */
private void sortRepo(String repoId){
QueryWrapper<QuRepo> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(QuRepo::getRepoId, repoId);
List<QuRepo> list = this.list(wrapper);
if(CollectionUtils.isEmpty(list)){
return;
}
int sort = 1;
for(QuRepo item: list){
item.setSort(sort);
sort++;
}
this.updateBatchById(list);
}
}

@ -0,0 +1,283 @@
package com.yf.exam.modules.qu.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yf.exam.ability.upload.config.UploadConfig;
import com.yf.exam.core.api.dto.PagingReqDTO;
import com.yf.exam.core.exception.ServiceException;
import com.yf.exam.core.utils.BeanMapper;
import com.yf.exam.modules.qu.dto.QuAnswerDTO;
import com.yf.exam.modules.qu.dto.QuDTO;
import com.yf.exam.modules.qu.dto.export.QuExportDTO;
import com.yf.exam.modules.qu.dto.ext.QuDetailDTO;
import com.yf.exam.modules.qu.dto.request.QuQueryReqDTO;
import com.yf.exam.modules.qu.entity.Qu;
import com.yf.exam.modules.qu.entity.QuAnswer;
import com.yf.exam.modules.qu.entity.QuRepo;
import com.yf.exam.modules.qu.enums.QuType;
import com.yf.exam.modules.qu.mapper.QuMapper;
import com.yf.exam.modules.qu.service.QuAnswerService;
import com.yf.exam.modules.qu.service.QuRepoService;
import com.yf.exam.modules.qu.service.QuService;
import com.yf.exam.modules.qu.utils.ImageCheckUtils;
import com.yf.exam.modules.repo.service.RepoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
///**
// * <p>
// * 语言设置 服务实现类
// * </p>
// *
// * @author 聪明笨狗
// * @since 2020-05-25 10:17
// */
@Service
public class QuServiceImpl extends ServiceImpl<QuMapper, Qu> implements QuService {
@Autowired
private QuAnswerService quAnswerService;
@Autowired
private QuRepoService quRepoService;
@Autowired
private ImageCheckUtils imageCheckUtils;
@Override
public IPage<QuDTO> paging(PagingReqDTO<QuQueryReqDTO> reqDTO) {
//创建分页对象
Page page = new Page<>(reqDTO.getCurrent(), reqDTO.getSize());
//转换结果
IPage<QuDTO> pageData = baseMapper.paging(page, reqDTO.getParams());
return pageData;
}
@Transactional(rollbackFor = Exception.class)
@Override
public void delete(List<String> ids) {
// 移除题目
this.removeByIds(ids);
// 移除选项
QueryWrapper<QuAnswer> wrapper = new QueryWrapper<>();
wrapper.lambda().in(QuAnswer::getQuId, ids);
quAnswerService.remove(wrapper);
// 移除题库绑定
QueryWrapper<QuRepo> wrapper1 = new QueryWrapper<>();
wrapper1.lambda().in(QuRepo::getQuId, ids);
quRepoService.remove(wrapper1);
}
@Override
public List<Qu> listByRandom(String repoId, Integer quType, List<String> excludes, Integer size) {
return baseMapper.listByRandom(repoId, quType, excludes, size);
}
@Override
public QuDetailDTO detail(String id) {
QuDetailDTO respDTO = new QuDetailDTO();
Qu qu = this.getById(id);
BeanMapper.copy(qu, respDTO);
List<QuAnswerDTO> answerList = quAnswerService.listByQu(id);
respDTO.setAnswerList(answerList);
List<String> repoIds = quRepoService.listByQu(id);
respDTO.setRepoIds(repoIds);
return respDTO;
}
@Transactional(rollbackFor = Exception.class)
@Override
public void save(QuDetailDTO reqDTO) {
// 校验数据
this.checkData(reqDTO, "");
Qu qu = new Qu();
BeanMapper.copy(reqDTO, qu);
// 校验图片地址
imageCheckUtils.checkImage(qu.getImage(), "题干图片地址错误!");
// 更新
this.saveOrUpdate(qu);
// 保存全部问题
quAnswerService.saveAll(qu.getId(), reqDTO.getAnswerList());
// 保存到题库
quRepoService.saveAll(qu.getId(), qu.getQuType(), reqDTO.getRepoIds());
}
@Override
public List<QuExportDTO> listForExport(QuQueryReqDTO query) {
return baseMapper.listForExport(query);
}
@Override
public int importExcel(List<QuExportDTO> dtoList) {
//根据题目名称分组
Map<Integer, List<QuExportDTO>> anMap = new HashMap<>(16);
//题目本体信息
Map<Integer, QuExportDTO> quMap = new HashMap<>(16);
//数据分组
for (QuExportDTO item : dtoList) {
// 空白的ID
if (StringUtils.isEmpty(item.getNo())) {
continue;
}
Integer key;
//序号
try {
key = Integer.parseInt(item.getNo());
} catch (Exception e) {
continue;
}
//如果已经有题目了,直接处理选项
if (anMap.containsKey(key)) {
anMap.get(key).add(item);
} else {
//如果没有,将题目内容和选项一起
List<QuExportDTO> subList = new ArrayList<>();
subList.add(item);
anMap.put(key, subList);
quMap.put(key, item);
}
}
int count = 0;
try {
//循环题目插入
for (Integer key : quMap.keySet()) {
QuExportDTO im = quMap.get(key);
//题目基本信息
QuDetailDTO qu = new QuDetailDTO();
qu.setContent(im.getQContent());
qu.setAnalysis(im.getQAnalysis());
qu.setQuType(Integer.parseInt(im.getQuType()));
qu.setCreateTime(new Date());
//设置回答列表
List<QuAnswerDTO> answerList = this.processAnswerList(anMap.get(key));
//设置题目
qu.setAnswerList(answerList);
//设置引用题库
qu.setRepoIds(im.getRepoList());
// 保存答案
this.save(qu);
count++;
}
} catch (ServiceException e) {
e.printStackTrace();
throw new ServiceException(1, "导入出现问题,行:" + count + "" + e.getMessage());
}
return count;
}
// /**
// * 处理回答列表
// *
// * @param importList
// * @return
// */
private List<QuAnswerDTO> processAnswerList(List<QuExportDTO> importList) {
List<QuAnswerDTO> list = new ArrayList<>(16);
for (QuExportDTO item : importList) {
QuAnswerDTO a = new QuAnswerDTO();
a.setIsRight("1".equals(item.getAIsRight()));
a.setContent(item.getAContent());
a.setAnalysis(item.getAAnalysis());
a.setId("");
list.add(a);
}
return list;
}
// /**
// * 校验题目信息
// *
// * @param qu
// * @param no
// * @throws Exception
// */
public void checkData(QuDetailDTO qu, String no) {
if (StringUtils.isEmpty(qu.getContent())) {
throw new ServiceException(1, no + "题目内容不能为空!");
}
if (CollectionUtils.isEmpty(qu.getRepoIds())) {
throw new ServiceException(1, no + "至少要选择一个题库!");
}
List<QuAnswerDTO> answers = qu.getAnswerList();
if (CollectionUtils.isEmpty(answers)) {
throw new ServiceException(1, no + "客观题至少要包含一个备选答案!");
}
int trueCount = 0;
for (QuAnswerDTO a : answers) {
if (a.getIsRight() == null) {
throw new ServiceException(1, no + "必须定义选项是否正确项!");
}
if (StringUtils.isEmpty(a.getContent())) {
throw new ServiceException(1, no + "选项内容不为空!");
}
if (a.getIsRight()) {
trueCount += 1;
}
}
if (trueCount == 0) {
throw new ServiceException(1, no + "至少要包含一个正确项!");
}
//单选题
if (qu.getQuType().equals(QuType.RADIO) && trueCount > 1) {
throw new ServiceException(1, no + "单选题不能包含多个正确项!");
}
}
}

@ -0,0 +1,31 @@
package com.yf.exam.modules.qu.utils;
import com.yf.exam.ability.upload.config.UploadConfig;
import com.yf.exam.core.exception.ServiceException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ImageCheckUtils {
@Autowired
private UploadConfig conf;
// /**
// * 进行图片校验!
// * @param image
// * @param throwMsg
// */
public void checkImage(String image, String throwMsg) {
if(StringUtils.isBlank(image)){
return;
}
// 校验图片地址
if(!image.startsWith(conf.getUrl())){
throw new ServiceException(throwMsg);
}
}
}

@ -0,0 +1,94 @@
<template>
<el-select
v-model="currentValue"
:multiple="multi"
:remote-method="fetchData"
filterable
remote
clearable
placeholder="选择或搜索考试"
class="filter-item"
@change="handlerChange"
>
<el-option
v-for="item in dataList"
:key="item.id"
:label="item.title"
:value="item.id"
/>
</el-select>
</template>
<script>
import { fetchList } from '@/api/exam/exam'
export default {
name: 'ExamSelect',
props: {
// /**
// *
// */
multi: Boolean,
// /**
// *
// */
value: Array,
// /**
// * valueArray
// */
default: String
},
data() {
return {
// /**
// *
// */
dataList: [],
// /**
// *
// */
currentValue: []
}
},
watch: {
// /**
// * valuevaluecurrentValue
// */
value: {
handler() {
this.currentValue = this.value
}
}
},
created() {
// /**
// * currentValuefetchData
// */
this.currentValue = this.value
this.fetchData()
},
methods: {
// /**
// *
// */
fetchData() {
fetchList().then(response => {
this.dataList = response.data.records
})
},
// /**
// *
// * @param e
// */
handlerChange(e) {
console.log(e)
this.$emit('change', e)
this.$emit('input', e)
}
}
}
</script>

@ -0,0 +1,85 @@
<template>
<div>
<file-upload-local v-model="fileUrl" :accept="accept" :tips="tips" :list-type="listType" />
</div>
</template>
<script>
import FileUploadLocal from './local'
export default {
name: 'FileUpload',
components: { FileUploadLocal },
props: {
// /**
// * URL
// */
value: String,
// /**
// *
// */
accept: {
type: String,
default: '*'
},
// /**
// *
// */
tips: String,
// /**
// *
// */
listType: {
type: String,
default: 'picture'
}
},
data() {
return {
// /**
// * URL
// */
fileUrl: ''
}
},
watch: {
// /**
// * valuevaluefillValuefileUrl
// */
value: {
handler() {
this.fillValue()
}
},
// /**
// * fileUrlfileUrlfileUrl
// */
fileUrl: {
handler() {
this.$emit('input', this.fileUrl)
}
}
},
mounted() {
//
},
created() {
// /**
// * fileUrl
// */
this.fillValue()
},
methods: {
// /**
// * valuefileUrl
// */
fillValue() {
this.fileUrl = this.value
}
}
}
</script>

@ -0,0 +1,155 @@
<template>
<div class="content">
<el-upload
v-model="fileUrl"
:action="server"
:accept="accept"
:before-remove="beforeRemove"
:on-remove="handleRemove"
:on-success="handleSuccess"
:on-exceed="handleExceed"
:drag="listType!=='picture'"
:limit="limit"
:headers="header"
:file-list="fileList"
:list-type="listType"
>
<el-button v-if="listType==='picture'" size="small" type="primary"></el-button>
<i v-if="listType!=='picture'" class="el-icon-upload" />
<div v-if="listType!=='picture'" class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
<div v-if="tips" slot="tip" class="el-upload__tip">{{ tips }}</div>
</el-upload>
</div>
</template>
<script>
import { getToken } from '@/utils/auth'
export default {
name: 'FileUploadLocal',
components: { FileUploadLocal },
props: {
// /**
// * URL
// */
value: String,
// /**
// *
// */
accept: String,
// /**
// *
// */
tips: String,
// /**
// *
// */
listType: String,
// /**
// * 1
// */
limit: {
type: Number,
default: 1
}
},
data() {
return {
// /**
// *
// */
server: `${process.env.VUE_APP_BASE_API}/common/api/file/upload`,
// /**
// *
// */
fileList: [],
// /**
// * URL
// */
fileUrl: '',
// /**
// *
// */
header: {}
}
},
watch: {
// /**
// * valuevaluefillValuefileUrlfileList
// */
value: {
handler() {
this.fillValue()
}
}
},
created() {
// /**
// * fileUrlfileListtoken
// */
this.fillValue()
this.header = { token: getToken() }
},
methods: {
// /**
// * valuefileUrlfileList
// */
fillValue() {
this.fileList = []
this.fileUrl = this.value
if (this.fileUrl) {
this.fileList = [{ name: this.fileUrl, url: this.fileUrl }]
}
},
// /**
// *
// */
handleExceed() {
this.$message.warning(`每次只能上传 ${this.limit} 个文件`)
},
// /**
// *
// * @returns {Promise} Promise
// */
beforeRemove() {
return this.$confirm(`确定移除文件吗?`)
},
// /**
// *
// */
handleRemove() {
this.$emit('input', '')
this.fileList = []
},
// /**
// *
// * @param {Object} response
// */
handleSuccess(response) {
if (response.code === 1) {
this.$message({
type: 'error',
message: response.msg
})
this.fileList = []
return
}
this.$emit('input', response.data.url)
}
}
}
</script>

@ -0,0 +1,50 @@
<template>
<div style="padding: 0 15px;" @click="toggleClick">
<svg
:class="{'is-active': isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script>
export default {
name: 'Hamburger',
props: {
// /**
// * false
// */
isActive: {
type: Boolean,
default: false
}
},
methods: {
// /**
// * toggleClick
// */
toggleClick() {
this.$emit('toggleClick')
}
}
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>

@ -0,0 +1,236 @@
<template>
<div :class="{'show': show}" class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-select
ref="headerSearchSelect"
v-model="search"
:remote-method="querySearch"
filterable
default-first-option
remote
placeholder="Search"
class="header-search-select"
@change="change"
>
<el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')" />
</el-select>
</div>
</template>
<script>
// fuse is a lightweight fuzzy-search module
// make search results more in line with expectations
import Fuse from 'fuse.js'
import path from 'path'
export default {
name: 'HeaderSearch',
data() {
return {
// /**
// *
// */
search: '',
// /**
// *
// */
options: [],
// /**
// *
// */
searchPool: [],
// /**
// *
// */
show: false,
// /**
// * Fuse
// */
fuse: undefined
}
},
computed: {
// /**
// *
// * @returns {Array}
// */
routes() {
return this.$store.getters.permission_routes
}
},
watch: {
// /**
// *
// */
routes() {
this.searchPool = this.generateRoutes(this.routes)
},
// /**
// * Fuse
// */
searchPool(list) {
this.initFuse(list)
},
// /**
// *
// */
show(value) {
if (value) {
document.body.addEventListener('click', this.close)
} else {
document.body.removeEventListener('click', this.close)
}
}
},
mounted() {
// /**
// *
// */
this.searchPool = this.generateRoutes(this.routes)
},
methods: {
// /**
// *
// */
click() {
this.show = !this.show
if (this.show) {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
}
},
// /**
// *
// */
close() {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
this.options = []
this.show = false
},
// /**
// *
// * @param {Object} val
// */
change(val) {
this.$router.push(val.path)
this.search = ''
this.options = []
this.$nextTick(() => {
this.show = false
})
},
// /**
// * Fuse
// * @param {Array} list
// */
initFuse(list) {
this.fuse = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [{
name: 'title',
weight: 0.7
}, {
name: 'path',
weight: 0.3
}]
})
},
// /**
// *
// * @param {Array} routes
// * @param {String} basePath '/'
// * @param {Array} prefixTitle
// * @returns {Array}
// */
generateRoutes(routes, basePath = '/', prefixTitle = []) {
let res = []
for (const router of routes) {
// skip hidden router
if (router.hidden) { continue }
const data = {
path: path.resolve(basePath, router.path),
title: [...prefixTitle]
}
if (router.meta && router.meta.title) {
data.title = [...data.title, router.meta.title]
if (router.redirect !== 'noRedirect') {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data)
}
}
// recursive child routes
if (router.children) {
const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes]
}
}
}
return res
},
// /**
// *
// * @param {String} query
// */
querySearch(query) {
if (query !== '') {
this.options = this.fuse.search(query)
} else {
this.options = []
}
}
}
}
</script>
<style lang="scss" scoped>
.header-search {
font-size: 0 !important;
.search-icon {
cursor: pointer;
font-size: 18px;
vertical-align: middle;
}
.header-search-select {
font-size: 18px;
transition: width 0.2s;
width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
display: inline-block;
vertical-align: middle;
::v-deep .el-input__inner {
border-radius: 0;
border: 0;
padding-left: 0;
padding-right: 0;
box-shadow: none !important;
border-bottom: 1px solid #d9d9d9;
vertical-align: middle;
}
}
&.show {
.header-search-select {
width: 210px;
margin-left: 10px;
}
}
}
</style>

@ -0,0 +1,89 @@
<template>
<el-select
v-model="values"
:remote-method="fetchList"
style="width: 100%"
multiple
filterable
remote
reserve-keyword
clearable
automatic-dropdown
placeholder="请选择角色"
@change="handlerChange"
>
<el-option
v-for="item in list"
:key="item.id"
:label="item.title"
:value="item.id"
/>
</el-select>
</template>
<script>
import { fetchList } from '@/api/sys/role/role'
export default {
name: 'MeetRole',
props: {
/**
* 初始选中的角色ID数组
*/
value: {
type: Array,
default: () => []
}
},
data() {
return {
// /**
// *
// */
list: [],
// /**
// * ID
// */
values: []
}
},
watch: {
// /**
// * valuevalues
// */
value: {
handler(newVal) {
this.values = newVal
},
deep: true
}
},
created() {
// /**
// * values
// */
this.values = this.value
this.fetchList()
},
methods: {
// /**
// * list
// */
fetchList() {
fetchList().then(response => {
this.list = response.data
})
},
// /**
// * changeinput
// * @param {Array} e ID
// */
handlerChange(e) {
this.$emit('change', e)
this.$emit('input', e)
}
}
}
</script>

@ -0,0 +1,149 @@
<template>
<div :class="{'hidden': hidden}" class="pagination-container">
<el-pagination
:background="background"
:current-page.sync="currentPage"
:page-size.sync="pageSize"
:layout="layout"
:page-sizes="pageSizes"
:total="total"
v-bind="$attrs"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script>
import { scrollTo } from '@/utils/scroll-to'
export default {
name: 'Pagination',
props: {
// /**
// *
// */
total: {
required: true,
type: Number
},
// /**
// * 1
// */
page: {
type: Number,
default: 1
},
// /**
// * 20
// */
limit: {
type: Number,
default: 20
},
// /**
// * [10, 20, 30, 50]
// */
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50]
}
},
// /**
// * 'total, sizes, prev, pager, next, jumper'
// */
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
// /**
// * true
// */
background: {
type: Boolean,
default: true
},
// /**
// * true
// */
autoScroll: {
type: Boolean,
default: true
},
// /**
// * false
// */
hidden: {
type: Boolean,
default: false
}
},
computed: {
// /**
// * page
// */
currentPage: {
get() {
return this.page
},
set(val) {
this.$emit('update:page', val)
}
},
// /**
// * limit
// */
pageSize: {
get() {
return this.limit
},
set(val) {
this.$emit('update:limit', val)
}
}
},
methods: {
// /**
// * pagination
// * @param {Number} val
// */
handleSizeChange(val) {
this.$emit('pagination', { page: this.currentPage, limit: val })
if (this.autoScroll) {
scrollTo(0, 800)
}
},
// /**
// * pagination
// * @param {Number} val
// */
handleCurrentChange(val) {
this.$emit('pagination', { page: val, limit: this.pageSize })
if (this.autoScroll) {
scrollTo(0, 800)
}
}
}
}
</script>
<style scoped>
.pagination-container {
background: #fff;
padding: 32px 16px;
}
.pagination-container.hidden {
display: none;
}
</style>

@ -0,0 +1,158 @@
<template>
<div :style="{zIndex: zIndex, height: height, width: width}" class="pan-item">
<div class="pan-info">
<div class="pan-info-roles-container">
<!-- 插槽用于插入自定义内容 -->
<slot />
</div>
</div>
<!-- eslint-disable-next-line -->
<div :style="{backgroundImage: `url(${image})`}" class="pan-thumb"></div>
</div>
</template>
<script>
export default {
name: 'PanThumb',
props: {
// /**
// * URL
// */
image: {
type: String,
required: true
},
// /**
// * z-index1
// */
zIndex: {
type: Number,
default: 1
},
// /**
// * '150px'
// */
width: {
type: String,
default: '150px'
},
// /**
// * '150px'
// */
height: {
type: String,
default: '150px'
}
}
}
</script>
<style scoped>
.pan-item {
width: 200px;
height: 200px;
border-radius: 50%;
display: inline-block;
position: relative;
cursor: default;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.pan-info-roles-container {
padding: 20px;
text-align: center;
}
.pan-thumb {
width: 100%;
height: 100%;
background-position: center center;
background-size: cover;
border-radius: 50%;
overflow: hidden;
position: absolute;
transform-origin: 95% 40%;
transition: all 0.3s ease-in-out;
}
/* .pan-thumb:after {
content: '';
width: 8px;
height: 8px;
position: absolute;
border-radius: 50%;
top: 40%;
left: 95%;
margin: -4px 0 0 -4px;
background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
} */
.pan-info {
position: absolute;
width: inherit;
height: inherit;
border-radius: 50%;
overflow: hidden;
box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
}
.pan-info h3 {
color: #fff;
text-transform: uppercase;
position: relative;
letter-spacing: 2px;
font-size: 18px;
margin: 0 60px;
padding: 22px 0 0 0;
height: 85px;
font-family: 'Open Sans', Arial, sans-serif;
text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3);
}
.pan-info p {
color: #fff;
padding: 10px 5px;
font-style: italic;
margin: 0 30px;
font-size: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.5);
}
.pan-info p a {
display: block;
color: #333;
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
color: #fff;
font-style: normal;
font-weight: 700;
text-transform: uppercase;
font-size: 9px;
letter-spacing: 1px;
padding-top: 24px;
margin: 7px auto 0;
font-family: 'Open Sans', Arial, sans-serif;
opacity: 0;
transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s;
transform: translateX(60px) rotate(90deg);
}
.pan-info p a:hover {
background: rgba(255, 255, 255, 0.5);
}
.pan-item:hover .pan-thumb {
transform: rotate(-110deg);
}
.pan-item:hover .pan-info p a {
opacity: 1;
transform: translateX(0px) rotate(0deg);
}
</style>

@ -0,0 +1,105 @@
<template>
<el-select
v-model="currentValue"
:multiple="multi"
:remote-method="fetchData"
filterable
remote
reserve-keyword
clearable
automatic-dropdown
placeholder="选择或搜索题库"
class="filter-item"
@change="handlerChange"
>
<el-option
v-for="item in dataList"
:key="item.id"
:label="item.title"
:value="item.id"
/>
</el-select>
</template>
<script>
import { fetchPaging } from '@/api/qu/repo'
export default {
name: 'RepoSelect',
props: {
// /**
// * false
// */
multi: {
type: Boolean,
default: false
},
// /**
// *
// */
value: {
type: [String, Array]
},
// /**
// * ID
// */
excludes: {
type: Array
}
},
data() {
return {
// /**
// *
// */
dataList: [],
// /**
// *
// */
currentValue: []
}
},
watch: {
// /**
// * valuecurrentValue
// */
value: {
handler() {
this.currentValue = this.value
}
}
},
created() {
// currentValuevalue
this.currentValue = this.value
//
this.fetchData()
},
methods: {
// /**
// *
// * @param {String} q
// */
fetchData(q) {
fetchPaging({ current: 1, size: 1000, params: { title: q, excludes: this.excludes }}).then(res => {
this.dataList = res.data.records
})
},
// /**
// * changeinput
// * @param {Number|Array} e
// */
handlerChange(e) {
const obj = this.dataList.find((item) => {
return item.id === e
})
this.$emit('change', obj)
this.$emit('input', e)
}
}
}
</script>

@ -0,0 +1,195 @@
<template>
<div ref="rightPanel" :class="{show:show}" class="rightPanel-container">
<!-- 背景遮罩层 -->
<div class="rightPanel-background" />
<!-- 右侧内容面板 -->
<div class="rightPanel">
<!-- 切换按钮根据show状态显示不同的图标 -->
<div :style="{'top':buttonTop+'px','background-color':theme}" class="handle-button" @click="show=!show">
<i :class="show?'el-icon-close':'el-icon-setting'" />
</div>
<!-- 插槽用于插入自定义内容 -->
<div class="rightPanel-items">
<slot />
</div>
</div>
</div>
</template>
<script>
import { addClass, removeClass } from '@/utils'
export default {
name: 'RightPanel',
props: {
// /**
// * false
// */
clickNotClose: {
default: false,
type: Boolean
},
// /**
// * 250px
// */
buttonTop: {
default: 250,
type: Number
}
},
data() {
return {
// /**
// *
// */
show: false
}
},
computed: {
// /**
// *
// */
theme() {
return this.$store.state.settings.theme
}
},
watch: {
// /**
// * show
// * @param {Boolean} value show
// */
show(value) {
if (value && !this.clickNotClose) {
this.addEventClick()
}
if (value) {
addClass(document.body, 'showRightPanel')
} else {
removeClass(document.body, 'showRightPanel')
}
}
},
mounted() {
// body
this.insertToBody()
},
beforeDestroy() {
//
const elx = this.$refs.rightPanel
elx.remove()
},
methods: {
// /**
// *
// */
addEventClick() {
window.addEventListener('click', this.closeSidebar)
},
// /**
// *
// * @param {Event} evt
// */
closeSidebar(evt) {
const parent = evt.target.closest('.rightPanel')
if (!parent) {
this.show = false
window.removeEventListener('click', this.closeSidebar)
}
},
// /**
// * body
// */
insertToBody() {
const elx = this.$refs.rightPanel
const body = document.querySelector('body')
body.insertBefore(elx, body.firstChild)
}
}
}
</script>
<style>
/**
* 当body元素应用showRightPanel类时处理页面的overflow和宽度
*/
.showRightPanel {
overflow: hidden;
position: relative;
width: calc(100% - 15px);
}
</style>
<style lang="scss" scoped>
///**
// *
// */
.rightPanel-background {
position: fixed;
top: 0;
left: 0;
opacity: 0;
transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
background: rgba(0, 0, 0, .2);
z-index: -1;
}
/**
* 右侧内容面板样式
*/
.rightPanel {
width: 100%;
max-width: 260px;
height: 100vh;
position: fixed;
top: 0;
right: 0;
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
transition: all .25s cubic-bezier(.7, .3, .1, 1);
transform: translate(100%);
background: #fff;
z-index: 40000;
}
/**
* 当右侧面板显示时应用的样式
*/
.show {
transition: all .3s cubic-bezier(.7, .3, .1, 1);
.rightPanel-background {
z-index: 20000;
opacity: 1;
width: 100%;
height: 100%;
}
.rightPanel {
transform: translate(0);
}
}
/**
* 切换按钮样式
*/
.handle-button {
width: 48px;
height: 48px;
position: absolute;
left: -48px;
text-align: center;
font-size: 24px;
border-radius: 6px 0 0 6px !important;
z-index: 0;
pointer-events: auto;
cursor: pointer;
color: #fff;
line-height: 48px;
i {
font-size: 24px;
line-height: 48px;
}
}
</style>

@ -0,0 +1,60 @@
<template>
<div>
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
</div>
</template>
<script>
import screenfull from 'screenfull'
export default {
name: 'Screenfull',
data() {
return {
isFullscreen: false
}
},
mounted() {
this.init()
},
beforeDestroy() {
this.destroy()
},
methods: {
click() {
if (!screenfull.enabled) {
this.$message({
message: 'you browser can not work',
type: 'warning'
})
return false
}
screenfull.toggle()
},
change() {
this.isFullscreen = screenfull.isFullscreen
},
init() {
if (screenfull.enabled) {
screenfull.on('change', this.change)
}
},
destroy() {
if (screenfull.enabled) {
screenfull.off('change', this.change)
}
}
}
}
</script>
<style scoped>
.screenfull-svg {
display: inline-block;
cursor: pointer;
fill: #5a5e66;;
width: 20px;
height: 20px;
vertical-align: 10px;
}
</style>

@ -0,0 +1,57 @@
<template>
<el-dropdown trigger="click" @command="handleSetSize">
<div>
<svg-icon class-name="size-icon" icon-class="size" />
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">
{{
item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
export default {
data() {
return {
sizeOptions: [
{ label: '默认', value: 'default' },
{ label: '中等', value: 'medium' },
{ label: '小', value: 'small' },
{ label: '极小', value: 'mini' }
]
}
},
computed: {
size() {
return this.$store.getters.size
}
},
methods: {
handleSetSize(size) {
this.$ELEMENT.size = size
this.$store.dispatch('app/setSize', size)
this.refreshView()
this.$message({
message: '字体切换成功!',
type: 'success'
})
},
refreshView() {
// In order to make the cached page re-rendered
this.$store.dispatch('tagsView/delAllCachedViews', this.$route)
const { fullPath } = this.$route
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
}
}
}
</script>

@ -0,0 +1,62 @@
<template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :href="iconName" />
</svg>
</template>
<script>
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
import { isExternal } from '@/utils/validate'
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</style>

@ -0,0 +1,175 @@
<template>
<el-color-picker
v-model="theme"
:predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
class="theme-picker"
popper-class="theme-picker-dropdown"
/>
</template>
<script>
const version = require('element-ui/package.json').version // element-ui version from node_modules
const ORIGINAL_THEME = '#409EFF' // default color
export default {
data() {
return {
chalk: '', // content of theme-chalk css
theme: ''
}
},
computed: {
defaultTheme() {
return this.$store.state.settings.theme
}
},
watch: {
defaultTheme: {
handler: function(val, oldVal) {
this.theme = val
},
immediate: true
},
async theme(val) {
const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
if (typeof val !== 'string') return
const themeCluster = this.getThemeCluster(val.replace('#', ''))
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
console.log(themeCluster, originalCluster)
const $message = this.$message({
message: ' Compiling the theme',
customClass: 'theme-message',
type: 'success',
duration: 0,
iconClass: 'el-icon-loading'
})
const getHandler = (variable, id) => {
return () => {
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
let styleTag = document.getElementById(id)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', id)
document.head.appendChild(styleTag)
}
styleTag.innerText = newStyle
}
}
if (!this.chalk) {
const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
await this.getCSSString(url, 'chalk')
}
const chalkHandler = getHandler('chalk', 'chalk-style')
chalkHandler()
const styles = [].slice.call(document.querySelectorAll('style'))
.filter(style => {
const text = style.innerText
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
})
styles.forEach(style => {
const { innerText } = style
if (typeof innerText !== 'string') return
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
})
this.$emit('change', val)
$message.close()
}
},
methods: {
updateStyle(style, oldCluster, newCluster) {
let newStyle = style
oldCluster.forEach((color, index) => {
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
})
return newStyle
},
getCSSString(url, variable) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
resolve()
}
}
xhr.open('GET', url)
xhr.send()
})
},
getThemeCluster(theme) {
const tintColor = (color, tint) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
if (tint === 0) { // when primary color is in its rgb space
return [red, green, blue].join(',')
} else {
red += Math.round(tint * (255 - red))
green += Math.round(tint * (255 - green))
blue += Math.round(tint * (255 - blue))
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
}
const shadeColor = (color, shade) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
red = Math.round((1 - shade) * red)
green = Math.round((1 - shade) * green)
blue = Math.round((1 - shade) * blue)
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
const clusters = [theme]
for (let i = 0; i <= 9; i++) {
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
}
clusters.push(shadeColor(theme, 0.1))
return clusters
}
}
}
</script>
<style>
.theme-message,
.theme-picker-dropdown {
z-index: 99999 !important;
}
.theme-picker .el-color-picker__trigger {
height: 26px !important;
width: 26px !important;
padding: 2px;
}
.theme-picker-dropdown .el-color-dropdown__link-btn {
display: none;
}
</style>

@ -0,0 +1,78 @@
// Inspired by https://github.com/Inndy/vue-clipboard2
const Clipboard = require('clipboard')
if (!Clipboard) {
throw new Error('you should npm install `clipboard` --save at first ')
}
export default {
// /**
// * 当指令绑定到元素时调用
// * @param {Element} el 被绑定的元素
// * @param {Object} binding 包含指令信息的对象
// */
bind(el, binding) {
if (binding.arg === 'success') {
// 存储成功回调函数
el._v_clipboard_success = binding.value
} else if (binding.arg === 'error') {
// 存储错误回调函数
el._v_clipboard_error = binding.value
} else {
// 创建Clipboard实例并配置其行为
const clipboard = new Clipboard(el, {
text() { return binding.value }, // 返回需要复制的文本
action() { return binding.arg === 'cut' ? 'cut' : 'copy' } // 根据指令参数决定是剪切还是复制
})
// 监听复制成功事件
clipboard.on('success', e => {
const callback = el._v_clipboard_success
callback && callback(e) // 调用成功回调函数
})
// 监听复制错误事件
clipboard.on('error', e => {
const callback = el._v_clipboard_error
callback && callback(e) // 调用错误回调函数
})
// 将Clipboard实例存储在元素上
el._v_clipboard = clipboard
}
},
// /**
// * 当指令所在的模板更新时调用
// * @param {Element} el 被绑定的元素
// * @param {Object} binding 包含指令信息的对象
// */
update(el, binding) {
if (binding.arg === 'success') {
// 更新成功回调函数
el._v_clipboard_success = binding.value
} else if (binding.arg === 'error') {
// 更新错误回调函数
el._v_clipboard_error = binding.value
} else {
// 更新Clipboard实例的文本和行为
el._v_clipboard.text = function() { return binding.value }
el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' }
}
},
// /**
// * 当指令与元素解绑时调用
// * @param {Element} el 被绑定的元素
// * @param {Object} binding 包含指令信息的对象
// */
unbind(el, binding) {
if (binding.arg === 'success') {
// 删除成功回调函数
delete el._v_clipboard_success
} else if (binding.arg === 'error') {
// 删除错误回调函数
delete el._v_clipboard_error
} else {
// 销毁Clipboard实例并删除相关属性
el._v_clipboard.destroy()
delete el._v_clipboard
}
}
}

@ -0,0 +1,21 @@
import Clipboard from './clipboard'
// /**
// * 定义一个安装函数用于将Clipboard指令注册到Vue
// * @param {Vue} Vue Vue构造函数
// */
const install = function(Vue) {
Vue.directive('Clipboard', Clipboard) // 将Clipboard指令注册到Vue
}
// 如果window对象中存在Vue实例则直接安装该指令
if (window.Vue) {
window.clipboard = Clipboard // 将Clipboard对象挂载到window上
Vue.use(install); // 调用install函数安装指令// eslint-disable-line 表示忽略此行的eslint检查
}
// 将install方法添加到Clipboard对象上以便外部可以调用
Clipboard.install = install
// 导出Clipboard对象作为默认导出
export default Clipboard

@ -0,0 +1,21 @@
import permission from './permission'
// /**
// * 定义一个安装函数用于将permission指令注册到Vue
// * @param {Vue} Vue Vue构造函数
// */
const install = function(Vue) {
Vue.directive('permission', permission) // 将permission指令注册到Vue
}
// 如果window对象中存在Vue实例则直接安装该指令
if (window.Vue) {
window['permission'] = permission // 将permission对象挂载到window上
Vue.use(install); // 调用install函数安装指令// eslint-disable-line 表示忽略此行的eslint检查
}
// 将install方法添加到permission对象上以便外部可以调用
permission.install = install
// 导出permission对象作为默认导出
export default permission

@ -0,0 +1,34 @@
import store from '@/store'
// /**
// * 定义一个Vue指令用于权限控制
// */
export default {
// /**
// * 当指令绑定的元素插入到DOM中时调用
// * @param {Element} el 指令绑定的DOM元素
// * @param {Object} binding 包含指令相关信息的对象
// * @param {Object} vnode 包含虚拟节点相关信息的对象
// */
inserted(el, binding, vnode) {
const { value } = binding // 从binding对象中获取指令的值
const roles = store.getters && store.getters.roles // 从store的getters中获取用户的角色列表
// 检查指令的值是否存在且是一个数组并且数组长度大于0
if (value && value instanceof Array && value.length > 0) {
const permissionRoles = value // 指令值中包含的权限角色列表
// 检查用户是否具有至少一个指令中指定的角色
const hasPermission = roles.some(role => {
return permissionRoles.includes(role) // 判断当前角色是否在权限角色列表中
})
// 如果用户没有权限移除绑定的DOM元素
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el) // 移除DOM元素
}
} else {
// 如果指令值不存在或不是有效的数组,抛出错误
throw new Error(`need roles! Like v-permission="['admin','editor']"`)
}
}
}

@ -0,0 +1,91 @@
const vueSticky = {}
let listenAction
vueSticky.install = Vue => {
Vue.directive('sticky', {
inserted(el, binding) {
const params = binding.value || {}
const stickyTop = params.stickyTop || 0
const zIndex = params.zIndex || 1000
const elStyle = el.style
elStyle.position = '-webkit-sticky'
elStyle.position = 'sticky'
// if the browser support css stickyCurrently Safari, Firefox and Chrome Canary
// if (~elStyle.position.indexOf('sticky')) {
// elStyle.top = `${stickyTop}px`;
// elStyle.zIndex = zIndex;
// return
// }
const elHeight = el.getBoundingClientRect().height
const elWidth = el.getBoundingClientRect().width
elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}`
const parentElm = el.parentNode || document.documentElement
const placeholder = document.createElement('div')
placeholder.style.display = 'none'
placeholder.style.width = `${elWidth}px`
placeholder.style.height = `${elHeight}px`
parentElm.insertBefore(placeholder, el)
let active = false
const getScroll = (target, top) => {
const prop = top ? 'pageYOffset' : 'pageXOffset'
const method = top ? 'scrollTop' : 'scrollLeft'
let ret = target[prop]
if (typeof ret !== 'number') {
ret = window.document.documentElement[method]
}
return ret
}
const sticky = () => {
if (active) {
return
}
if (!elStyle.height) {
elStyle.height = `${el.offsetHeight}px`
}
elStyle.position = 'fixed'
elStyle.width = `${elWidth}px`
placeholder.style.display = 'inline-block'
active = true
}
const reset = () => {
if (!active) {
return
}
elStyle.position = ''
placeholder.style.display = 'none'
active = false
}
const check = () => {
const scrollTop = getScroll(window, true)
const offsetTop = el.getBoundingClientRect().top
if (offsetTop < stickyTop) {
sticky()
} else {
if (scrollTop < elHeight + stickyTop) {
reset()
}
}
}
listenAction = () => {
check()
}
window.addEventListener('scroll', listenAction)
},
unbind() {
window.removeEventListener('scroll', listenAction)
}
})
}
export default vueSticky

@ -0,0 +1,21 @@
import waves from './waves' // 导入waves模块
// /**
// * 定义一个安装函数用于将waves指令注册到Vue
// * @param {Vue} Vue Vue构造函数
// */
const install = function(Vue) {
Vue.directive('waves', waves) // 将waves指令注册到Vue
}
// 如果window对象中存在Vue实例则直接安装该指令
if (window.Vue) {
window.waves = waves // 将waves对象挂载到window上
Vue.use(install); // 调用install函数安装指令// eslint-disable-line 表示忽略此行的eslint检查
}
// 将install方法添加到waves对象上以便外部可以调用
waves.install = install
// 导出waves对象作为默认导出
export default waves

@ -0,0 +1,26 @@
.waves-ripple {
position: absolute;
border-radius: 100%;
background-color: rgba(0, 0, 0, 0.15);
background-clip: padding-box;
pointer-events: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-transform: scale(0);
-ms-transform: scale(0);
transform: scale(0);
opacity: 1;
}
.waves-ripple.z-active {
opacity: 0;
-webkit-transform: scale(2);
-ms-transform: scale(2);
transform: scale(2);
-webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
transition: opacity 1.2s ease-out, transform 0.6s ease-out;
transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
}

@ -0,0 +1,102 @@
import './waves.css' // 导入waves.css样式文件
const context = '@@wavesContext' // 定义一个上下文标识符
// /**
// * 处理点击事件,生成波纹效果
// * @param {Element} el 指令绑定的DOM元素
// * @param {Object} binding 包含指令相关信息的对象
// * @returns {Function} 处理波纹效果的函数
// */
function handleClick(el, binding) {
// /**
// * 具体处理波纹效果的函数
// * @param {Event} e 点击事件对象
// */
function handle(e) {
const customOpts = Object.assign({}, binding.value) // 复制指令的值到customOpts
const opts = Object.assign({
ele: el, // 波纹作用元素
type: 'hit', // hit 点击位置扩散 center中心点扩展
color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
}, customOpts) // 将customOpts合并到默认配置中
const target = opts.ele // 获取波纹作用的目标元素
if (target) {
target.style.position = 'relative' // 设置目标元素的position为relative
target.style.overflow = 'hidden' // 设置目标元素的overflow为hidden
const rect = target.getBoundingClientRect() // 获取目标元素的尺寸和位置
let ripple = target.querySelector('.waves-ripple') // 查找已存在的波纹元素
if (!ripple) {
ripple = document.createElement('span') // 创建新的波纹元素
ripple.className = 'waves-ripple' // 设置波纹元素的类名
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px' // 设置波纹元素的宽度和高度
target.appendChild(ripple) // 将波纹元素添加到目标元素中
} else {
ripple.className = 'waves-ripple' // 重置波纹元素的类名
}
// 根据opts.type设置波纹元素的位置
switch (opts.type) {
case 'center':
ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px' // 设置波纹元素的top位置为中心
ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px' // 设置波纹元素的left位置为中心
break
default:
ripple.style.top =
(e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
document.body.scrollTop) + 'px' // 设置波纹元素的top位置为点击位置
ripple.style.left =
(e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
document.body.scrollLeft) + 'px' // 设置波纹元素的left位置为点击位置
}
ripple.style.backgroundColor = opts.color // 设置波纹元素的背景颜色
ripple.className = 'waves-ripple z-active' // 添加激活类名以显示波纹效果
return false // 阻止默认事件行为
}
}
// 如果el中没有context对象则创建一个新的
if (!el[context]) {
el[context] = {
removeHandle: handle // 将handle函数赋值给removeHandle属性
}
} else {
el[context].removeHandle = handle // 更新removeHandle属性为新的handle函数
}
return handle // 返回handle函数
}
// /**
// * 定义一个Vue指令用于生成波纹效果
// */
export default {
// /**
// * 当指令绑定到元素时调用
// * @param {Element} el 指令绑定的DOM元素
// * @param {Object} binding 包含指令相关信息的对象
// */
bind(el, binding) {
el.addEventListener('click', handleClick(el, binding), false) // 添加点击事件监听器
},
// /**
// * 当指令更新时调用
// * @param {Element} el 指令绑定的DOM元素
// * @param {Object} binding 包含指令相关信息的对象
// */
update(el, binding) {
el.removeEventListener('click', el[context].removeHandle, false) // 移除旧的点击事件监听器
el.addEventListener('click', handleClick(el, binding), false) // 添加新的点击事件监听器
},
// /**
// * 当指令从元素上解绑时调用
// * @param {Element} el 指令绑定的DOM元素
// */
unbind(el) {
el.removeEventListener('click', el[context].removeHandle, false) // 移除点击事件监听器
el[context] = null // 将context对象设置为null
delete el[context] // 删除context属性
}
}
Loading…
Cancel
Save