Commit 80dd9773 by tangyi

Merge branch 'dev'

parents b822b4d2 647f4b5f
Version v3.5.0 (2019-12-08)
--------------------------
改进:
* 升级spring boot、spring cloud版本
新功能:
* 前台重构
* 集成七牛云存储
* 增加成绩排名
* 增加多选题、判断题
* 导入导出优化,使用阿里easyexcel库
* 其它优化,如:引入loadingCache,业务错误码规范等
Version v3.4.0 (2019-10-27) Version v3.4.0 (2019-10-27)
-------------------------- --------------------------
改进: 改进:
......
...@@ -46,8 +46,8 @@ ...@@ -46,8 +46,8 @@
| 名称 | 版本 | | 名称 | 版本 |
| --------- | -------- | | --------- | -------- |
| `Spring Boot` | `2.1.9.RELEASE` | | `Spring Boot` | `2.1.11.RELEASE` |
| `Spring Cloud` | `Greenwich.SR3` | | `Spring Cloud` | `Greenwich.SR4` |
# 5 系统架构 # 5 系统架构
...@@ -94,10 +94,13 @@ ...@@ -94,10 +94,13 @@
## 7.1 前台功能 ## 7.1 前台功能
1. 考试 1. 首页
![image](docs/images/image_web.png)
2. 考试
![image](docs/images/image_web_exam.png) ![image](docs/images/image_web_exam.png)
2. 查看错题 3. 查看错题
![image](docs/images/image_web_incorrect_answer.png) ![image](docs/images/image_web_incorrect_answer.png)
## 7.2 后台功能 ## 7.2 后台功能
......
package com.github.tangyi.common.config.handler; package com.github.tangyi.common.config.handler;
import com.github.tangyi.common.core.constant.ApiMsg;
import com.github.tangyi.common.core.exceptions.CommonException; import com.github.tangyi.common.core.exceptions.CommonException;
import com.github.tangyi.common.core.model.ResponseBean; import com.github.tangyi.common.core.model.ResponseBean;
import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.context.support.DefaultMessageSourceResolvable;
...@@ -43,8 +44,7 @@ public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler ...@@ -43,8 +44,7 @@ public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler
.stream() .stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage) .map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList()); .collect(Collectors.toList());
ResponseBean<List<String>> responseBean = new ResponseBean<>(errors); ResponseBean<List<String>> responseBean = new ResponseBean<>(errors, ApiMsg.KEY_SERVICE, ApiMsg.ERROR);
responseBean.setStatus(status.value());
return new ResponseEntity<>(responseBean, headers, status); return new ResponseEntity<>(responseBean, headers, status);
} }
...@@ -56,10 +56,7 @@ public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler ...@@ -56,10 +56,7 @@ public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler
*/ */
@ExceptionHandler(CommonException.class) @ExceptionHandler(CommonException.class)
public ResponseEntity<ResponseBean<String>> handleCommonException(Exception e) { public ResponseEntity<ResponseBean<String>> handleCommonException(Exception e) {
ResponseBean<String> responseBean = new ResponseBean<>(); ResponseBean<String> responseBean = new ResponseBean<>(e.getMessage(), ApiMsg.KEY_SERVICE, ApiMsg.ERROR);
responseBean.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
responseBean.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
responseBean.setMsg(e.getMessage());
return new ResponseEntity<>(responseBean, HttpStatus.OK); return new ResponseEntity<>(responseBean, HttpStatus.OK);
} }
} }
\ No newline at end of file
...@@ -127,5 +127,11 @@ ...@@ -127,5 +127,11 @@
<artifactId>json</artifactId> <artifactId>json</artifactId>
<version>${json.version}</version> <version>${json.version}</version>
</dependency> </dependency>
<!-- easyexcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>
package com.github.tangyi.common.core.cache.loadingcache;
import com.github.tangyi.common.core.exceptions.CommonException;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
*
* LoadingCache工具类,单例
* 用法:
* 1. LoadingCacheHelper.getInstance().getOrDefault(SimpleCacheLoader.class, "key", "defaultValue");
* 2. LoadingCacheHelper.getInstance().get(SimpleCacheLoader.class, "key");
*
* @author tangyi
* @date 2019-11-25 16:38
*/
@Slf4j
public class LoadingCacheHelper {
public static final ListeningExecutorService REFRESH_POOLS = MoreExecutors.listeningDecorator(Executors
.newFixedThreadPool(1, new ThreadFactoryBuilder().setNameFormat("refresh-cache-thread-%d").build()));
private static class LoadingCacheHelperInstance {
public static final LoadingCacheHelper instance = new LoadingCacheHelper();
}
public static LoadingCacheHelper getInstance() {
return LoadingCacheHelper.LoadingCacheHelperInstance.instance;
}
/**
* 保存所有loadingCache实例
*/
private Map<String, LoadingCache> loadingCacheMap;
private LoadingCacheHelper() {
loadingCacheMap = new HashMap<>();
}
/**
* 初始化loadingCache
* @param clazz clazz
* @param duration 频率,单位秒
* @param <K> key
* @param <V> value
* @return LoadingCache
*/
public <K, V> LoadingCache<K, V> initLoadingCache(Class<? extends CacheLoader<K, V>> clazz, long duration) {
return initLoadingCache(clazz, duration, 0);
}
/**
* 初始化loadingCache
* @param clazz clazz
* @param duration 频率,单位秒
* @param <K> key
* @param <V> value
* @param cacheSize cacheSize
* @return LoadingCache
*/
private <K, V> LoadingCache<K, V> initLoadingCache(Class<? extends CacheLoader<K, V>> clazz, long duration,
long cacheSize) {
return initLoadingCache(clazz, duration, 0, cacheSize);
}
/**
* 初始化loadingCache
* @param clazz clazz
* @param refreshAfterWriteDuration 单位秒
* @param expireAfterAccessDuration 单位秒
* @param <K> key
* @param <V> value
* @param cacheSize cacheSize
* @return LoadingCache
*/
private <K, V> LoadingCache<K, V> initLoadingCache(Class<? extends CacheLoader<K, V>> clazz,
long refreshAfterWriteDuration, long expireAfterAccessDuration, long cacheSize) {
try {
log.info("Instantiating LoadingCache: {}", clazz);
CacheLoader<K, V> cacheLoader = clazz.newInstance();
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
builder.concurrencyLevel(1);
if (expireAfterAccessDuration > 0) {
// 在给定时间内没有被读/写访问,则回收
builder.expireAfterAccess(expireAfterAccessDuration, TimeUnit.SECONDS);
} else {
// 自动刷新
builder.refreshAfterWrite(refreshAfterWriteDuration, TimeUnit.SECONDS);
}
if (cacheSize > 0)
builder.maximumSize(cacheSize);
LoadingCache<K, V> cache = builder.build(cacheLoader);
this.loadingCacheMap.put(clazz.getSimpleName(), cache);
return cache;
} catch (Exception e) {
log.error("Error Instantiating LoadingCache: " + clazz, e);
throw new CommonException(e, "Error Instantiating LoadingCache: " + clazz);
}
}
/**
* 从指定的loadingCache里获取内容
* @param clazz clazz
* @param key key
* @param defaultValue defaultValue
* @param <K> key
* @param <V> value
* @return value
*/
@SuppressWarnings("unchecked")
public <K, V> V getOrDefault(Class<? extends CacheLoader<K, V>> clazz, K key, V defaultValue) {
V value = get(clazz, key);
return value == null ? defaultValue : value;
}
/**
* 从指定的loadingCache里获取内容
* @param clazz clazz
* @param key key
* @param <K> key
* @param <V> value
* @return value,没有对应的key或获取异常则返回null
*/
@SuppressWarnings("unchecked")
public <K, V> V get(Class<? extends CacheLoader<K, V>> clazz, K key) {
LoadingCache<K, V> cache = this.loadingCacheMap.get(clazz.getSimpleName());
if (cache != null) {
try {
return cache.get(key);
} catch (Exception e) {
log.error("Get from loadingCache error, {}, {}, {}", clazz, key, e.getMessage(), e);
}
}
return null;
}
public Map<String, LoadingCache> getLoadingCacheMap() {
return loadingCacheMap;
}
/**
* 刷新某个key
* @param classSimpleName classSimpleName
* @param key key
*/
@SuppressWarnings("unchecked")
public void refresh(String classSimpleName, String key) {
LoadingCache cache = this.loadingCacheMap.get(classSimpleName);
if (cache != null) {
cache.refresh(key);
log.info("Refresh loadingCache: {}, {}", classSimpleName, key);
}
}
/**
* 刷新所有key
* @param classSimpleName classSimpleName
*/
@SuppressWarnings("unchecked")
public void refreshAll(String classSimpleName) {
LoadingCache<String, ?> cache = this.loadingCacheMap.get(classSimpleName);
if (cache != null) {
for (String key : cache.asMap().keySet()) {
cache.refresh(key);
log.info("Refresh loadingCache: {}, {}", classSimpleName, key);
}
}
}
}
...@@ -24,7 +24,6 @@ public class AppStartupRunner implements CommandLineRunner { ...@@ -24,7 +24,6 @@ public class AppStartupRunner implements CommandLineRunner {
@Override @Override
public void run(String... args) throws Exception { public void run(String... args) throws Exception {
log.info("================ start command line ================ "); log.info("================ start command line ================ ");
log.info("set system properties...");
// 设置系统属性 // 设置系统属性
if (StringUtils.isNotBlank(sysProperties.getCacheExpire())) if (StringUtils.isNotBlank(sysProperties.getCacheExpire()))
System.setProperty(CommonConstant.CACHE_EXPIRE, sysProperties.getCacheExpire()); System.setProperty(CommonConstant.CACHE_EXPIRE, sysProperties.getCacheExpire());
......
...@@ -2,7 +2,7 @@ package com.github.tangyi.common.core.config; ...@@ -2,7 +2,7 @@ package com.github.tangyi.common.core.config;
import com.github.tangyi.common.core.properties.SnowflakeProperties; import com.github.tangyi.common.core.properties.SnowflakeProperties;
import com.github.tangyi.common.core.utils.SnowflakeIdWorker; import com.github.tangyi.common.core.utils.SnowflakeIdWorker;
import org.springframework.beans.factory.annotation.Autowired; import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
...@@ -13,13 +13,14 @@ import org.springframework.context.annotation.Configuration; ...@@ -13,13 +13,14 @@ import org.springframework.context.annotation.Configuration;
* @date 2019/4/26 11:17 * @date 2019/4/26 11:17
*/ */
@Configuration @Configuration
@AllArgsConstructor
public class SnowFlake { public class SnowFlake {
@Autowired private final SnowflakeProperties snowflakeProperties;
private SnowflakeProperties snowflakeProperties;
@Bean @Bean
public SnowflakeIdWorker initTokenWorker() { public SnowflakeIdWorker initTokenWorker() {
return new SnowflakeIdWorker(Integer.parseInt(snowflakeProperties.getWorkId()), Integer.parseInt(snowflakeProperties.getDataCenterId())); return new SnowflakeIdWorker(Integer.parseInt(snowflakeProperties.getWorkId()),
} Integer.parseInt(snowflakeProperties.getDataCenterId()));
}
} }
package com.github.tangyi.common.core.constant;
import java.util.HashMap;
import java.util.Map;
/**
* 封装常用的业务错误码和提示信息
* @author tangyi
* @date 2019/12/11 17:35
*/
public class ApiMsg {
private ApiMsg() {
}
/**
* 错误
*/
public static final int ERROR = 0;
/**
* 空
*/
public static final int EMPTY = 1;
/**
* 失败
*/
public static final int FAILED = 2;
/**
* NULL
*/
public static final int NULL = 3;
/**
* 找不到
*/
public static final int NOT_FOUND = 4;
/**
* 不可用
*/
public static final int UNAVAILABLE = 5;
/**
* 超时
*/
public static final int EXPIRED = 6;
/**
* 非法
*/
public static final int INVALID = 7;
/**
* 拒绝
*/
public static final int DENIED = 8;
// =============== 业务key ================
/**
* 成功
*/
public static final int KEY_SUCCESS = 200;
/**
* 错误
*/
public static final int KEY_ERROR = 500;
/**
* 未知内容
*/
public static final int KEY_UNKNOWN = 400;
/**
* 服务
*/
public static final int KEY_SERVICE = 401;
/**
* 验证码
*/
public static final int KEY_VALIDATE_CODE = 402;
/**
* token
*/
public static final int KEY_TOKEN = 403;
/**
* 访问
*/
public static final int KEY_ACCESS = 404;
/**
* code和提示内容的对应关系
*/
private static final Map<Integer, String> CODE_MAP = new HashMap<>();
/**
* code和提示内容的对应关系
*/
private static final Map<Integer, String> KEY_MAP = new HashMap<>();
static {
CODE_MAP.put(KEY_SUCCESS, "SUCCESS");
CODE_MAP.put(EMPTY, "EMPTY");
CODE_MAP.put(ERROR, "ERROR");
CODE_MAP.put(FAILED, "FAILED");
CODE_MAP.put(NULL, "NULL");
CODE_MAP.put(NOT_FOUND, "NOT_FOUND");
CODE_MAP.put(UNAVAILABLE, "UNAVAILABLE");
CODE_MAP.put(EXPIRED, "EXPIRED");
CODE_MAP.put(INVALID, "INVALID");
CODE_MAP.put(DENIED, "DENIED");
}
static {
KEY_MAP.put(KEY_ERROR, "");
KEY_MAP.put(KEY_UNKNOWN, "UNKNOWN");
KEY_MAP.put(KEY_SERVICE, "SERVICE");
KEY_MAP.put(KEY_VALIDATE_CODE, "VALIDATE CODE");
KEY_MAP.put(KEY_TOKEN, "TOKEN");
KEY_MAP.put(KEY_ACCESS, "ACCESS");
}
public static String code2Msg(int codeKey, int msgKey) {
return KEY_MAP.get(codeKey) + " " + CODE_MAP.get(msgKey);
}
public static String msg(int code) {
return CODE_MAP.get(code);
}
}
...@@ -176,5 +176,15 @@ public class CommonConstant { ...@@ -176,5 +176,15 @@ public class CommonConstant {
* baskPackage * baskPackage
*/ */
public static final String BASE_PACKAGE = "com.github.tangyi"; public static final String BASE_PACKAGE = "com.github.tangyi";
/**
* 男
*/
public static final Integer GENDER_MAN = 0;
/**
* 女
*/
public static final Integer GENDER_WOMEN = 1;
} }
package com.github.tangyi.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 性别枚举
* @author tangyi
* @date 2019/12/10 14:23
*/
@Getter
@AllArgsConstructor
public enum GenderEnum {
MEN("男", 0), WOMEN("女", 1);
private String name;
private Integer value;
public static GenderEnum matchByValue(Integer value) {
for (GenderEnum item : GenderEnum.values()) {
if (item.value.equals(value)) {
return item;
}
}
return MEN;
}
public static GenderEnum matchByName(String name) {
for (GenderEnum item : GenderEnum.values()) {
if (item.name.equals(name)) {
return item;
}
}
return MEN;
}
}
...@@ -11,7 +11,7 @@ import lombok.Getter; ...@@ -11,7 +11,7 @@ import lombok.Getter;
*/ */
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum LoginType { public enum LoginTypeEnum {
/** /**
* 账号密码登录 * 账号密码登录
......
package com.github.tangyi.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 状态枚举
* @author tangyi
* @date 2019/12/10 14:32
*/
@Getter
@AllArgsConstructor
public enum StatusEnum {
ENABLE("启用", 0), DISABLE("禁用", 1);
private String name;
private Integer value;
public static StatusEnum matchByValue(Integer value) {
for (StatusEnum item : StatusEnum.values()) {
if (item.value.equals(value)) {
return item;
}
}
return ENABLE;
}
public static StatusEnum matchByName(String name) {
for (StatusEnum item : StatusEnum.values()) {
if (item.name.equals(name)) {
return item;
}
}
return ENABLE;
}
}
package com.github.tangyi.common.core.model; package com.github.tangyi.common.core.model;
import com.github.tangyi.common.core.constant.ApiMsg;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
...@@ -13,80 +14,33 @@ import java.io.Serializable; ...@@ -13,80 +14,33 @@ import java.io.Serializable;
@Data @Data
public class ResponseBean<T> implements Serializable { public class ResponseBean<T> implements Serializable {
public static final long serialVersionUID = 42L; public static final long serialVersionUID = 42L;
public static final int NO_LOGIN = -1; private String msg = ApiMsg.msg(ApiMsg.KEY_SUCCESS);
/** private int code = ApiMsg.KEY_SUCCESS;
* 成功
*/
public static final int SUCCESS = 200;
/** private T data;
* 失败
*/
public static final int FAIL = 500;
/** public ResponseBean() {
* 验证码错误 super();
*/ }
public static final int INVALID_VALIDATE_CODE_ERROR = 478;
/** public ResponseBean(T data) {
* 验证码过期错误 super();
*/ this.data = data;
public static final int VALIDATE_CODE_EXPIRED_ERROR = 479; }
/** public ResponseBean(T data, int keyCode, int msgCode) {
* 用户名不存在或密码错误 super();
*/ this.data = data;
public static final int USERNAME_NOT_FOUND_OR_PASSWORD_ERROR = 400; this.code = Integer.parseInt(keyCode + "" + msgCode);
this.msg = ApiMsg.code2Msg(keyCode, msgCode);
}
/** public ResponseBean(T data, String msg) {
* 当前操作没有权限 super();
*/ this.data = data;
public static final int UNAUTHORIZED = 401; this.msg = msg;
}
/**
* 当前操作没有权限
*/
public static final int NO_PERMISSION = 403;
private String msg = "success";
private int code = SUCCESS;
/**
* http 状态码
*/
private int status = 200;
private T data;
public ResponseBean() {
super();
}
public ResponseBean(T data) {
super();
this.data = data;
}
public ResponseBean(T data, String msg) {
super();
this.data = data;
this.msg = msg;
}
public ResponseBean(Throwable e) {
super();
this.msg = e.getMessage();
this.code = FAIL;
}
public ResponseBean(Throwable e, int code) {
super();
this.msg = e.getMessage();
this.code = code;
}
} }
...@@ -13,6 +13,14 @@ import java.util.List; ...@@ -13,6 +13,14 @@ import java.util.List;
*/ */
public interface CrudMapper<T> extends BaseMapper { public interface CrudMapper<T> extends BaseMapper {
/**
* 根据ID查询,统一类型为Long
*
* @param id id
* @return T
*/
T getById(Long id);
/** /**
* 获取单条数据 * 获取单条数据
* *
......
...@@ -14,16 +14,6 @@ import org.springframework.context.annotation.Configuration; ...@@ -14,16 +14,6 @@ import org.springframework.context.annotation.Configuration;
public class SysProperties { public class SysProperties {
/** /**
* fastDfs服务器的HTTP地址
*/
private String fdfsHttpHost;
/**
* 上传地址
*/
private String uploadUrl;
/**
* 默认头像 * 默认头像
*/ */
private String defaultAvatar; private String defaultAvatar;
...@@ -42,4 +32,6 @@ public class SysProperties { ...@@ -42,4 +32,6 @@ public class SysProperties {
* 缓存超时时间 * 缓存超时时间
*/ */
private String cacheExpire; private String cacheExpire;
private String gatewaySecret;
} }
...@@ -4,7 +4,6 @@ import com.github.pagehelper.PageHelper; ...@@ -4,7 +4,6 @@ import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.github.tangyi.common.core.persistence.BaseEntity; import com.github.tangyi.common.core.persistence.BaseEntity;
import com.github.tangyi.common.core.persistence.CrudMapper; import com.github.tangyi.common.core.persistence.CrudMapper;
import com.github.tangyi.common.core.utils.ReflectionUtil;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
...@@ -25,14 +24,9 @@ public abstract class CrudService<D extends CrudMapper<T>, T extends BaseEntity< ...@@ -25,14 +24,9 @@ public abstract class CrudService<D extends CrudMapper<T>, T extends BaseEntity<
* *
* @param id id * @param id id
* @return T * @return T
* @throws Exception
*/ */
@SuppressWarnings("unchecked") public T get(Long id) {
public T get(Long id) throws Exception { return dao.getById(id);
Class<T> entityClass = ReflectionUtil.getClassGenricType(getClass(), 1);
T entity = entityClass.getConstructor(Long.class).newInstance("0");
entity.setId(id);
return dao.get(entity);
} }
/** /**
......
...@@ -29,7 +29,7 @@ public class JsonMapper extends ObjectMapper { ...@@ -29,7 +29,7 @@ public class JsonMapper extends ObjectMapper {
try { try {
return this.writeValueAsString(object); return this.writeValueAsString(object);
} catch (IOException e) { } catch (IOException e) {
log.warn("将解析JSON为字符串失败:" + object, e); log.warn("Object to json failed: " + object, e);
return null; return null;
} }
} }
...@@ -41,7 +41,7 @@ public class JsonMapper extends ObjectMapper { ...@@ -41,7 +41,7 @@ public class JsonMapper extends ObjectMapper {
try { try {
return this.readValue(jsonString, clazz); return this.readValue(jsonString, clazz);
} catch (IOException e) { } catch (IOException e) {
log.warn("将解析JSON为对象失败:" + jsonString, e); log.warn("Json to object failed: " + jsonString, e);
return null; return null;
} }
} }
...@@ -53,7 +53,7 @@ public class JsonMapper extends ObjectMapper { ...@@ -53,7 +53,7 @@ public class JsonMapper extends ObjectMapper {
try { try {
return (T) this.readValue(jsonString, javaType); return (T) this.readValue(jsonString, javaType);
} catch (IOException e) { } catch (IOException e) {
log.warn("将解析JSON为对象失败:" + jsonString, e); log.warn("Json to object failed: " + jsonString, e);
return null; return null;
} }
} }
......
package com.github.tangyi.common.core.utils; package com.github.tangyi.common.core.utils;
import com.github.tangyi.common.core.constant.ApiMsg;
import com.github.tangyi.common.core.model.ResponseBean; import com.github.tangyi.common.core.model.ResponseBean;
/** /**
*
*
* @author tangyi * @author tangyi
* @date 2019-10-08 12:03 * @date 2019-10-08 12:03
*/ */
public class ResponseUtil { public class ResponseUtil {
private ResponseUtil() {
}
/** /**
* 是否成功 * 是否成功
* @param responseBean responseBean * @param responseBean responseBean
* @return boolean * @return boolean
*/ */
public static boolean isSuccess(ResponseBean<?> responseBean) { public static boolean isSuccess(ResponseBean<?> responseBean) {
return responseBean != null && responseBean.getStatus() == ResponseBean.SUCCESS; return responseBean != null && responseBean.getCode() == ApiMsg.KEY_SUCCESS;
} }
} }
package com.github.tangyi.common.core.utils.excel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* 封装简单数据导入的逻辑,解析3000条刷一次数据库
* @author tangyi
* @date 2019/12/10 16:48
*/
public abstract class AbstractExcelImportListener<T> extends AnalysisEventListener<T> {
protected final Logger logger = LoggerFactory.getLogger(getClass());
/**
* 每隔3000条存储数据库
*/
private static final int BATCH_COUNT = 3000;
/**
* 需要导入的数据
*/
private List<T> dataList = new ArrayList<>();
@Override
public void invoke(T dataModel, AnalysisContext context) {
dataList.add(dataModel);
// 达到BATCH_COUNT则保存进数据库,防止数据几万条数据在内存,容易OOM
if (dataList.size() >= BATCH_COUNT) {
saveData(dataList);
// 存储完成清理list
dataList.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 最后一次保存
saveData(dataList);
logger.info("All data is parsed!");
}
/**
* 保存数据,子类实现
*/
public abstract void saveData(List<T> dataList);
}
package com.github.tangyi.common.core.utils.excel;
import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.github.tangyi.common.core.utils.DateUtils;
import com.github.tangyi.common.core.utils.Servlets;
import com.github.tangyi.common.core.utils.excel.annotation.ExcelModel;
import com.github.tangyi.common.core.utils.excel.exception.ExcelException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.util.List;
/**
* excel导入导出工具类
* @author tangyi
* @date 2018/11/26 22:20
*/
@Slf4j
public class ExcelToolUtil {
private static final String DEFAULT_SHEET_NAME = "sheet1";
/**
* 私有构造方法,禁止实例化
*/
private ExcelToolUtil() {
}
/**
* 导出Excel
* @param request request
* @param response response
* @param dataList 数据list
* @param clazz clazz
*/
public static <T> void writeExcel(HttpServletRequest request, HttpServletResponse response, List<T> dataList,
Class<T> clazz) {
// 获取fileName和sheetName
ExcelModel excelModel = clazz.getDeclaredAnnotation(ExcelModel.class);
String fileName = DateUtils.localDateMillisToString(LocalDateTime.now());
String sheetName = DEFAULT_SHEET_NAME;
if (excelModel != null) {
fileName = excelModel.value() + fileName;
sheetName = excelModel.sheets()[0];
}
// 导出
writeExcel(request, response, dataList, fileName, sheetName, clazz);
}
/**
* 导出Excel
* @param request request
* @param response response
* @param dataList 数据list
* @param fileName 文件名
* @param sheetName sheet 名
* @param clazz clazz
*/
public static <T> void writeExcel(HttpServletRequest request, HttpServletResponse response, List<T> dataList,
String fileName, String sheetName, Class<T> clazz) {
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcelFactory
.write(getOutputStream(fileName, request, response, ExcelTypeEnum.XLSX), clazz).build();
WriteSheet writeSheet = EasyExcelFactory.writerSheet(sheetName).build();
excelWriter.write(dataList, writeSheet);
} finally {
if (excelWriter != null)
excelWriter.finish();
}
}
/**
* 获取OutputStream
* @param fileName fileName
* @param request request
* @param response response
* @param excelTypeEnum excelTypeEnum
* @return OutputStream
*/
private static OutputStream getOutputStream(String fileName, HttpServletRequest request,
HttpServletResponse response, ExcelTypeEnum excelTypeEnum) {
try {
// 设置响应头,处理浏览器间的中文乱码问题
response.addHeader(HttpHeaders.CONTENT_DISPOSITION,
Servlets.getDownName(request, fileName + excelTypeEnum.getValue()));
return response.getOutputStream();
} catch (IOException e) {
throw new ExcelException("get OutputStream error!");
}
}
/**
* 导入Excel
* @param inputStream inputStream
* @param clazz clazz
* @param listener listener
* @return boolean
*/
public static <T> Boolean readExcel(InputStream inputStream, Class<T> clazz, AnalysisEventListener<T> listener) {
Boolean success = Boolean.TRUE;
ExcelReader excelReader = null;
try {
excelReader = EasyExcelFactory.read(inputStream, clazz, listener).build();
ReadSheet readSheet = EasyExcelFactory.readSheet(0).build();
excelReader.read(readSheet);
} catch (Exception e) {
log.error("Read excel error: {}", e.getMessage(), e);
success = Boolean.FALSE;
} finally {
if (excelReader != null)
excelReader.finish();
}
return success;
}
}
package com.github.tangyi.common.core.utils.excel.annotation;
import java.lang.annotation.*;
/**
* @author tangyi
* @date 2019/12/10 16:04
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExcelModel {
/**
* 模块名,用于导出时的文件名
* @return String
*/
String value() default "";
/**
* 页名
* @return String
*/
String[] sheets() default {"sheet1"};
}
package com.github.tangyi.common.core.utils.excel.converter;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.github.tangyi.common.core.enums.GenderEnum;
/**
* 性别转换
* @author tangyi
* @date 2019/12/10 14:16
*/
public class GenderConverter implements Converter<Integer> {
@Override
public Class<?> supportJavaTypeKey() {
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) throws Exception {
return GenderEnum.matchByName(cellData.getStringValue()).getValue();
}
@Override
public CellData<String> convertToExcelData(Integer value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) throws Exception {
return new CellData<>(GenderEnum.matchByValue(value).getName());
}
}
package com.github.tangyi.common.core.utils.excel.converter;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.github.tangyi.common.core.enums.StatusEnum;
/**
* 状态转换
* @author tangyi
* @date 2019/12/10 14:33
*/
public class StatusConverter implements Converter<Integer> {
@Override
public Class<?> supportJavaTypeKey() {
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) throws Exception {
return StatusEnum.matchByName(cellData.getStringValue()).getValue();
}
@Override
public CellData<String> convertToExcelData(Integer value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) throws Exception {
return new CellData<>(StatusEnum.matchByValue(value).getName());
}
}
package com.github.tangyi.common.core.utils.excel.exception;
/**
* @author tangyi
* @date 2019/12/9 21:47
*/
public class ExcelException extends RuntimeException {
public ExcelException(String msg) {
super(msg);
}
}
...@@ -45,7 +45,12 @@ public class UserVo extends BaseEntity<UserVo> { ...@@ -45,7 +45,12 @@ public class UserVo extends BaseEntity<UserVo> {
*/ */
private Long avatarId; private Long avatarId;
/** /**
* 头像地址
*/
private String avatarUrl;
/**
* 邮箱 * 邮箱
*/ */
private String email; private String email;
......
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<parent>
<groupId>com.github.tangyi</groupId>
<artifactId>common</artifactId>
<version>${revision}</version>
</parent>
<artifactId>common-oss</artifactId>
<name>${project.artifactId}</name>
<description>附件存储公共依赖</description>
<dependencies>
<!-- common-core -->
<dependency>
<groupId>com.github.tangyi</groupId>
<artifactId>common-core</artifactId>
</dependency>
<!-- 七牛云 -->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
</dependency>
<!--fastDfs-->
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.github.tangyi.user.config; package com.github.tangyi.oss.config;
import com.github.tobato.fastdfs.FdfsClientConfig; import com.github.tobato.fastdfs.FdfsClientConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableMBeanExport; import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
...@@ -12,10 +13,12 @@ import org.springframework.jmx.support.RegistrationPolicy; ...@@ -12,10 +13,12 @@ import org.springframework.jmx.support.RegistrationPolicy;
* @author tangyi * @author tangyi
* @date 2018-01-05 14:45 * @date 2018-01-05 14:45
*/ */
@Deprecated
@Configuration @Configuration
@Import(FdfsClientConfig.class) @Import(FdfsClientConfig.class)
// 解决jmx重复注册bean的问题 // 解决jmx重复注册bean的问题
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING) @EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
@ConditionalOnExpression("!'${fdfs}'.isEmpty()")
public class FastDfsConfig { public class FastDfsConfig {
} }
......
package com.github.tangyi.oss.config;
import lombok.Data;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 七牛云配置
* @author tangyi
* @date 2019/12/8 8:19 下午
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "qiniu")
@ConditionalOnExpression("!'${qiniu}'.isEmpty()")
public class QiNiuConfig {
private String accessKey;
private String secretKey;
private String bucket;
/**
* 外部访问域名
*/
private String domainOfBucket;
/**
* 链接超时时间,单位秒
*/
private Integer expire;
}
package com.github.tangyi.oss.exceptions;
import com.github.tangyi.common.core.exceptions.CommonException;
/**
* oss exception
* @author tangyi
* @date 2019/12/8 8:41 下午
*/
public class OssException extends CommonException {
public OssException(String msg) {
super(msg);
}
public OssException(Throwable throwable, String msg) {
super(throwable, msg);
}
}
package com.github.tangyi.user.service; package com.github.tangyi.oss.service;
import com.github.tobato.fastdfs.domain.fdfs.GroupState; import com.github.tobato.fastdfs.domain.fdfs.GroupState;
import com.github.tobato.fastdfs.domain.fdfs.MetaData; import com.github.tobato.fastdfs.domain.fdfs.MetaData;
...@@ -27,6 +27,7 @@ import java.util.Set; ...@@ -27,6 +27,7 @@ import java.util.Set;
* @author tangyi * @author tangyi
* @date 2018-01-04 10:34 * @date 2018-01-04 10:34
*/ */
@Deprecated
@Slf4j @Slf4j
@AllArgsConstructor @AllArgsConstructor
@Service @Service
...@@ -74,12 +75,12 @@ public class FastDfsService { ...@@ -74,12 +75,12 @@ public class FastDfsService {
*/ */
public String uploadFile(InputStream inputStream, long size, String extName, Set<MetaData> metaDataSet) { public String uploadFile(InputStream inputStream, long size, String extName, Set<MetaData> metaDataSet) {
try { try {
log.info("上传文件大小{},后缀名{}", size, extName); log.info("Attachment size: {},extName: {}", size, extName);
StorePath storePath = fastFileStorageClient.uploadFile(inputStream, size, extName, metaDataSet); StorePath storePath = fastFileStorageClient.uploadFile(inputStream, size, extName, metaDataSet);
log.info("上传文件成功,group:{},path:{}", storePath.getGroup(), storePath.getPath()); log.info("Upload success, group: {}, path: {}", storePath.getGroup(), storePath.getPath());
return storePath.getFullPath(); return storePath.getFullPath();
} catch (Exception e) { } catch (Exception e) {
log.error("上传文件失败!", e); log.error("Upload failed", e);
} }
return null; return null;
} }
...@@ -97,7 +98,7 @@ public class FastDfsService { ...@@ -97,7 +98,7 @@ public class FastDfsService {
*/ */
public String uploadAppenderFile(String groupName, InputStream inputStream, long size, String extName) { public String uploadAppenderFile(String groupName, InputStream inputStream, long size, String extName) {
try { try {
log.info("上传文件大小{},后缀名{}", size, extName); log.info("Attachment size: {},extName: {}", size, extName);
StorePath storePath = appendFileStorageClient.uploadAppenderFile(groupName, inputStream, size, extName); StorePath storePath = appendFileStorageClient.uploadAppenderFile(groupName, inputStream, size, extName);
log.info("上传文件成功,group:{},path:{}", storePath.getGroup(), storePath.getPath()); log.info("上传文件成功,group:{},path:{}", storePath.getGroup(), storePath.getPath());
return storePath.getFullPath(); return storePath.getFullPath();
......
package com.github.tangyi.oss.service;
import com.github.tangyi.common.core.constant.CommonConstant;
import com.github.tangyi.common.core.utils.JsonMapper;
import com.github.tangyi.oss.config.QiNiuConfig;
import com.github.tangyi.oss.exceptions.OssException;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.BucketManager;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
/**
* 七牛云
* @author tangyi
* @date 2019/12/8 20:25
*/
@Slf4j
@Service
public class QiNiuService {
private final QiNiuConfig qiNiuConfig;
private Auth auth;
private UploadManager uploadManager;
private BucketManager bucketManager;
@Autowired
public QiNiuService(QiNiuConfig qiNiuConfig) {
this.qiNiuConfig = qiNiuConfig;
}
@PostConstruct
public void init() {
if (StringUtils.isNotBlank(qiNiuConfig.getAccessKey()) && StringUtils.isNotBlank(qiNiuConfig.getSecretKey())) {
auth = Auth.create(qiNiuConfig.getAccessKey(), qiNiuConfig.getSecretKey());
Configuration config = new Configuration(Region.region2());
uploadManager = new UploadManager(config);
bucketManager = new BucketManager(auth, config);
}
}
/**
* 获取七牛云token
*
* @return String
*/
public String getQiNiuToken() {
return auth.uploadToken(qiNiuConfig.getBucket());
}
/**
* 上传七牛云
*
* @param uploadBytes 文件
* @param fileName 文件名 默认不指定key的情况下,以文件内容的hash值作为文件名
* @return String
*/
public String upload(byte[] uploadBytes, String fileName) {
try {
Response response = uploadManager.put(uploadBytes, fileName, getQiNiuToken());
//解析上传成功的结果
DefaultPutRet putRet = JsonMapper.getInstance().fromJson(response.bodyString(), DefaultPutRet.class);
return qiNiuConfig.getDomainOfBucket() + "/" + putRet.key;
} catch (QiniuException ex) {
log.error("upload to qiniu exception: {}", ex.getMessage(), ex);
throw new OssException(ex, "upload to qiniu exception");
}
}
/**
* 获取图片url
*
* @param fileName fileName
* @return String
*/
public String getDownloadUrl(String fileName) throws UnsupportedEncodingException {
String encodedFileName = URLEncoder.encode(fileName, CommonConstant.UTF8).replace("+", "%20");
String publicUrl = String.format("%s/%s", qiNiuConfig.getDomainOfBucket(), encodedFileName);
return auth.privateDownloadUrl(publicUrl, qiNiuConfig.getExpire());
}
/**
* 删除附件
* @param fileName fileName
* @return boolean
*/
public boolean delete(String fileName) {
try {
bucketManager.delete(qiNiuConfig.getBucket(), fileName);
return Boolean.TRUE;
} catch (Exception e) {
log.error("delete attachment exception:{}", e.getMessage(), e);
throw new OssException(e, "delete attachment exception");
}
}
/**
* 获取域名
* @return String
*/
public String getDomainOfBucket() {
return qiNiuConfig.getDomainOfBucket();
}
}
...@@ -70,7 +70,7 @@ public class MobileLoginSuccessHandler implements AuthenticationSuccessHandler { ...@@ -70,7 +70,7 @@ public class MobileLoginSuccessHandler implements AuthenticationSuccessHandler {
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails); OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication); OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
OAuth2AccessToken oAuth2AccessToken = defaultAuthorizationServerTokenServices.createAccessToken(oAuth2Authentication); OAuth2AccessToken oAuth2AccessToken = defaultAuthorizationServerTokenServices.createAccessToken(oAuth2Authentication);
log.info("获取token 成功:{}", oAuth2AccessToken.getValue()); log.info("Get token success: {}", oAuth2AccessToken.getValue());
response.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8");
response.setContentType(SecurityConstant.CONTENT_TYPE); response.setContentType(SecurityConstant.CONTENT_TYPE);
PrintWriter printWriter = response.getWriter(); PrintWriter printWriter = response.getWriter();
......
...@@ -18,5 +18,6 @@ ...@@ -18,5 +18,6 @@
<module>common-feign</module> <module>common-feign</module>
<module>common-log</module> <module>common-log</module>
<module>common-config</module> <module>common-config</module>
<module>common-oss</module>
</modules> </modules>
</project> </project>
...@@ -64,11 +64,10 @@ pagehelper: ...@@ -64,11 +64,10 @@ pagehelper:
# 系统配置 # 系统配置
sys: sys:
adminUser: ${ADMIN_USER:admin} # 管理员账号,默认是admin adminUser: ${ADMIN_USER:admin} # 管理员账号,默认是admin
fdfsHttpHost: ${ATTACHMENT_HOST:http://192.168.0.95}:${ATTACHMENT_PORT:8080} # fastDfs的HTTP配置
uploadUrl: api/user/v1/attachment/upload
defaultAvatar: https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80 defaultAvatar: https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80
key: '1234567887654321' key: '1234567887654321'
cacheExpire: 86400 # 缓存失效时间,单位秒,默认一天 cacheExpire: 86400 # 缓存失效时间,单位秒,默认一天
gatewaySecret: ${GATEWAY_SECRET:test}
# feign相关配置 # feign相关配置
feign: feign:
...@@ -120,12 +119,6 @@ preview: ...@@ -120,12 +119,6 @@ preview:
- api/exam # 考试服务 - api/exam # 考试服务
- api/msc - api/msc
# 开启网关token转换
# 暂时关闭,此功能在微信端登录有问题
gateway:
token-transfer:
enabled: ${GATEWAY_TOKEN_TRANSFER:false}
# 集群ID生成配置 # 集群ID生成配置
cluster: cluster:
workId: ${CLUSTER_WORKID:1} workId: ${CLUSTER_WORKID:1}
......
...@@ -94,10 +94,20 @@ fdfs: ...@@ -94,10 +94,20 @@ fdfs:
tracker-list: #TrackerList参数,支持多个 tracker-list: #TrackerList参数,支持多个
- ${FDFS_HOST:192.168.0.95}:${FDFS_PORT:22122} - ${FDFS_HOST:192.168.0.95}:${FDFS_PORT:22122}
# ===================================================================
# 七牛云存储配置
# ===================================================================
qiniu:
access-key: ${QINIU_ACCEESS_KEY:test}
secret-key: ${QINIU_SECRET_KEY:test}
bucket: microservice-exam
domain-of-bucket: ${QINIU_DOMAIN:test}
expire: 3600 # 链接超时时间,单位秒,默认一小时
# 系统配置 # 系统配置
sys: sys:
adminUser: ${ADMIN_USER:admin} # 管理员账号,默认是admin adminUser: ${ADMIN_USER:admin} # 管理员账号,默认是admin
fdfsHttpHost: ${ATTACHMENT_HOST:http://192.168.0.95}:${ATTACHMENT_PORT:8080} # fastDfs的HTTP配置
uploadUrl: api/user/v1/attachment/upload uploadUrl: api/user/v1/attachment/upload
defaultAvatar: https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80 defaultAvatar: https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80
key: '1234567887654321' key: '1234567887654321'
...@@ -171,3 +181,4 @@ cluster: ...@@ -171,3 +181,4 @@ cluster:
workId: ${CLUSTER_WORKID:1} workId: ${CLUSTER_WORKID:1}
dataCenterId: ${CLUSTER_DATA_CENTER_ID:1} dataCenterId: ${CLUSTER_DATA_CENTER_ID:1}
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
<!--<appender-ref ref="logstash"/>--> <!--<appender-ref ref="logstash"/>-->
</root> </root>
<logger name="com.github.tangyi" level="debug"> <logger name="com.github.tangyi" level="debug" additivity="false">
<appender-ref ref="console"/> <appender-ref ref="console"/>
<appender-ref ref="debug"/> <appender-ref ref="debug"/>
<appender-ref ref="error"/> <appender-ref ref="error"/>
......
...@@ -7,6 +7,7 @@ TENANT_CODE=gitee ...@@ -7,6 +7,7 @@ TENANT_CODE=gitee
# 网关token转换 # 网关token转换
GATEWAY_TOKEN_TRANSFER=false GATEWAY_TOKEN_TRANSFER=false
GATEWAY_SECRET=test
# 环境配置 # 环境配置
SPRING_PROFILES_ACTIVE=native SPRING_PROFILES_ACTIVE=native
...@@ -35,6 +36,11 @@ MYSQL_PASSWORD=11 ...@@ -35,6 +36,11 @@ MYSQL_PASSWORD=11
FDFS_HOST=fdfs FDFS_HOST=fdfs
FDFS_PORT=22122 FDFS_PORT=22122
# 七牛云配置
QINIU_ACCEESS_KEY=test
QINIU_SECRET_KEY=test
QINIU_DOMAIN=test
# 配置中心的账号密码 # 配置中心的账号密码
CONFIG_SERVER_USERNAME=admin CONFIG_SERVER_USERNAME=admin
CONFIG_SERVER_PASSWORD=11 CONFIG_SERVER_PASSWORD=11
......
docs/images/image_ui_exam.png

62.9 KB | W: | H:

docs/images/image_ui_exam.png

245 KB | W: | H:

docs/images/image_ui_exam.png
docs/images/image_ui_exam.png
docs/images/image_ui_exam.png
docs/images/image_ui_exam.png
  • 2-up
  • Swipe
  • Onion skin
docs/images/image_ui_menu.png

99.8 KB | W: | H:

docs/images/image_ui_menu.png

273 KB | W: | H:

docs/images/image_ui_menu.png
docs/images/image_ui_menu.png
docs/images/image_ui_menu.png
docs/images/image_ui_menu.png
  • 2-up
  • Swipe
  • Onion skin
docs/images/image_ui_msg.png

101 KB | W: | H:

docs/images/image_ui_msg.png

332 KB | W: | H:

docs/images/image_ui_msg.png
docs/images/image_ui_msg.png
docs/images/image_ui_msg.png
docs/images/image_ui_msg.png
  • 2-up
  • Swipe
  • Onion skin
docs/images/image_web_exam.png

49.5 KB | W: | H:

docs/images/image_web_exam.png

248 KB | W: | H:

docs/images/image_web_exam.png
docs/images/image_web_exam.png
docs/images/image_web_exam.png
docs/images/image_web_exam.png
  • 2-up
  • Swipe
  • Onion skin
// 网关地址 // 网关地址
const GATEWAY_HOST = process.env.GATEWAY_HOST || '127.0.0.1' const GATEWAY_HOST = process.env.GATEWAY_HOST || 'localhost'
const GATEWAY_PORT = process.env.GATEWAY_PORT || '9180' const GATEWAY_PORT = process.env.GATEWAY_PORT || '9180'
// 转发配置 // 转发配置
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<title>系统演示</title> <title>系统演示</title>
</head> </head>
<body> <body>
<script src=<%= BASE_URL %>/tinymce4.7.5/tinymce.min.js></script> <script src=/static/tinymce4.7.5/tinymce.min.js></script>
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
......
{ {
"name": "spring-microservice-exam-ui", "name": "spring-microservice-exam-ui",
"version": "3.4.0", "version": "3.5.0",
"description": "在线考试", "description": "在线考试",
"author": "tangyi <1633736729@qq.com>", "author": "tangyi <1633736729@qq.com>",
"private": true, "private": true,
......
...@@ -31,6 +31,14 @@ export function preview (id) { ...@@ -31,6 +31,14 @@ export function preview (id) {
}) })
} }
export function getDownloadUrl (id) {
return request({
url: baseAttachmentUrl + '/download',
method: 'get',
params: {id: id}
})
}
export function addObj (obj) { export function addObj (obj) {
return request({ return request({
url: baseAttachmentUrl, url: baseAttachmentUrl,
......
...@@ -81,7 +81,7 @@ export default { ...@@ -81,7 +81,7 @@ export default {
lock: '锁屏' lock: '锁屏'
}, },
login: { login: {
title: '欢迎使用考试管理系统', title: '硕果云教育管理系统',
logIn: '登录', logIn: '登录',
tenantCode: '单位ID', tenantCode: '单位ID',
identifier: '账号', identifier: '账号',
......
...@@ -40,12 +40,6 @@ router.beforeEach((to, from, next) => { ...@@ -40,12 +40,6 @@ router.beforeEach((to, from, next) => {
next({ path: '/' }) next({ path: '/' })
}) })
}) })
// 获取附件配置信息
if (store.getters.sysConfig.fdfsHttpHost === undefined) {
store.dispatch('GetSysConfig').then(res => {}).catch(() => {
console.log('获取系统配置失败!')
})
}
} else { } else {
// 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓ // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
if (hasPermission(store.getters.roles, to.meta.roles)) { if (hasPermission(store.getters.roles, to.meta.roles)) {
......
import axios from 'axios' import axios from 'axios'
import store from '../store' import store from '../store'
import { getToken, setToken, getRefreshToken, getTenantCode } from '@/utils/auth' import { getToken, setToken, getRefreshToken, getTenantCode } from '@/utils/auth'
import { isNotEmpty } from '@/utils/util' import { isNotEmpty, isSuccess } from '@/utils/util'
import { refreshToken } from '@/api/admin/login' import { refreshToken } from '@/api/admin/login'
import { Message } from 'element-ui' import { Message } from 'element-ui'
import errorCode from '@/const/errorCode' import errorCode from '@/const/errorCode'
...@@ -37,6 +37,10 @@ axios.interceptors.request.use(config => { ...@@ -37,6 +37,10 @@ axios.interceptors.request.use(config => {
// HTTP response拦截 // HTTP response拦截
axios.interceptors.response.use(data => { axios.interceptors.response.use(data => {
NProgress.done() NProgress.done()
// 请求失败,弹出提示信息
if (!isSuccess(data.data)) {
Message({ message: data.data.msg, type: 'error' })
}
return data return data
}, error => { }, error => {
NProgress.done() NProgress.done()
......
...@@ -257,20 +257,6 @@ export const exportExcel = function (response) { ...@@ -257,20 +257,6 @@ export const exportExcel = function (response) {
} }
/** /**
* 返回附件的预览地址
* @param sysConfig
* @param fastFileId
* @returns {string}
*/
export const getAttachmentPreviewUrl = function (sysConfig, fastFileId) {
let url = ''
if (isNotEmpty(sysConfig.fdfsHttpHost)) {
url = sysConfig.fdfsHttpHost + '/' + fastFileId
}
return url
}
/**
* 判断对象是否为空 * 判断对象是否为空
* @param obj * @param obj
* @returns {boolean} * @returns {boolean}
...@@ -370,3 +356,16 @@ export const formatDate = (date, fmt) => { ...@@ -370,3 +356,16 @@ export const formatDate = (date, fmt) => {
export const padLeftZero = (str) => { export const padLeftZero = (str) => {
return ('00' + str).substr(str.length) return ('00' + str).substr(str.length)
} }
/**
* 判断响应是否成功
* @param obj
* @returns {boolean}
*/
export const isSuccess = (response) => {
let success = true
if (!isNotEmpty(response) || (response.code !== undefined && response.code !== 200)) {
success = false
}
return success
}
...@@ -69,26 +69,13 @@ ...@@ -69,26 +69,13 @@
</template> </template>
<script> <script>
import { fetchList, addObj, putObj, delAttachment } from '@/api/admin/attachment' import { fetchList, addObj, putObj, delAttachment, getDownloadUrl } from '@/api/admin/attachment'
import waves from '@/directive/waves' import waves from '@/directive/waves'
import { getToken } from '@/utils/auth' // getToken from cookie import { getToken } from '@/utils/auth' // getToken from cookie
import { notifySuccess, messageSuccess } from '@/utils/util' import { notifySuccess, messageSuccess, isNotEmpty } from '@/utils/util'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import SpinnerLoading from '@/components/SpinnerLoading' import SpinnerLoading from '@/components/SpinnerLoading'
const calendarTypeOptions = [
{ key: 'CN', display_name: 'China' },
{ key: 'US', display_name: 'USA' },
{ key: 'JP', display_name: 'Japan' },
{ key: 'EU', display_name: 'Eurozone' }
]
// arr to obj ,such as { CN : "China", US : "USA" }
const calendarTypeKeyValue = calendarTypeOptions.reduce((acc, cur) => {
acc[cur.key] = cur.display_name
return acc
}, {})
export default { export default {
name: 'AttachmentManagement', name: 'AttachmentManagement',
components: { components: {
...@@ -108,9 +95,6 @@ export default { ...@@ -108,9 +95,6 @@ export default {
statusFilter (status) { statusFilter (status) {
return status === '0' ? '启用' : '禁用' return status === '0' ? '启用' : '禁用'
}, },
typeFilter (type) {
return calendarTypeKeyValue[type]
},
attachmentTypeFilter (type) { attachmentTypeFilter (type) {
let attachType let attachType
if (type === '1') { if (type === '1') {
...@@ -229,7 +213,11 @@ export default { ...@@ -229,7 +213,11 @@ export default {
}) })
}, },
handleDownload (row) { handleDownload (row) {
window.location.href = '/api/user/v1/attachment/download?id=' + row.id getDownloadUrl(row.id).then(response => {
if (isNotEmpty(response.data)) {
window.open('http://' + response.data.data, '_blank')
}
})
}, },
updateData () { updateData () {
this.$refs['dataForm'].validate((valid) => { this.$refs['dataForm'].validate((valid) => {
......
...@@ -155,3 +155,4 @@ export default { ...@@ -155,3 +155,4 @@ export default {
} }
} }
</script> </script>
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
<svg-icon icon-class="tab" class-name="card-panel-icon" /> <svg-icon icon-class="tab" class-name="card-panel-icon" />
</div> </div>
<div class="card-panel-description"> <div class="card-panel-description">
<div class="card-panel-text">及格率</div> <div class="card-panel-text">课程数</div>
<count-to :start-val="0" :end-val="13600" :duration="2600" class="card-panel-num"/> <count-to :start-val="0" :end-val="13600" :duration="2600" class="card-panel-num"/>
</div> </div>
</div> </div>
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
<script> <script>
import CountTo from 'vue-count-to' import CountTo from 'vue-count-to'
import { getDashboard } from '@/api/admin/sys' import { getDashboard } from '@/api/admin/sys'
import { isNotEmpty } from '@/utils/util' import { isNotEmpty, isSuccess, messageFail } from '@/utils/util'
export default { export default {
components: { components: {
...@@ -73,7 +73,7 @@ export default { ...@@ -73,7 +73,7 @@ export default {
}, },
getDashboardData () { getDashboardData () {
getDashboard().then(response => { getDashboard().then(response => {
if (isNotEmpty(response.data) && isNotEmpty(response.data.data)) { if (isSuccess(response)) {
const data = response.data.data const data = response.data.data
if (isNotEmpty(data.onlineUserNumber)) { if (isNotEmpty(data.onlineUserNumber)) {
this.onlineUserNumber = parseInt(data.onlineUserNumber) this.onlineUserNumber = parseInt(data.onlineUserNumber)
...@@ -95,7 +95,6 @@ export default { ...@@ -95,7 +95,6 @@ export default {
<style rel="stylesheet/scss" lang="scss" scoped> <style rel="stylesheet/scss" lang="scss" scoped>
.panel-group { .panel-group {
margin-top: 18px;
.card-panel-col{ .card-panel-col{
margin-bottom: 32px; margin-bottom: 32px;
} }
...@@ -107,8 +106,10 @@ export default { ...@@ -107,8 +106,10 @@ export default {
overflow: hidden; overflow: hidden;
color: #666; color: #666;
background: #fff; background: #fff;
box-shadow: 4px 4px 40px rgba(0, 0, 0, .05); box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
border-color: rgba(0, 0, 0, .05); border-color: rgba(0, 0, 0, .05);
border-radius: 6px;
border-top: 3px solid #40c9c6;
&:hover { &:hover {
.card-panel-icon-wrapper { .card-panel-icon-wrapper {
color: #fff; color: #fff;
...@@ -116,13 +117,13 @@ export default { ...@@ -116,13 +117,13 @@ export default {
.icon-people { .icon-people {
background: #40c9c6; background: #40c9c6;
} }
.icon-message { .icon-form {
background: #36a3f7; background: #36a3f7;
} }
.icon-money { .icon-chart {
background: #f4516c; background: #f4516c;
} }
.icon-shoppingCard { .icon-tab {
background: #34bfa3 background: #34bfa3
} }
} }
......
...@@ -3,24 +3,44 @@ ...@@ -3,24 +3,44 @@
<panel-group @handleSetLineChartData="handleSetLineChartData"/> <panel-group @handleSetLineChartData="handleSetLineChartData"/>
<el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;"> <el-row class="chart-wrapper" style="background:#fff;padding:16px 16px 0;margin-bottom:32px;">
<line-chart :chart-data="lineChartData"/> <div class="chart-wrapper-header">
<div>趋势</div>
</div>
<div class="chart-wrapper-body">
<line-chart :chart-data="lineChartData"/>
</div>
</el-row> </el-row>
<el-row :gutter="32"> <el-row :gutter="32">
<el-col :xs="24" :sm="24" :lg="8"> <el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper"> <div class="chart-wrapper">
<raddar-chart/> <div class="chart-wrapper-header">
<div>趋势</div>
</div>
<div class="chart-wrapper-body">
<raddar-chart/>
</div>
</div> </div>
</el-col> </el-col>
<el-col :xs="24" :sm="24" :lg="8"> <el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper"> <div class="chart-wrapper">
<pie-chart/> <div class="chart-wrapper-header">
<div>趋势</div>
</div>
<div class="chart-wrapper-body">
<pie-chart/>
</div>
</div> </div>
</el-col> </el-col>
<el-col :xs="24" :sm="24" :lg="8"> <el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper"> <div class="chart-wrapper">
<bar-chart/> <div class="chart-wrapper-header">
<div>趋势</div>
</div>
<div class="chart-wrapper-body">
<bar-chart/>
</div>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
...@@ -78,12 +98,21 @@ export default { ...@@ -78,12 +98,21 @@ export default {
<style rel="stylesheet/scss" lang="scss" scoped> <style rel="stylesheet/scss" lang="scss" scoped>
.dashboard-editor-container { .dashboard-editor-container {
padding: 32px; padding: 20px;
background-color: rgb(240, 242, 245);
.chart-wrapper { .chart-wrapper {
background: #fff; background: #fff;
padding: 16px 16px 0; padding: 16px 16px 0;
margin-bottom: 32px; margin-bottom: 32px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
}
.chart-wrapper-header {
padding: 8px 12px;
border-bottom: 1px solid #EBEEF5;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.chart-wrapper-body {
padding-top: 20px;
} }
} }
</style> </style>
...@@ -169,6 +169,8 @@ export default { ...@@ -169,6 +169,8 @@ export default {
setTimeout(() => { setTimeout(() => {
this.listLoading = false this.listLoading = false
}, 500) }, 500)
}).catch(() => {
this.listLoading = false
}) })
}, },
handleFilter () { handleFilter () {
......
...@@ -656,6 +656,8 @@ export default { ...@@ -656,6 +656,8 @@ export default {
setTimeout(() => { setTimeout(() => {
this.listLoading = false this.listLoading = false
}, 500) }, 500)
}).catch(() => {
this.listLoading = false
}) })
}, },
handleFilter () { handleFilter () {
......
...@@ -168,7 +168,7 @@ ...@@ -168,7 +168,7 @@
</div> </div>
</el-form> </el-form>
</el-col> </el-col>
<el-col :span="4"> <el-col :span="4" style="text-align: center;">
<span>选择题目</span> <span>选择题目</span>
<div class="answer-number"> <div class="answer-number">
<el-button class="number-btn" circle v-for="(value, index) in subjectIds" :key="index" @click="toSubject(index, value.subjectId, value.type)" >&nbsp;{{index + 1}}&nbsp;</el-button> <el-button class="number-btn" circle v-for="(value, index) in subjectIds" :key="index" @click="toSubject(index, value.subjectId, value.type)" >&nbsp;{{index + 1}}&nbsp;</el-button>
...@@ -318,6 +318,8 @@ export default { ...@@ -318,6 +318,8 @@ export default {
setTimeout(() => { setTimeout(() => {
this.listLoading = false this.listLoading = false
}, 500) }, 500)
}).catch(() => {
this.listLoading = false
}) })
}, },
handleFilter () { handleFilter () {
...@@ -499,7 +501,6 @@ export default { ...@@ -499,7 +501,6 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" rel="stylesheet/scss" scoped> <style lang="scss" rel="stylesheet/scss" scoped>
/* 题目 */
.subject-title { .subject-title {
color: #333333; color: #333333;
font-size: 18px; font-size: 18px;
...@@ -517,7 +518,6 @@ export default { ...@@ -517,7 +518,6 @@ export default {
text-align: right; text-align: right;
} }
} }
/* 题目选项 */
.subject-option { .subject-option {
padding-bottom: 10px; padding-bottom: 10px;
padding-left: 10px; padding-left: 10px;
...@@ -526,13 +526,14 @@ export default { ...@@ -526,13 +526,14 @@ export default {
margin: 20px; margin: 20px;
} }
.subject-content { .subject-content {
background: #F6F7FA; height: 300px;
border-radius: 4px; border-radius: 4px;
margin-bottom: 21px;
padding: 12px 0 12px 22px; padding: 12px 0 12px 22px;
margin-bottom: 12px;
position: relative; position: relative;
color: #666666; color: #666666;
text-align: left; text-align: left;
box-shadow: 0 2px 8px 0 rgba(0,0,0,.1);
} }
.correct { .correct {
color: #F56C6C; color: #F56C6C;
...@@ -573,4 +574,10 @@ export default { ...@@ -573,4 +574,10 @@ export default {
} }
} }
} }
.user-info {
}
.answer {
padding: 6px;
}
</style> </style>
...@@ -186,6 +186,8 @@ export default { ...@@ -186,6 +186,8 @@ export default {
setTimeout(() => { setTimeout(() => {
this.listLoading = false this.listLoading = false
}, 500) }, 500)
}).catch(() => {
this.listLoading = false
}) })
}, },
handleFilter () { handleFilter () {
......
...@@ -572,6 +572,8 @@ export default { ...@@ -572,6 +572,8 @@ export default {
this.list = response.data.list this.list = response.data.list
this.total = parseInt(response.data.total) this.total = parseInt(response.data.total)
this.listLoading = false this.listLoading = false
}).catch(() => {
this.listLoading = false
}) })
}, },
// 点击新建题目 // 点击新建题目
......
<template> <template>
<div class="menu-wrapper"> <div class="menu-wrapper">
<div style="background-color: rgb(48, 65, 86);">
<div class="ams-block-component ams-block" style="color: rgb(255, 255, 255); font-size: 30px; text-align: center; margin-bottom: 8px; font-family: Roboto; padding-top: 10px;">
硕果云<div class="ams-operations el-form--inline"></div>
</div>
</div>
<template v-for="(item, index) in menu"> <template v-for="(item, index) in menu">
<el-menu-item v-if="item.children.length===0 " :index="filterPath(item.path,index)" :key="item.label" @click="open(item)"> <el-menu-item v-if="item.children.length === 0 " :index="filterPath(item.path,index)" :key="item.label" @click="open(item)">
<svg-icon :icon-class="item.icon" /> <svg-icon :icon-class="item.icon" />
<span slot="title">{{ item.label }}</span> <span slot="title">{{ item.label }}</span>
</el-menu-item> </el-menu-item>
...@@ -12,7 +17,7 @@ ...@@ -12,7 +17,7 @@
<span slot="title" :class="{'el-menu--display':isCollapse}">{{ item.label }}</span> <span slot="title" :class="{'el-menu--display':isCollapse}">{{ item.label }}</span>
</template> </template>
<template v-for="(child,cindex) in item.children"> <template v-for="(child,cindex) in item.children">
<el-menu-item v-if="child.children.length==0" :index="filterPath(child.path,cindex)" :key="cindex" @click="open(child)"> <el-menu-item v-if="child.children.length === 0" :index="filterPath(child.path,cindex)" :key="cindex" @click="open(child)">
<span slot="title">{{ child.label }}</span> <span slot="title">{{ child.label }}</span>
</el-menu-item> </el-menu-item>
<sidebar-item v-else :menu="[child]" :key="cindex" :is-collapse="isCollapse"/> <sidebar-item v-else :menu="[child]" :key="cindex" :is-collapse="isCollapse"/>
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
:show-file-list="false" :show-file-list="false"
:on-success="handleAvatarSuccess" :on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload" :before-upload="beforeAvatarUpload"
:action="sysConfig.uploadUrl" action="api/user/v1/attachment/upload"
:headers="headers" :headers="headers"
:data="params" :data="params"
class="avatar-uploader"> class="avatar-uploader">
......
...@@ -212,6 +212,8 @@ export default { ...@@ -212,6 +212,8 @@ export default {
setTimeout(() => { setTimeout(() => {
this.listLoading = false this.listLoading = false
}, 500) }, 500)
}).catch(() => {
this.listLoading = false
}) })
}, },
handleFilter () { handleFilter () {
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<el-button v-if="menu_btn_export" icon="el-icon-download" plain @click="handleExport">{{ $t('table.export') }}</el-button> <el-button v-if="menu_btn_export" icon="el-icon-download" plain @click="handleExport">{{ $t('table.export') }}</el-button>
<el-row> <el-row>
<el-col :span="5" style ="margin-top:10px;"> <el-col :span="4" style ="margin-top:10px;">
<el-tree <el-tree
ref="tree" ref="tree"
:data="treeData" :data="treeData"
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
@node-collapse="nodeCollapse" @node-collapse="nodeCollapse"
/> />
</el-col> </el-col>
<el-col :span="19" style="margin-top:10px;"> <el-col :span="20" style="margin-top:10px;">
<el-card class="box-card"> <el-card class="box-card">
<el-form ref="form" :rules="rules" :label-position="labelPosition" :model="form" label-width="100px" style="width: 90%;"> <el-form ref="form" :rules="rules" :label-position="labelPosition" :model="form" label-width="100px" style="width: 90%;">
<el-row> <el-row>
......
...@@ -207,6 +207,8 @@ export default { ...@@ -207,6 +207,8 @@ export default {
setTimeout(() => { setTimeout(() => {
this.listLoading = false this.listLoading = false
}, 500) }, 500)
}).catch(() => {
this.listLoading = false
}) })
}, },
handleFilter () { handleFilter () {
......
...@@ -212,6 +212,8 @@ export default { ...@@ -212,6 +212,8 @@ export default {
setTimeout(() => { setTimeout(() => {
this.listLoading = false this.listLoading = false
}, 500) }, 500)
}).catch(() => {
this.listLoading = false
}) })
}, },
handleFilter () { handleFilter () {
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
ref="multipleTable" ref="multipleTable"
:key="tableKey" :key="tableKey"
:data="list" :data="list"
highlight-current-row highligidStringht-current-row
style="width: 100%;" style="width: 100%;"
@cell-dblclick="handleUpdate" @cell-dblclick="handleUpdate"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
...@@ -44,16 +44,6 @@ ...@@ -44,16 +44,6 @@
<span v-for="role in scope.row.roleList" :key="role.id">{{ role.roleName }} </span> <span v-for="role in scope.row.roleList" :key="role.id">{{ role.roleName }} </span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('table.phone')">
<template slot-scope="scope">
<span>{{ scope.row.phone }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.email')">
<template slot-scope="scope">
<span>{{ scope.row.email }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.status')"> <el-table-column :label="$t('table.status')">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tag :type="scope.row.status | statusTypeFilter">{{ scope.row.status | statusFilter }}</el-tag> <el-tag :type="scope.row.status | statusTypeFilter">{{ scope.row.status | statusFilter }}</el-tag>
...@@ -399,6 +389,8 @@ export default { ...@@ -399,6 +389,8 @@ export default {
setTimeout(() => { setTimeout(() => {
this.listLoading = false this.listLoading = false
}, 500) }, 500)
}).catch(() => {
this.listLoading = false
}) })
}, },
handleFilter () { handleFilter () {
...@@ -550,7 +542,7 @@ export default { ...@@ -550,7 +542,7 @@ export default {
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
exportObj({ idString: '' }).then(response => { exportObj([]).then(response => {
// 导出Excel // 导出Excel
exportExcel(response) exportExcel(response)
}) })
......
// 网关地址 // 网关地址
const GATEWAY_HOST = process.env.GATEWAY_HOST || '127.0.0.1' const GATEWAY_HOST = process.env.GATEWAY_HOST || 'localhost'
const GATEWAY_PORT = process.env.GATEWAY_PORT || '9180' const GATEWAY_PORT = process.env.GATEWAY_PORT || '9180'
// 转发配置 // 转发配置
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>系统演示</title> <title>硕果云教务</title>
</head> </head>
<body> <body>
<script src=/static/tinymce4.7.5/tinymce.min.js></script> <script src=/static/tinymce4.7.5/tinymce.min.js></script>
......
This source diff could not be displayed because it is too large. You can view the blob instead.
{ {
"name": "spring-microservice-exam-web", "name": "spring-microservice-exam-web",
"version": "3.4.0", "version": "3.5.0",
"description": "spring-microservice-exam-web", "description": "spring-microservice-exam-web",
"author": "tangyi <1633736729@qq.com>", "author": "tangyi <1633736729@qq.com>",
"private": true, "private": true,
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"vue": "^2.5.2", "vue": "^2.5.2",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-spinkit": "^1.4.1",
"vue2-animate": "^2.1.0", "vue2-animate": "^2.1.0",
"vue2-countdown": "^1.0.8", "vue2-countdown": "^1.0.8",
"vuex": "^3.0.1" "vuex": "^3.0.1"
......
...@@ -3,19 +3,14 @@ body { ...@@ -3,19 +3,14 @@ body {
margin: 0; margin: 0;
position: relative; position: relative;
overflow-x: hidden; overflow-x: hidden;
font-family: "Roboto Slab","Helvetica Neue",Helvetica,Arial,sans-serif; font-family: FZLTHJW--GB1-0,FZLTHJW--GB1;
line-height: 1.8; line-height: 1.8;
} }
a { a {
text-decoration: none; text-decoration: none;
outline: none; outline: none;
color: #555; color: #FFFFFF;
}
.header-w {
width: 1220px;
margin: 0 auto;
} }
.pr { .pr {
...@@ -62,24 +57,48 @@ a { ...@@ -62,24 +57,48 @@ a {
box-shadow: 0 3px 8px -6px rgba(0, 0, 0, .1); box-shadow: 0 3px 8px -6px rgba(0, 0, 0, .1);
} }
%home-common {
max-width: 1220px;
margin: auto auto 30px;
font-family: FZLTHJW--GB1-0,FZLTHJW--GB1;
text-align: center;
}
%message-common { %message-common {
padding-top: 10px; font-size: 48px;
height: 120px;
color: #fff;
background-color: #fff;
text-align: center; text-align: center;
font-weight: 400;
color: #4f403b;
line-height: 55px;
background-size: 236px;
background-repeat: no-repeat;
background-position: bottom;
padding-bottom: 25px;
margin-bottom: 50px;
} }
%img-common { %img-common {
color: #fff; -webkit-box-flex: 1;
background-color: #fff; flex: 1;
text-align: center; position: relative;
align-items: center; z-index: 1;
} }
.app-container { .app-container {
padding: 20px; padding: 20px;
} }
.common-container {
margin-top: 30px;
}
.pagination-container { .pagination-container {
margin-top: 30px; margin-top: 30px;
} }
%content-container {
margin-top: 70px;
}
.el-button {
margin: 2px;
}
@import "common"; @import "common";
#bg {
width: 100%;
margin: auto;
}
#home-msg {
font-size: 40px;
color: #3a3a3a !important;
position: absolute;
top: 120px;
text-align: center;
width: 100%;
}
/* 注册 */ /* 注册 */
.register-now { .register-now {
font-size: 20px; font-size: 20px;
...@@ -27,6 +15,15 @@ ...@@ -27,6 +15,15 @@
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
} }
.features {
@extend %home-common;
}
.scenes {
@extend %home-common;
}
/* 产品特色 */ /* 产品特色 */
.features-msg { .features-msg {
@extend %message-common; @extend %message-common;
...@@ -37,20 +34,6 @@ ...@@ -37,20 +34,6 @@
@extend %img-common; @extend %img-common;
} }
.msg-text {
font-size: 18px;
color: #178dff;
margin-top: 29px;
margin-bottom: 21px;
text-align: center;
}
.msg-text-detail {
font-size: 14px;
color: #474747;
text-align: center;
}
/* 适用场景 */ /* 适用场景 */
.scenes-msg { .scenes-msg {
@extend %message-common; @extend %message-common;
...@@ -66,14 +49,13 @@ ...@@ -66,14 +49,13 @@
} }
.function-img { .function-img {
@extend %img-common; @extend %img-common;
margin-bottom: 20px;
} }
/* 返回顶部 */ /* 返回顶部 */
.go-top-box { .go-top-box {
background-color: #fff; background-color: #fff;
position: fixed; position: fixed;
right: 100px; right: 50px;
bottom: 150px; bottom: 150px;
width: 40px; width: 40px;
height: 40px; height: 40px;
......
<template>
<div class="outer">
<Spinner name="three-bounce" color="#409EFF" fadeIn="quarter"/>
</div>
</template>
<style>
.outer{
height: 100%;
width: 100%;
position: absolute;
background-color: rgba(255,255,255,0.7);
z-index: 1100;
text-align: center;
}
.sk-spinner[data-v-39432f99]{
margin-top: 250px;
}
</style>
<script>
import Spinner from 'vue-spinkit'
export default {
components: {
Spinner
}
}
</script>
...@@ -39,7 +39,6 @@ router.beforeEach((to, from, next) => { ...@@ -39,7 +39,6 @@ router.beforeEach((to, from, next) => {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next() next()
} else { } else {
alert('请先登录')
next('/login') // 重定向到登录页 next('/login') // 重定向到登录页
NProgress.done() NProgress.done()
} }
......
import axios from 'axios' import axios from 'axios'
import store from '../store' import store from '../store'
import { getToken, setToken, getRefreshToken, getTenantCode } from '@/utils/auth' import { getToken, setToken, getRefreshToken, getTenantCode } from '@/utils/auth'
import { isNotEmpty } from '@/utils/util' import { isNotEmpty, isSuccess } from '@/utils/util'
import { refreshToken } from '@/api/admin/login' import { refreshToken } from '@/api/admin/login'
import { Message } from 'element-ui' import { Message } from 'element-ui'
import errorCode from '@/const/errorCode' import errorCode from '@/const/errorCode'
import NProgress from 'nprogress' // progress bar import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' import 'nprogress/nprogress.css'// progress bar style
// progress bar style
const whiteList = ['/auth/authentication/removeToken']// 白名单 const whiteList = ['/auth/authentication/removeToken']// 白名单
// 超时时间 // 超时时间
...@@ -16,7 +14,8 @@ axios.defaults.timeout = 30000 ...@@ -16,7 +14,8 @@ axios.defaults.timeout = 30000
// 跨域请求,允许保存cookie // 跨域请求,允许保存cookie
axios.defaults.withCredentials = true axios.defaults.withCredentials = true
NProgress.configure({ showSpinner: false })// NProgress Configuration NProgress.configure({ showSpinner: false })// NProgress Configuration
// HTTPrequest拦截
// HTTP request拦截
axios.interceptors.request.use(config => { axios.interceptors.request.use(config => {
NProgress.start() // start progress bar NProgress.start() // start progress bar
if (store.getters.access_token && whiteList.indexOf(config.url) === -1) { if (store.getters.access_token && whiteList.indexOf(config.url) === -1) {
...@@ -35,18 +34,20 @@ axios.interceptors.request.use(config => { ...@@ -35,18 +34,20 @@ axios.interceptors.request.use(config => {
return Promise.reject(error) return Promise.reject(error)
}) })
// HTTPresponse拦截 // HTTP response拦截
axios.interceptors.response.use(data => { axios.interceptors.response.use(data => {
NProgress.done() NProgress.done()
// 请求失败,弹出提示信息
if (!isSuccess(data.data)) {
Message({ message: data.data.msg, type: 'error' })
}
return data return data
}, error => { }, error => {
NProgress.done() NProgress.done()
if (error.response) { if (error.response) {
const originalRequest = error.config const originalRequest = error.config
const currentRefreshToken = getRefreshToken() const currentRefreshToken = getRefreshToken()
// 接口返回401 // 接口返回401并且已经重试过,自动刷新token
// 已经重试过
// 自动刷新token
if ((error.response.status === 401 || error.response.status === 403) && !originalRequest._retry && isNotEmpty(currentRefreshToken)) { if ((error.response.status === 401 || error.response.status === 403) && !originalRequest._retry && isNotEmpty(currentRefreshToken)) {
// 退出请求 // 退出请求
if (originalRequest.url.indexOf('removeToken') !== -1) { if (originalRequest.url.indexOf('removeToken') !== -1) {
...@@ -62,7 +63,7 @@ axios.interceptors.response.use(data => { ...@@ -62,7 +63,7 @@ axios.interceptors.response.use(data => {
return axios(originalRequest) return axios(originalRequest)
}).catch(() => { }).catch(() => {
// 刷新失败,执行退出 // 刷新失败,执行退出
store.dispatch('LogOut').then(() => { location.reload() }) store.dispatch('LogOut').then(() => location.reload())
}) })
} else if (error.response.status === 423) { } else if (error.response.status === 423) {
Message({ message: '演示环境不能操作', type: 'warning' }) Message({ message: '演示环境不能操作', type: 'warning' })
......
...@@ -149,7 +149,7 @@ export const isNotEmpty = (obj) => { ...@@ -149,7 +149,7 @@ export const isNotEmpty = (obj) => {
* @param duration * @param duration
*/ */
export const notify = (obj, title, msg, type, duration) => { export const notify = (obj, title, msg, type, duration) => {
obj.$notify({ title: title, message: msg, type: type, duration: duration }) obj.$notify({ title: title, message: msg, type: type, duration: duration, offset: 70})
} }
/** /**
...@@ -162,6 +162,15 @@ export const notifySuccess = (obj, msg) => { ...@@ -162,6 +162,15 @@ export const notifySuccess = (obj, msg) => {
} }
/** /**
* 警告提示
* @param obj
* @param msg
*/
export const notifyWarn = (obj, msg) => {
notify(obj, '警告', msg, 'warn', 2000)
}
/**
* 失败提示 * 失败提示
* @param obj * @param obj
* @param msg * @param msg
...@@ -209,3 +218,32 @@ export const formatDate = (date, fmt) => { ...@@ -209,3 +218,32 @@ export const formatDate = (date, fmt) => {
export const padLeftZero = (str) => { export const padLeftZero = (str) => {
return ('00' + str).substr(str.length) return ('00' + str).substr(str.length)
} }
/**
* 判断响应是否成功
* @param obj
* @returns {boolean}
*/
export const isSuccess = (response) => {
let success = true
if (!isNotEmpty(response) || (response.code !== undefined && response.code !== 200)) {
success = false
}
return success
}
/**
* 按指定长度截取字符串,超出部分显示...
* @param str
* @param len
* @returns {string}
*/
export const cropStr = (str, len) => {
let result = ''
if (isNotEmpty(str)) {
if (str.length > len) {
result = str.substring(0, len) + '...'
}
}
return result
}
<template> <template>
<div> <div>
<o-header></o-header> <o-header @handleSubmitExam="handleSubmitExam"></o-header>
<o-main></o-main> <o-main ref="mainRef"></o-main>
<o-footer></o-footer>
</div> </div>
</template> </template>
<script> <script>
import OHeader from './common/header' import OHeader from './common/header'
import OMain from './common/main' import OMain from './common/main'
import OFooter from './common/footer'
export default { export default {
data () { data () {
return {} return {}
}, },
components: { components: {
OHeader, OHeader,
OMain, OMain
OFooter },
methods: {
// 提交考试
handleSubmitExam() {
debugger
this.$refs.mainRef.handleSubmitExam()
}
} }
} }
</script> </script>
......
...@@ -4,57 +4,56 @@ ...@@ -4,57 +4,56 @@
<div class="site-info"> <div class="site-info">
<el-row> <el-row>
<el-col :span="4" :offset="2" class="footer-col"> <el-col :span="4" :offset="2" class="footer-col">
<h3 class="c1">服务支持</h3> <h4>链接</h4>
<ul> <a target="_blank" href="https://gitee.com/wells2333/spring-microservice-exam">码云</a>
<li class="c2"><a class="c3" target="_blank" href="https://gitee.com/wells2333/spring-microservice-exam">官方开源</a></li> <a target="_blank" href="https://github.com/">GITHUB</a>
<li class="c2"><a class="c3" target="_blank" href="https://gitee.com/wells2333">前后端项目</a></li> <a target="_blank" href="http://it99.club:81">管理后台</a>
</ul> <a target="_blank" href="https://gitee.com/wells2333/spring-microservice-exam/blob/master/CHANGELOG.md">更新日志</a>
</el-col> </el-col>
<el-col :span="4" class="footer-col"> <el-col :span="4" class="footer-col">
<h3 class="c1">自助服务</h3> <h4>工具</h4>
<ul> <a target="_blank" href="http://element-cn.eleme.io">Element Ui</a>
<li class="c2"><a class="c3" target="_blank" href="http://ehedgehog.net/">个人博客</a></li> <a target="_blank" href="https://cn.vuejs.org/">Vue</a>
<li class="c2"><a class="c3" target="_blank" href="https://gitee.com/wells2333">个人简介</a></li>
</ul>
</el-col> </el-col>
<el-col :span="4" class="footer-col"> <el-col :span="4" class="footer-col">
<h3 class="c1">其他项目</h3> <h4>社区</h4>
<ul> <a target="_blank" href="https://www.kancloud.cn/tangyi/spring-microservice-exam/1322864">看云</a>
<li class="c2"><a class="c3" target="_blank" href="http://spring.io/">开发框架</a></li> <a target="_blank" href="https://gitee.com/wells2333/spring-microservice-exam/issues">反馈建议</a>
<li class="c2"><a class="c3" target="_blank" href="https://gitee.com/wells2333">小程序工厂</a></li>
</ul>
</el-col> </el-col>
<el-col :span="4" class="footer-col"> <el-col :span="4" class="footer-col">
<h3 class="c1">友情链接</h3> <div class="we-chat">
<ul> 加入技术交流群,请扫二维码
<li class="c2"><a class="c3" target="_blank" href="https://cn.vuejs.org/">Vue</a></li> <br>
<li class="c2"><a class="c3" target="_blank" href="http://element-cn.eleme.io">element</a></li> (Spring Cloud技术交流群)
</ul> </div>
</el-col>
<el-col :span="4" class="footer-col">
<h3 class="c1">关注我</h3>
<ul>
<li class="c2"><a class="c3" target="_blank" href="http://wpa.qq.com/msgrd?v=3&uin=1633736729&site=qq&menu=yes">腾讯 QQ</a></li>
<li class="c2"><a class="c3" target="_blank" href="mailto:1633736729@qq.com">官方邮箱</a></li>
</ul>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
<div class="copyright"> <div class="line"></div>
<h4 class="content-c2">Copyright ©2019</h4>
<ul class="privacy"> <el-row :gutter="20">
<li class="content-c1"><a class="content-c0" @click="openLayer">法律声明</a></li> <el-col :span="12" :offset="8">
<li class="content-c1"><a class="content-c0" @click="openPrivacy">隐私条款</a></li> <div class="copyright">
<li class="content-c1"><a class="content-c0" target="_blank" href="javascript:void(-1);">开发者中心</a></li> <h4 class="content-c2" style="text-align: center;">Copyright ©2019</h4>
</ul> <ul class="privacy">
</div> <li class="content-c1"><a class="content-c0" @click="openLayer">法律声明</a></li>
<div class="cop"> <li class="content-c1"><a class="content-c0" @click="openPrivacy">隐私条款</a></li>
<a class="content-c3" href="https://gitee.com/wells2333" target="_blank"> <li class="content-c1"><a class="content-c0" target="_blank" href="https://gitee.com/wells2333/spring-microservice-exam">开发者中心</a></li>
<span class="content-c3">粤ICP备18038322号</span> </ul>
<span class="content-c3">粤ICP备18038322号-1</span> </div>
</a> </el-col>
</div> </el-row>
<el-row :gutter="20">
<el-col :span="12" :offset="6">
<div class="cop">
<a class="content-c3" href="https://gitee.com/wells2333" target="_blank">
<span class="content-c3">粤ICP备18038322号</span>
<span class="content-c3">粤ICP备18038322号-1</span>
</a>
</div>
</el-col>
</el-row>
</div> </div>
</div> </div>
</template> </template>
...@@ -68,20 +67,15 @@ export default { ...@@ -68,20 +67,15 @@ export default {
openLayer () { openLayer () {
this.$notify.info({ this.$notify.info({
title: '法律声明', title: '法律声明',
message: '此仅为个人练习开源模仿项目,仅供学习参考,承担不起任何法律问题' message: '此仅为个人练习开源模仿项目,仅供学习参考,承担不起任何法律问题',
offset: 70
}) })
}, },
openPrivacy () { openPrivacy () {
this.$notify.info({ this.$notify.info({
title: '隐私条款', title: '隐私条款',
message: '本网站将不会严格遵守有关法律法规和本隐私政策所载明的内容收集、使用您的信息' message: '本网站将不会严格遵守有关法律法规和本隐私政策所载明的内容收集、使用您的信息',
}) offset: 70
},
openHelp () {
this.$notify({
title: '离线帮助',
message: '没人会帮助你,请自己靠自己',
type: 'warning'
}) })
} }
}, },
...@@ -90,98 +84,30 @@ export default { ...@@ -90,98 +84,30 @@ export default {
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped> <style lang="scss" rel="stylesheet/scss" scoped>
.footer { .footer {
background: #f6f6f6; background-image: url("../../../static/images/home/footer-01.png");
height: 350px; background-position: 50%;
display: flex; background-color: #524970;
flex-direction: column; width: 100%;
align-items: center; box-sizing: border-box;
height: 450px;
} }
.container { .container {
width: 1220px; width: 1150px;
} margin: 0 auto;
.site-info {
height: 100px;
padding: 50px 0 130px;
border-bottom: 1px solid #e6e6e6;
position: relative; position: relative;
z-index: 1;
} }
.c1 {
color: #646464;
font-size: 12px;
padding: 0 0 14px;
}
.c2 {
color: #c3c3c3;
font-size: 12px;
padding: 6px 0;
list-style: none;
}
.c3 {
color: #969696;
}
.c4 {
position: absolute;
right: 0;
overflow: hidden;
line-height: 34px;
}
.tel {
font-size: 30px;
line-height: 1;
color: #646464;
top: -2px;
position: relative;
}
.c5 { .c5 {
color: #646464; color: #646464;
right: -70px; right: -70px;
position: relative; position: relative;
} }
.time {
margin-top: 5px;
right: -4px;
position: relative;
clear: both;
width: 241px;
font-size: 12px;
line-height: 18px;
color: #c3c3c3;
text-align: right;
}
.online {
clear: both;
width: 241px;
font-size: 12px;
line-height: 18px;
color: #c3c3c3;
text-align: right;
}
.button {
width: 130px;
height: 34px;
font-size: 14px;
color: #5079d9;
border: 1px solid #dcdcdc;
margin-top: 8px;
}
.copyright { .copyright {
color: #434d55; color: #434d55;
font-size: 12px; font-size: 12px;
padding: 10px 0 0; padding: 20px 0 0;
display: flex; display: flex;
align-items: left;
} }
.privacy { .privacy {
...@@ -210,24 +136,53 @@ export default { ...@@ -210,24 +136,53 @@ export default {
float: left; float: left;
height: 15px; height: 15px;
line-height: 15px; line-height: 15px;
color: #757575; color: #ffffff;
margin: 0 0 0 12px; margin: 0 0 0 12px;
} }
.cop { .cop {
clear: both; text-align: center;
height: 15px; height: 15px;
} }
.content-c3 { .content-c3 {
margin-right: 20px; margin-right: 20px;
color: #bdbdbd; color: #ffffff;
font-size: 12px; font-size: 12px;
height: 12px; height: 12px;
line-height: 1; line-height: 1;
} }
.footer-col ul { .footer-col {
padding-inline-start: 0px; width: 200px;
float: left;
font-family: FZLTHJW--GB1-0,FZLTHJW--GB1;
h4 {
color: #fff;
font-size: 30px;
font-weight: 400;
}
a {
display: block;
margin: 0;
line-height: 2.5;
font-size: 20px;
color: #bdb8ce;
font-weight: 400;
}
.we-chat {
position: absolute;
right: 0;
top: 80px;
width: 216px;
z-index: 2;
padding-top: 232px;
background-image: url("../../../static/images/home/WechatIMG4.png");
background-repeat: no-repeat;
background-position: top;
text-align: center;
color: #bdb8ce;
line-height: 22px;
}
} }
</style> </style>
<template> <template>
<section class="main"> <section class="main">
<transition name="fade-transform" mode="out-in"> <div class="main-wrapper">
<router-view :key="key"/> <div class="main-content">
</transition> <transition name="fade-transform" mode="out-in">
<router-view :key="key"/>
</transition>
</div>
</div>
</section> </section>
</template> </template>
...@@ -13,15 +17,33 @@ export default { ...@@ -13,15 +17,33 @@ export default {
key () { key () {
return this.$route.fullPath return this.$route.fullPath
} }
},
methods: {
// TODO 提交考试
handleSubmitExam() {
console.log('handleSubmitExam')
}
} }
} }
</script> </script>
<style scoped> <style lang="scss" rel="stylesheet/scss" scoped>
.main { .main {
min-height: calc(100vh - 454px); min-height: calc(100vh - 454px);
width: 100%; width: 100%;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
.main-wrapper {
max-width: none;
padding-left: 0;
padding-right: 0;
padding-bottom: 0;
.main-content {
margin-top: -2rem;
width: 100%;
overflow: hidden;
min-width: 1220px;
}
}
} }
</style> </style>
<template> <template>
<div class="app-container"> <div class="app-container">
<el-row class="exam-recode-msg">
<el-col :span="24" style="color: black;">
<h1>考试记录</h1>
</el-col>
</el-row>
<el-row> <el-row>
<el-col :span="20" :offset="2"> <el-col :span="20" :offset="2">
<el-table <el-table
...@@ -17,7 +12,7 @@ ...@@ -17,7 +12,7 @@
style="width: 100%;"> style="width: 100%;">
<el-table-column label="考试名称" align="center"> <el-table-column label="考试名称" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ scope.row.examinationName }}</span> <span :title="scope.row.examinationName">{{ scope.row.examinationName | examinationNameFilter }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="考试类型" min-width="90" align="center"> <el-table-column label="考试类型" min-width="90" align="center">
...@@ -81,7 +76,7 @@ ...@@ -81,7 +76,7 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { fetchList } from '@/api/exam/examRecord' import { fetchList } from '@/api/exam/examRecord'
import store from '@/store' import store from '@/store'
import { formatDate } from '@/utils/util' import { formatDate, cropStr } from '@/utils/util'
export default { export default {
filters: { filters: {
...@@ -113,6 +108,9 @@ export default { ...@@ -113,6 +108,9 @@ export default {
}, },
timeFilter (time) { timeFilter (time) {
return formatDate(new Date(time), 'yyyy-MM-dd hh:mm') return formatDate(new Date(time), 'yyyy-MM-dd hh:mm')
},
examinationNameFilter(name) {
return cropStr(name, 8)
} }
}, },
data () { data () {
...@@ -192,8 +190,8 @@ export default { ...@@ -192,8 +190,8 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" rel="stylesheet/scss" scoped> <style lang="scss" rel="stylesheet/scss" scoped>
@import "../../assets/css/common.scss"; @import "../../assets/css/common.scss";
.exam-recode-msg { .app-container {
@extend %message-common; @extend .common-container;
} }
.incorrect-answer-gray-box { .incorrect-answer-gray-box {
......
<template> <template>
<div class="app-container"> <div class="app-container">
<el-row class="incorrect-msg">
<el-col :span="24" style="color: black;">
<h1>错题</h1>
</el-col>
</el-row>
<el-row> <el-row>
<el-col :span="20" :offset="2"> <el-col :span="20" :offset="2">
<div class="subject-content" v-for="(tempIncorrectAnswer, index) in list" :key="tempIncorrectAnswer.id"> <div class="subject-content" v-for="(tempIncorrectAnswer, index) in list" :key="tempIncorrectAnswer.id">
...@@ -54,6 +49,8 @@ ...@@ -54,6 +49,8 @@
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { getAnswerListInfo } from '@/api/exam/answer' import { getAnswerListInfo } from '@/api/exam/answer'
import { isNotEmpty, notifyFail, notifyWarn, getAttachmentPreviewUrl, formatDate } from '@/utils/util'
export default { export default {
data () { data () {
...@@ -88,12 +85,7 @@ export default { ...@@ -88,12 +85,7 @@ export default {
this.total = response.data.total this.total = response.data.total
this.listLoading = false this.listLoading = false
}).catch(() => { }).catch(() => {
this.$notify({ notifyFail(this, '加载错题失败')
title: '失败',
message: '加载错题失败',
type: 'error',
duration: 2000
})
}) })
}, },
handleSizeChange (val) { handleSizeChange (val) {
...@@ -121,8 +113,8 @@ export default { ...@@ -121,8 +113,8 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" rel="stylesheet/scss" scoped> <style lang="scss" rel="stylesheet/scss" scoped>
@import "../../assets/css/common.scss"; @import "../../assets/css/common.scss";
.incorrect-msg { .app-container {
@extend %message-common; @extend .common-container;
} }
.incorrect-answer-gray-box { .incorrect-answer-gray-box {
......
<template> <template>
<div> <div>
<el-row class="practice-msg">
<el-col :span="24" style="color: black;">
<h1>在线学习</h1>
</el-col>
</el-row>
<div class="courses" v-loading="listLoading"> <div class="courses" v-loading="listLoading">
<el-row v-for="practice in practices" :key="practice.id"> <el-row v-for="practice in practices" :key="practice.id">
<el-col :offset="8"> <el-col :offset="8">
......
<template> <template>
<div> <div class="subject-box">
<el-row :gutter="30"> <el-row :gutter="30">
<el-col :span="18" :offset="2"> <el-col :span="18" :offset="2">
<el-card class="subject-box-card" v-loading="loading"> <el-card class="subject-box-card" v-loading="loading">
<div class="subject-exam-title" v-if="!loading && tempSubject.id !== ''">{{exam.examinationName}}(共{{subjectIds.length}}题,合计{{exam.totalScore}}分)</div> <div class="subject-exam-title" v-if="!loading && tempSubject.id !== ''">{{exam.examinationName}}(共{{subjectIds.length}}题,合计{{exam.totalScore}}分)</div>
<div class="subject-content" v-if="!loading && tempSubject.id !== ''"> <div class="subject-content" v-if="!loading && tempSubject.id !== ''">
<div class="subject-title"> <div class="subject-title">
<span class="subject-title-content" v-html="tempSubject.subjectName"></span> <span class="subject-title-content" v-html="tempSubject.subjectName"/>
<span class="subject-title-content">&nbsp;({{tempSubject.score}})分</span> <span class="subject-title-content">&nbsp;({{tempSubject.score}})分</span>
</div> </div>
<ul v-if="tempSubject.type === 0" class="subject-options"> <ul v-if="tempSubject.type === 0" class="subject-options">
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
</div> </div>
</div> </div>
<div class="current-progress"> <div class="current-progress">
当前进度: {{subjectIds.length}}/{{subjectIds.length}} 当前进度: {{subjectIndex}}/{{subjectIds.length}}
</div> </div>
<div class="answer-card"> <div class="answer-card">
<el-button type="text" icon="el-icon-date" @click="answerCard">答题卡</el-button> <el-button type="text" icon="el-icon-date" @click="answerCard">答题卡</el-button>
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
<div class="answer-card-title" >{{exam.examinationName}}(共{{subjectIds.length}}题,合计{{exam.totalScore}}分)</div> <div class="answer-card-title" >{{exam.examinationName}}(共{{subjectIds.length}}题,合计{{exam.totalScore}}分)</div>
<div class="answer-card-split"></div> <div class="answer-card-split"></div>
<el-row class="answer-card-content"> <el-row class="answer-card-content">
<el-button circle v-for="(value, index) in subjectIds" :key="index" @click="toSubject(value.subjectId, value.type)" >&nbsp;{{index + 1}}&nbsp;</el-button> <el-button circle v-for="(value, index) in subjectIds" :key="index" @click="toSubject(value.subjectId, value.type, index)" >&nbsp;{{index + 1}}&nbsp;</el-button>
</el-row> </el-row>
</el-dialog> </el-dialog>
</div> </div>
...@@ -72,7 +72,7 @@ import { getSubjectIds } from '@/api/exam/exam' ...@@ -72,7 +72,7 @@ import { getSubjectIds } from '@/api/exam/exam'
import { getCurrentTime } from '@/api/exam/examRecord' import { getCurrentTime } from '@/api/exam/examRecord'
import store from '@/store' import store from '@/store'
import moment from 'moment' import moment from 'moment'
import { notifySuccess, notifyFail, isNotEmpty } from '@/utils/util' import { notifySuccess, notifyFail, notifyWarn, isNotEmpty } from '@/utils/util'
import Tinymce from '@/components/Tinymce' import Tinymce from '@/components/Tinymce'
export default { export default {
...@@ -87,6 +87,7 @@ export default { ...@@ -87,6 +87,7 @@ export default {
startTime: 0, startTime: 0,
endTime: 0, endTime: 0,
disableSubmit: true, disableSubmit: true,
subjectIndex: 1,
tempSubject: { tempSubject: {
id: null, id: null,
examinationId: null, examinationId: null,
...@@ -128,7 +129,7 @@ export default { ...@@ -128,7 +129,7 @@ export default {
type: 0 type: 0
}, },
subjectIds: [], subjectIds: [],
subjectStartTime: undefined subjectStartTime: undefined,
} }
}, },
computed: { computed: {
...@@ -153,12 +154,7 @@ export default { ...@@ -153,12 +154,7 @@ export default {
}, },
methods: { methods: {
countDownS_cb: function (x) { countDownS_cb: function (x) {
this.$notify({ notifyWarn(this, '考试开始')
title: '提示',
message: '考试开始',
type: 'warn',
duration: 2000
})
}, },
// 开始考试 // 开始考试
startExam () { startExam () {
...@@ -166,25 +162,19 @@ export default { ...@@ -166,25 +162,19 @@ export default {
getCurrentTime().then(response => { getCurrentTime().then(response => {
const currentTime = moment(response.data.data) const currentTime = moment(response.data.data)
if (currentTime.isAfter(this.exam.endTime)) { if (currentTime.isAfter(this.exam.endTime)) {
this.$notify({ notifyWarn(this, '考试已结束')
title: '提示',
message: '考试已结束',
type: 'warn',
duration: 2000
})
} else if (currentTime.isBefore(this.exam.startTime)) { } else if (currentTime.isBefore(this.exam.startTime)) {
// 考试未开始 // 考试未开始
this.$notify({ notifyWarn(this, '考试未开始')
title: '提示',
message: '考试未开始',
type: 'warn',
duration: 2000
})
} else { } else {
// 获取考试的题目数量 // 获取考试的题目数量
getSubjectIds(this.exam.id).then(response => { getSubjectIds(this.exam.id).then(subjectResponse => {
// 题目数 // 题目数
this.subjectIds = response.data.data for (let i = 0; i < subjectResponse.data.data.length; i++) {
let { subjectId, type } = subjectResponse.data.data[i];
this.subjectIds.push({subjectId, type, index: i + 1})
}
this.updateSubjectIndex()
}) })
// 获取服务器的当前时间 // 获取服务器的当前时间
this.currentTime = currentTime.valueOf() this.currentTime = currentTime.valueOf()
...@@ -207,12 +197,7 @@ export default { ...@@ -207,12 +197,7 @@ export default {
}, },
// 考试结束 // 考试结束
countDownE_cb: function (x) { countDownE_cb: function (x) {
this.$notify({ notifyWarn(this, '考试结束')
title: '提示',
message: '考试结束',
type: 'warn',
duration: 2000
})
this.disableSubmit = true this.disableSubmit = true
this.loading = false this.loading = false
}, },
...@@ -237,7 +222,7 @@ export default { ...@@ -237,7 +222,7 @@ export default {
if (response.data.data === null) { if (response.data.data === null) {
if (nextType === 0) { if (nextType === 0) {
notifySuccess(this, '已经是最后一题了') notifySuccess(this, '已经是最后一题了')
} else { } else if (nextType === 1) {
notifySuccess(this, '已经是第一题了') notifySuccess(this, '已经是第一题了')
} }
} else { } else {
...@@ -248,7 +233,9 @@ export default { ...@@ -248,7 +233,9 @@ export default {
this.answer = isNotEmpty(this.tempAnswer) ? this.tempAnswer.answer : '' this.answer = isNotEmpty(this.tempAnswer) ? this.tempAnswer.answer : ''
// 保存题目答案到sessionStorage // 保存题目答案到sessionStorage
this.subject.answer = this.tempAnswer this.subject.answer = this.tempAnswer
this.tempAnswer.index = this.subjectIndex
store.dispatch('SetSubjectInfo', this.tempSubject).then(() => {}) store.dispatch('SetSubjectInfo', this.tempSubject).then(() => {})
this.updateSubjectIndex()
} }
this.loading = false this.loading = false
this.subjectStartTime = new Date() this.subjectStartTime = new Date()
...@@ -262,9 +249,10 @@ export default { ...@@ -262,9 +249,10 @@ export default {
this.dialogVisible = true this.dialogVisible = true
}, },
// 跳转题目 // 跳转题目
toSubject (subjectId, subjectType) { toSubject (subjectId, subjectType, index) {
// 保存当前题目,同时加载下一题 // 保存当前题目,同时加载下一题
this.saveCurrentSubjectAndGetNextSubject(2, subjectId, subjectType) this.saveCurrentSubjectAndGetNextSubject(2, subjectId, subjectType)
this.subjectIndex = index + 1
this.dialogVisible = false this.dialogVisible = false
}, },
// 提交 // 提交
...@@ -274,22 +262,27 @@ export default { ...@@ -274,22 +262,27 @@ export default {
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
let answerId = isNotEmpty(this.tempAnswer) ? this.tempAnswer.id : '' this.doSubmitExam(this.tempAnswer, this.exam, this.examRecord, this.userInfo, true)
// 构造答案 })
let answer = this.getAnswer(answerId) },
saveAndNext(answer, 0).then(response => { doSubmitExam(submitAnswer, submitExam, submitExamRecord, userInfo, toExamRecord) {
// 提交到后台 let answerId = isNotEmpty(submitAnswer) ? submitAnswer.id : ''
store.dispatch('SubmitExam', { examinationId: this.exam.id, examRecordId: this.examRecord.id, userId: this.userInfo.id }).then(() => { // 构造答案
notifySuccess(this, '提交成功') let answer = this.getAnswer(answerId)
// 禁用提交按钮 saveAndNext(answer, 0).then(response => {
this.disableSubmit = true // 提交到后台
store.dispatch('SubmitExam', { examinationId: submitExam.id, examRecordId: submitExamRecord.id, userId: userInfo.id }).then(() => {
notifySuccess(this, '提交成功')
// 禁用提交按钮
this.disableSubmit = true
if (toExamRecord) {
this.$router.push({name: 'exam-record'}) this.$router.push({name: 'exam-record'})
}).catch(() => { }
notifyFail(this, '提交失败')
})
}).catch(() => { }).catch(() => {
notifyFail(this, '提交题目失败') notifyFail(this, '提交失败')
}) })
}).catch(() => {
notifyFail(this, '提交题目失败')
}) })
}, },
// 选中选项 // 选中选项
...@@ -308,6 +301,20 @@ export default { ...@@ -308,6 +301,20 @@ export default {
type: this.tempSubject.type, type: this.tempSubject.type,
startTime: this.subjectStartTime startTime: this.subjectStartTime
} }
},
// 获取题目索引
getSubjectIndex(targetId) {
for (let subject of this.subjectIds) {
let { subjectId, index } = subject;
if (subjectId === targetId) {
return index
}
}
return 1
},
// 更新题目索引
updateSubjectIndex() {
this.subjectIndex = this.getSubjectIndex(this.tempSubject.id)
} }
} }
} }
...@@ -319,6 +326,9 @@ export default { ...@@ -319,6 +326,9 @@ export default {
.start-exam-msg { .start-exam-msg {
@extend %message-common; @extend %message-common;
} }
.subject-box {
margin-top: 50px;
}
.subject-box-card { .subject-box-card {
margin-bottom: 30px; margin-bottom: 30px;
min-height: 400px; min-height: 400px;
......
...@@ -336,12 +336,14 @@ export default { ...@@ -336,12 +336,14 @@ export default {
<style rel="stylesheet/scss" lang="scss"> <style rel="stylesheet/scss" lang="scss">
#bg_svg { #bg_svg {
position: fixed; position: fixed;
top: -10px;
left: 0; left: 0;
top: 10px;
width: 100%; width: 100%;
height: 600px;
z-index: -1; z-index: -1;
} }
.bg { .bg {
margin-top: 30px;
height: 100%; height: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment